Skip to content

Commit

Permalink
fix: pass logging config to brokers
Browse files Browse the repository at this point in the history
  • Loading branch information
kieran-mackle committed Mar 9, 2024
1 parent aae4f74 commit 1eb06b5
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 62 deletions.
30 changes: 10 additions & 20 deletions autotrader/autotrader.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,6 @@ def __init__(self) -> None:
self._broker: Union[dict[str, Broker], Broker] = None # Broker instance(s)
self._brokers_dict = None # Dictionary of brokers
self._broker_name = "" # Broker name(s)
self._broker_verbosity = 0 # Broker verbosity
self._environment: Literal["paper", "live"] = "paper" # Trading environment
self._account_id = None # Trading account
self._base_currency = None
Expand Down Expand Up @@ -205,7 +204,6 @@ def configure(
jupyter_notebook: Optional[bool] = False,
global_config: Optional[dict] = None,
instance_str: Optional[str] = None,
broker_verbosity: Optional[int] = 0,
home_currency: Optional[str] = None,
deploy_time: Optional[datetime] = None,
logger_kwargs: Optional[dict] = None,
Expand All @@ -232,10 +230,9 @@ def configure(
feed : str, optional
The data feed to be used. This can be the same as the broker
being used, or another data source. Options include 'yahoo',
'oanda', 'ib', 'dydx', 'ccxt', 'local' or 'none'. When data is provided
via the add_data method, the feed is automatically set to 'local'. If
"none" is specified (note: str, not a NoneType object), OHLCV data will
not be fetched. The default is None.
'oanda', 'ib', 'ccxt:exchange', 'local'. When data is provided
via the add_data method, the feed is automatically set to 'local'.
The default is None.
notify : int, optional
The level of notifications (0, 1, 2). The default is 0.
Expand Down Expand Up @@ -272,9 +269,6 @@ def configure(
the instance string will be of the form 'autotrader_instance_n'.
The default is None.
broker_verbosity : int, optional
The verbosity of the broker. The default is 0.
home_currency : str, optional
The home currency of trading accounts used (intended for FX
conversions). The default is None.
Expand Down Expand Up @@ -313,7 +307,6 @@ def configure(
self._jupyter_notebook = jupyter_notebook
self._global_config_dict = global_config
self._instance_str = instance_str
self._broker_verbosity = broker_verbosity
self._base_currency = home_currency
self._deploy_time = deploy_time

Expand All @@ -328,6 +321,7 @@ def configure(
logger_kwargs["stdout_level"] = verbosity_map.get(verbosity, logging.INFO)

# Save logger kwargs for other classes
# TODO - make verbosity control print out only, and logging separate
self._logger_kwargs = logger_kwargs

def add_strategy(
Expand Down Expand Up @@ -890,13 +884,7 @@ def run(self) -> Union[None, Broker]:
# Scan watchlist has not overwritten strategy watchlist
self._update_strategy_watchlist()

# Check for added strategies
if len(self._strategy_configs) == 0 and not self._papertrading:
self.logger.warning(
"no strategy has been provided. Do so by using the"
+ " 'add_strategy' method of AutoTrader."
)

# Check run mode
if sum([self._backtest_mode, self._scan_mode]) > 1:
self.logger.error(
"Backtest mode and scan mode are both set to True,"
Expand Down Expand Up @@ -1421,9 +1409,9 @@ def _main(self) -> None:
# TODO - move this into the config construct method get_broker_config
if self._multiple_brokers:
for broker, config in broker_config.items():
config["verbosity"] = self._broker_verbosity
config["verbosity"] = self._verbosity
else:
broker_config["verbosity"] = self._broker_verbosity
broker_config["verbosity"] = self._verbosity

# Connect to exchanges
self.logger.info("Connecting to exchanges...")
Expand Down Expand Up @@ -1592,7 +1580,6 @@ def _instantiate_brokers(self, broker_config: dict[str, any]) -> None:

# Instantiate brokers
brokers = {}
# brokers_utils = {}
for broker_key, config in broker_config.items():
# Import relevant broker and utilities modules
if self._backtest_mode or self._papertrading:
Expand All @@ -1602,6 +1589,9 @@ def _instantiate_brokers(self, broker_config: dict[str, any]) -> None:
# Use real broker
broker_name = broker_key.lower().split(":")[0]

# Add logging options to broker
config["logging_options"] = self._logger_kwargs

# Import relevant module
broker_module = importlib.import_module(f"autotrader.brokers.{broker_name}")

Expand Down
47 changes: 33 additions & 14 deletions autotrader/brokers/ccxt.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import ccxt
import pandas as pd
from datetime import datetime, timezone
from autotrader.utilities import get_logger
from autotrader.brokers.broker import Broker
from autotrader.brokers.trading import OrderBook
from autotrader.brokers.trading import Order, Trade, Position
Expand All @@ -13,11 +14,7 @@ def __init__(self, config: dict) -> None:
# Unpack config and connect to broker-side API
self.exchange: str = config["exchange"]
exchange_instance = getattr(ccxt, self.exchange)

# TODO - improve how this is handled - need to review upstream
# but using quick fix for now
if "api_key" in config:
# TODO - allow expanded config here
ccxt_config = {
"apiKey": config["api_key"],
"secret": config["secret"],
Expand All @@ -27,6 +24,11 @@ def __init__(self, config: dict) -> None:
else:
ccxt_config = {}

# Create logger
self._logger = get_logger(
name=f"{self.exchange}_broker", **config["logging_options"]
)

# Instantiate exchange connection
self.api: ccxt.Exchange = exchange_instance(ccxt_config)

Expand Down Expand Up @@ -76,7 +78,7 @@ def get_balance(self, instrument: str = None) -> float:
return 0

def place_order(self, order: Order, **kwargs) -> None:
"""Disassemble order_details dictionary to place order."""
"""Place an order."""
order()

# Add order params
Expand Down Expand Up @@ -305,7 +307,7 @@ def get_candles(

# Check granularity was provided
if granularity is None:
raise Exception("Please specify candlestick granularity.")
granularity = "1m"

def fetch_between_dates():
# Fetches data between two dates
Expand Down Expand Up @@ -333,7 +335,7 @@ def fetch_between_dates():
data += raw_data

# Increment start_ts
start_ts = raw_data[-1][0]
start_ts = raw_data[-1][0] if len(raw_data) > 1 else end_ts

# Sleep to throttle
time.sleep(0.5)
Expand Down Expand Up @@ -387,9 +389,14 @@ def fetch_between_dates():

return data

def get_orderbook(self, instrument: str, **kwargs) -> OrderBook:
def get_orderbook(
self, instrument: str, limit: int = None, params: dict = None, **kwargs
) -> OrderBook:
"""Returns the orderbook"""
response = self.api.fetch_order_book(symbol=instrument, **kwargs)
params = params if params else {}
response = self.api.fetch_order_book(
symbol=instrument, limit=limit, params=params
)

# Unify format
orderbook: dict[str, list] = {}
Expand All @@ -400,9 +407,19 @@ def get_orderbook(self, instrument: str, **kwargs) -> OrderBook:

return OrderBook(instrument, orderbook)

def get_public_trades(self, instrument: str, *args, **kwargs):
def get_public_trades(
self,
instrument: str,
since: int = None,
limit: int = None,
params: dict = None,
**kwargs,
):
"""Get the public trade history for an instrument."""
ccxt_trades = self.api.fetch_trades(instrument, **kwargs)
params = params if params else {}
ccxt_trades = self.api.fetch_trades(
instrument, since=since, limit=limit, params=params
)

# Convert to standard form
trades = []
Expand All @@ -417,9 +434,10 @@ def get_public_trades(self, instrument: str, *args, **kwargs):

return trades

def get_funding_rate(self, instrument: str, **kwargs):
def get_funding_rate(self, instrument: str, params: dict = None, **kwargs):
"""Returns the current funding rate."""
response = self.api.fetch_funding_rate(instrument, **kwargs)
params = params if params else {}
response = self.api.fetch_funding_rate(instrument, params=params)
fr_dict = {
"symbol": instrument,
"rate": response["fundingRate"],
Expand All @@ -433,9 +451,10 @@ def _ccxt_funding_history(
count: int = None,
start_time: datetime = None,
end_time: datetime = None,
params: dict = {},
params: dict = None,
):
"""Fetches the funding rate history."""
params = params if params else {}

def response_to_df(response):
"""Converts response to DataFrame."""
Expand Down
31 changes: 23 additions & 8 deletions autotrader/brokers/virtual.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import os
import pickle
import importlib
import traceback
import numpy as np
import pandas as pd
from decimal import Decimal
from typing import Callable, Union
from datetime import datetime, timezone
from autotrader.utilities import get_logger
from autotrader.brokers.broker import AbstractBroker
from autotrader.brokers.trading import Order, Position, Trade, OrderBook

Expand Down Expand Up @@ -81,10 +83,14 @@ class Broker(AbstractBroker):
The commission value associated with liquidity taking orders.
"""

def __init__(self, broker_config: dict = None) -> None:
def __init__(self, config: dict = None) -> None:
"""Initialise virtual broker."""
if broker_config is not None:
self._verbosity = broker_config["verbosity"]
# Create logger
self._logging_options = config["logging_options"]
self._logger = get_logger(name="virtual_broker", **self._logging_options)

if config is not None:
self._verbosity = config["verbosity"]
else:
self._verbosity = 0
self._data_broker = None
Expand Down Expand Up @@ -373,6 +379,7 @@ def configure(
)

# Connect to the data broker
data_config["logging_options"] = self._logging_options
self._data_broker = self._get_data_broker(data_config)

# Initialise balance
Expand Down Expand Up @@ -497,11 +504,9 @@ def place_order(self, order: Order, **kwargs) -> None:
+ f"(reference price: {cross_ref_price}, "
+ f"limit price: {order.order_limit_price})"
)
except:
except Exception as e:
# Exception, continue
# TODO - log!
print("ERROR CHECKING LIMIT ORDER")
pass
self._logger.error(f"Error checking limit order: {e}")

# Check order type again
if order.order_type == "modify" and not invalid_order:
Expand Down Expand Up @@ -697,7 +702,12 @@ def get_candles(
# be at least 1 candle's duration worth since the current time.
# Currently implicitly assuming latest_time will update by the candle duration.
count = count if count is not None else len(candles)
candles = candles.loc[candles.index < self._latest_time].tail(count)
if self._paper_trading:
# Do not need to to a time check
candles = candles.tail(count)
else:
# Backtesting - check for lookahead
candles = candles.loc[candles.index < self._latest_time].tail(count)

return candles

Expand Down Expand Up @@ -791,6 +801,9 @@ def _update_positions(
trade : dict, optional
A public trade, used to update virtual limit orders.
"""
# Update internal clock
self._latest_time = dt

# Get latest candle
candle = self.get_candles(instrument, count=1).iloc[0]

Expand Down Expand Up @@ -959,6 +972,8 @@ def _update_all(self, dt: datetime):

except Exception as e:
# Something went wrong
self._logger.error(f"Exception when updating orders: {e}")
self._logger.info(traceback.format_exc())
print(f"Exception when updating orders: {e}\n")

# Cancel orders for this instrument
Expand Down
23 changes: 3 additions & 20 deletions autotrader/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def get_broker_config(
else:
broker_key = broker

supported_brokers = ["oanda", "ib", "ccxt", "dydx", "virtual"]
supported_brokers = ["oanda", "ib", "ccxt", "virtual"]
if broker.lower() not in supported_brokers:
raise Exception(f"Unsupported broker: '{broker}'")

Expand Down Expand Up @@ -160,24 +160,6 @@ def get_broker_config(
),
}

elif broker.lower() == "dydx":
try:
eth_address = global_config["DYDX"]["ETH_ADDRESS"]
eth_private_key = global_config["DYDX"]["ETH_PRIV_KEY"]
config = {
"data_source": "dYdX",
"ETH_ADDRESS": eth_address,
"ETH_PRIV_KEY": eth_private_key,
}
except KeyError:
raise Exception(
"Using dYdX for trading requires authentication via "
+ "the global configuration. Please make sure you provide the "
+ "following keys:\n ETH_ADDRESS: your ETH address "
+ "\n ETH_PRIV_KEY: your ETH private key."
+ "These must all be provided under the 'dYdX' key."
)

elif broker.lower() == "ccxt":
try:
config_data = global_config[broker_key.upper()]
Expand Down Expand Up @@ -250,6 +232,7 @@ def get_data_config(feed: str, global_config: Optional[dict] = None, **kwargs) -
global_config : dict
The global configuration dictionary.
"""
# TODO - review if this is needed - probably can merge with broker config above
if feed is None:
print("Please specify a data feed.")
sys.exit(0)
Expand All @@ -259,7 +242,7 @@ def get_data_config(feed: str, global_config: Optional[dict] = None, **kwargs) -
feed, exchange = feed.lower().split(":")

# Check feed
supported_feeds = ["oanda", "ib", "ccxt", "dydx", "yahoo", "local", "none"]
supported_feeds = ["oanda", "ib", "ccxt", "yahoo", "local", "none"]
if feed.lower() not in supported_feeds:
raise Exception(f"Unsupported data feed: '{feed}'")

Expand Down

0 comments on commit 1eb06b5

Please sign in to comment.