Skip to content

Commit

Permalink
fix tests
Browse files Browse the repository at this point in the history
  • Loading branch information
timkpaine committed Jul 5, 2019
1 parent fcf2246 commit 1636141
Show file tree
Hide file tree
Showing 25 changed files with 470 additions and 265 deletions.
1 change: 0 additions & 1 deletion aat/backtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ def receive(self, data: MarketData) -> None:
# TODO allow if market data for bid/ask
if data.type == TickType.TRADE:
self.callback(TickType.TRADE, data)
log.info(data)
else:
self.callback(TickType.ERROR, data)

Expand Down
75 changes: 1 addition & 74 deletions aat/callback.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from abc import ABCMeta, abstractmethod
from .enums import Side
from .structs import MarketData, TradeResponse
from .logging import log

Expand Down Expand Up @@ -40,79 +39,7 @@ def onExit(self):

def onAnalyze(self, engine):
'''onAnalyze'''
if not engine:
return
import pandas
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import seaborn as sns
matplotlib.rc('font', **{'size': 5})

# extract data from trading engine
portfolio_value = engine.portfolio_value()
requests = engine.query.query_tradereqs()
responses = engine.query.query_traderesps()
trades = pandas.DataFrame([{'time': x.time, 'price': x.price} for x in engine.query.query_trades(instrument=requests[0].instrument, page=None)])
trades.set_index(['time'], inplace=True)

# format into pandas
pd = pandas.DataFrame(portfolio_value, columns=['time', 'value', 'pnl'])
pd.set_index(['time'], inplace=True)
# setup charting
sns.set_style('darkgrid')
fig = plt.figure()
ax1 = fig.add_subplot(311)
ax2 = fig.add_subplot(312)
ax3 = fig.add_subplot(313)

# plot algo performance
pd.plot(ax=ax1, y=['value'], legend=False, fontsize=5, rot=0)

# plot up/down chart
pd['pos'] = pd['pnl']
pd['neg'] = pd['pnl']
pd['pos'][pd['pos'] <= 0] = np.nan
pd['neg'][pd['neg'] > 0] = np.nan
pd.plot(ax=ax2, y=['pos', 'neg'], kind='area', stacked=False, color=['green', 'red'], legend=False, linewidth=0, fontsize=5, rot=0)
ax2.set_ylabel('Realized PnL')

# annotate with key data
ax1.set_title('Performance')
ax1.set_ylabel('Portfolio value($)')
for xy in [portfolio_value[0][:2]] + [portfolio_value[-1][:2]]:
ax1.annotate('$%s' % xy[1], xy=xy, textcoords='data')
ax3.annotate('$%s' % xy[1], xy=xy, textcoords='data')

# plot trade intent/trade action
ax3.set_ylabel('Intent/Action')
ax3.set_xlabel('Date')

ax3.plot(trades)
ax3.plot([x.time for x in requests if x.side == Side.BUY],
[x.price for x in requests if x.side == Side.BUY],
'2', color='y')
ax3.plot([x.time for x in requests if x.side == Side.SELL],
[x.price for x in requests if x.side == Side.SELL],
'1', color='y')
ax3.plot([x.time for x in responses if x.side == Side.BUY], # FIXME
[x.price for x in responses if x.side == Side.BUY],
'^', color='g')
ax3.plot([x.time for x in responses if x.side == Side.SELL], # FIXME
[x.price for x in responses if x.side == Side.SELL],
'v', color='r')

# set same limits
y_bot, y_top = ax1.get_ylim()
x_bot, x_top = ax1.get_xlim()
ax1.set_xlim(x_bot, x_top)
ax2.set_xlim(x_bot, x_top)
ax3.set_xlim(x_bot, x_top)
dif = (x_top-x_bot)*.01
ax1.set_xlim(x_bot-dif, x_top+dif)
ax2.set_xlim(x_bot-dif, x_top+dif)
ax3.set_xlim(x_bot-dif, x_top+dif)
plt.show()
pass

def onHalt(self, data):
'''onHalt'''
Expand Down
6 changes: 3 additions & 3 deletions aat/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ class BacktestConfig(HasTraits):


class RiskConfig(HasTraits):
max_drawdown = Float(default_value=100.0) # % Max strat drawdown before liquidation
max_risk = Float(default_value=100.0) # % Max to risk on any trade
total_funds = Float(default_value=0.0) # % Of total funds to use
max_drawdown = Float(default_value=100.0) # % Max drawdown before liquidation
max_risk = Float(default_value=100.0) # % Max to risk
total_funds = Float(default_value=0.0) # total funds available
trading_type = Instance(klass=TradingType, args=('NONE',), kwargs={})


Expand Down
6 changes: 3 additions & 3 deletions aat/execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ def simulation(self, req):

def requestBuy(self, req: TradeRequest) -> TradeResponse:
# can afford?
balance = self.exchanges[req.exchange].accounts()[req.instrument.underlying.value[0]].balance
balance = self.exchanges[req.exchange].accounts()[req.instrument.underlying.value[1]].balance

if balance < req.volume:
if balance < req.volume * req.price:
return self.insufficientFunds(req)

if self.trading_type == TradingType.BACKTEST:
Expand All @@ -69,7 +69,7 @@ def requestBuy(self, req: TradeRequest) -> TradeResponse:

def requestSell(self, req: TradeRequest) -> TradeResponse:
# can afford?
balance = self.exchanges[req.exchange].accounts()[req.instrument.underlying.value[1]].balance
balance = self.exchanges[req.exchange].accounts()[req.instrument.underlying.value[0]].balance

if balance < req.volume:
return self.insufficientFunds(req)
Expand Down
4 changes: 2 additions & 2 deletions aat/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ def _parse_strategy(strategy, config) -> None:

def _parse_risk(risk, config) -> None:
config.risk_options.max_drawdown = float(risk.get('max_drawdown', config.risk_options.max_drawdown))
config.risk_options.max_risk = float(risk.get('max_drawdown', config.risk_options.max_risk))
config.risk_options.total_funds = float(risk.get('total_funds', config.risk_options.total_funds))
config.risk_options.max_risk = float(risk.get('max_risk', config.risk_options.max_risk))
config.risk_options.total_funds = 0.0


def _parse_default(default, config) -> None:
Expand Down
87 changes: 72 additions & 15 deletions aat/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,19 @@
from datetime import datetime
from functools import reduce
from typing import List, Dict
from .enums import TickType, TradeResult, Side, ExchangeType, PairType, CurrencyType # noqa: F401
from .enums import TradeResult, ExchangeType, PairType, CurrencyType, TradingType, Side
from .exceptions import QueryException
from .execution import Execution
from .logging import log
from .risk import Risk
from .strategy import TradingStrategy
from .structs import Instrument, MarketData, TradeRequest, TradeResponse
from .utils import iterate_accounts


class QueryEngine(object):
def __init__(self,
trading_type: TradingType = None,
exchanges: List[ExchangeType] = None,
pairs: List[PairType] = None,
instruments: List[Instrument] = None,
Expand All @@ -21,6 +24,7 @@ def __init__(self,
execution: Execution = None):
# self._executor = ThreadPoolExecutor(16)
self._all = []
self._trading_type = trading_type

self._accounts = accounts

Expand Down Expand Up @@ -120,28 +124,43 @@ def query_traderesps(self,
def onTrade(self, data: MarketData) -> None:
self._all.append(data)

if data.type == TickType.TRADE:
self._trades.append(data)
if data.instrument not in self._trades_by_instrument:
self._trades_by_instrument[data.instrument] = []
self._trades_by_instrument[data.instrument].append(data)
if data.instrument not in self._last_price_by_asset_and_exchange:
self._last_price_by_asset_and_exchange[data.instrument] = {}
self._last_price_by_asset_and_exchange[data.instrument][data.exchange] = data
self._last_price_by_asset_and_exchange[data.instrument]['ANY'] = data
self._trades.append(data)
if data.instrument not in self._trades_by_instrument:
self._trades_by_instrument[data.instrument] = []
self._trades_by_instrument[data.instrument].append(data)
if data.instrument not in self._last_price_by_asset_and_exchange:
self._last_price_by_asset_and_exchange[data.instrument] = {}
self._last_price_by_asset_and_exchange[data.instrument][data.exchange] = data
self._last_price_by_asset_and_exchange[data.instrument]['ANY'] = data

self._recalculate_value(data)

if data.order_id in self._pending.keys():
resp = self._pending[data.order_id]
resp.volume = resp.remaining - data.remaining

# if they're equal, ignore remaining
resp.volume = resp.remaining - data.remaining if resp != data else resp.volume
resp.remaining = data.remaining

for strat in self._strats:
strat.onFill(resp)

self.updateAccounts(resp)
self._risk.update(resp)

if data.order_id in self._pending.keys() and data.remaining <= 0:
del self._pending[data.order_id]
if data.remaining <= 0:
del self._pending[data.order_id]

def onFill(self, resp: TradeResponse) -> None:
if self._trading_type not in (TradingType.BACKTEST, TradingType.SIMULATION):
# only used during offline trading
raise QueryException('Must derive fills from market data!')

if resp.volume <= 0 or resp.status != TradeResult.FILLED:
# sckip
return
resp.remaining = 0.0
self.onTrade(resp)

def onCancel(self, data: MarketData) -> None:
if data.order_id in self._pending.keys():
Expand All @@ -152,9 +171,45 @@ def onCancel(self, data: MarketData) -> None:
def strategies(self):
return self._strats

def updateAccounts(self, resp: TradeResponse = None) -> None:
'''update the holdings and spot value of accounts'''
if resp:
account_left = self._accounts[resp.instrument.underlying.value[0]][resp.exchange]
account_right = self._accounts[resp.instrument.underlying.value[1]][resp.exchange]

if resp.side == Side.BUY:
# if buy
# 5 BTCUSD @ $2 -> from_btc += volume, to_usd -= price*volume
account_left.balance += resp.volume
account_right.balance -= resp.volume * resp.price
else:
# if sell
# 5 BTCUSD @ $2 -> from_btc -= volume, to_usd += price*volume
account_left.balance -= resp.volume
account_right.balance += resp.volume * resp.price

accounts = (account_left, account_right)
else:
accounts = iterate_accounts(self.accounts)

# WARNING
# don't update value on every tick
for account in accounts:
log.info(f'Updating value of account {account}')
if account.currency == CurrencyType.USD:
# if holding USD, add value
account.value = account.balance
else:
# calculate USD value
price = self._last_price_by_asset_and_exchange[resp.instrument][resp.exchange].price
account.value = account.balance * price
log.info(f'New value: {account}')

def newPending(self, resp: TradeResponse) -> None:
if resp.status == TradeResult.PENDING:
self._pending[resp.order_id] = resp
self._pending[resp.order_id] = resp

def pendingOrders(self) -> List[TradeResponse]:
return self._pending.values()

def push_tradereq(self, req: TradeRequest) -> None:
self._trade_reqs.append(req)
Expand All @@ -163,6 +218,8 @@ def push_tradereq(self, req: TradeRequest) -> None:
self._trade_reqs_by_instrument[req.instrument].append(req)

def push_traderesp(self, resp: TradeResponse) -> None:
if resp.status in (TradeResult.REJECTED, TradeResult.PENDING, TradeResult.NONE):
return
self._trade_resps.append(resp)
if resp.instrument not in self._trade_resps_by_instrument:
self._trade_resps_by_instrument[resp.instrument] = []
Expand Down
47 changes: 26 additions & 21 deletions aat/risk.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
from .config import RiskConfig
from .enums import Side, TradeResult, OrderType, RiskReason, ExchangeType
from .exchange import Exchange
from .structs import TradeRequest, TradeResponse, Account, Instrument
from .logging import log
from .structs import TradeRequest, TradeResponse, Account, Instrument
from .utils import iterate_accounts


class Risk(object):
Expand All @@ -13,16 +14,10 @@ def __init__(self, options: RiskConfig, exchanges: List[Exchange], accounts: Lis
self.max_drawdown = options.max_drawdown
self.max_risk = options.max_risk
self.total_funds = options.total_funds
self.outstanding = 0.0 # type: float # TODO get from open orders

self.max_running_outstanding = 0.0
self.max_running_outstanding_incr = [] # type: list

self.max_running_drawdown = 0.0 # type: float
self.max_running_drawdown_incr = [] # type: list

self.max_running_risk = 0.0 # type: float
self.max_running_risk_incr = [] # type: list
self.outstanding = 0.0
self.starting_funds = options.total_funds
self.drawdown = 0.0

self.exchanges = exchanges
self.accounts = accounts
Expand Down Expand Up @@ -89,7 +84,7 @@ def request(self, req: TradeRequest) -> TradeRequest:
exchange=req.exchange,
instrument=req.instrument,
order_type=req.order_type,
vol=volume,
vol=0.0,
price=req.price,
time=req.time,
status=False,
Expand All @@ -103,25 +98,35 @@ def requestSell(self, req: TradeRequest):
'''precheck for risk compliance'''
return self.request(req)

def updateAccounts(self):
'''update risk numbers'''
log.critical('risk not fully implemented - updateRisk')
value = 0.0

for account in iterate_accounts(self.accounts):
value += account.value

if value < self.total_funds:
log.info(f'reducing total funds from {self.total_funds} to {value}')

self.total_funds = value
log.info(f'drawdown: {self.total_funds - self.starting_funds}')

def update(self, resp: TradeResponse):
'''update risk after execution'''
log.critical('risk not fully implemented')
log.critical('risk not fully implemented - update')
self.updateAccounts()

if resp.status == TradeResult.FILLED:
# FIXME
self.outstanding += abs(resp.volume * resp.price) * (1 if resp.side == Side.BUY else -1)

# FIXME
# self.max_running_outstanding = max(self.max_running_outstanding,
# self.outstanding)
# self.max_running_outstanding_incr.append(
# self.max_running_outstanding)

# TODO self.max_running_risk =
# TODO self.max_running_drawdown =
elif resp.status == TradeResult.REJECTED:
self.outstanding -= abs(resp.volume * resp.price) * (1 if resp.side == Side.BUY else -1)

def cancel(self, resp: TradeResponse):
'''update risk after cancelling or rejecting order'''
log.critical('risk not fully implemented')
log.critical('risk not fully implemented - cancel')
self.updateAccounts()

self.outstanding -= abs(resp.volume * resp.price) * (1 if resp.side == Side.BUY else -1)
8 changes: 4 additions & 4 deletions aat/strategies/buy_and_hold.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
class BuyAndHoldStrategy(TradingStrategy):
def __init__(self, *args, **kwargs) -> None:
super(BuyAndHoldStrategy, self).__init__(*args, **kwargs)
self.bought = None
self.bought = {}

def onFill(self, res: TradeResponse) -> None:
self.bought = res
self.bought[res.instrument] = res
log.info('bought %.2f @ %.2f' % (res.volume, res.price))

def onTrade(self, data: MarketData) -> bool:
if self.bought is None:
if data.instrument not in self.bought:
req = TradeRequest(side=Side.BUY,
volume=1,
instrument=data.instrument,
Expand All @@ -24,7 +24,7 @@ def onTrade(self, data: MarketData) -> bool:
time=data.time)
log.info("requesting buy : %s", req)
self.request(req)
self.bought = 'pending'
self.bought[data.instrument] = 'pending'

def onError(self, e) -> None:
log.critical(e)
Expand Down

0 comments on commit 1636141

Please sign in to comment.