Skip to content

Commit

Permalink
add position_manager to strategy
Browse files Browse the repository at this point in the history
for this strategy only
  • Loading branch information
letianzj committed Aug 12, 2020
1 parent 9aaaa01 commit c8516cc
Show file tree
Hide file tree
Showing 10 changed files with 281 additions and 64 deletions.
4 changes: 2 additions & 2 deletions examples/buy_and_hold_strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import pytz
from quanttrading2.util import read_ohlcv_csv
from quanttrading2.strategy import StrategyBase
from quanttrading2 import BacktestEngine
from quanttrading2 import BacktestGymEngine, BacktestEngine


class BuyAndHoldStrategy(StrategyBase):
Expand All @@ -22,7 +22,7 @@ def on_tick(self, event):
if not self.invested:
df_hist = self._data_board.get_hist_price(symbol, event.timestamp)
close = df_hist.iloc[-1].Close
target_size = int(self.cash / close)
target_size = int(self._position_manager.initial_capital / close)
self.adjust_position(symbol, size_from=0, size_to=target_size)
self.invested = True

Expand Down
3 changes: 2 additions & 1 deletion quanttrading2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@
from .position import *
from .risk import *
from .strategy import *
from .backtest_engine import BacktestEngine
from .backtest_engine import BacktestEngine
from .backtest_gym_engine import BacktestGymEngine
14 changes: 11 additions & 3 deletions quanttrading2/backtest_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ def add_data(self, data_key, data_source, watch=True):
self._performance_manager.add_watch(data_key, data_source)

def _setup(self):
"""
Tis needs to be run after strategy and data are loaded
because it subscribes to market data
"""
## 1. data_feed
self._data_feed.subscribe_market_data()

Expand All @@ -77,7 +81,7 @@ def _setup(self):
)

## 4. set strategy
self._strategy.on_init(self._events_engine, self._data_board, self._position_manager)
self._strategy.on_init(self._events_engine, self._data_board, self.multiplier_dict)
self._strategy.on_start()

## 5. performance manager and portfolio manager
Expand All @@ -89,6 +93,7 @@ def _setup(self):

## 6. wire up event handlers
self._events_engine.register_handler(EventType.TICK, self._tick_event_handler)
# to be consistent with current live, order is placed directly
self._events_engine.register_handler(EventType.ORDER, self._order_event_handler)
self._events_engine.register_handler(EventType.FILL, self._fill_event_handler)

Expand All @@ -99,9 +104,11 @@ def _tick_event_handler(self, tick_event):
# performance update goes before position and databoard updates because it updates previous day performance
# it can't update today because orders haven't been filled yet.
self._performance_manager.update_performance(self._current_time, self._position_manager, self._data_board)
self._position_manager.mark_to_market(self._current_time, tick_event.full_symbol, tick_event.price, self._data_board)
self._position_manager.mark_to_market(tick_event.timestamp, tick_event.full_symbol, tick_event.price, self._data_board)
self._strategy.on_tick(tick_event) # plus strategy.position_manager market to marekt
# data_baord update last, so it still holds price of last tick; for position MtM
# for backtest, strategy pull directly from hist_data; so it doesn't matter
self._data_board.on_tick(tick_event)
self._strategy.on_tick(tick_event)

def _order_event_handler(self, order_event):
"""
Expand All @@ -120,6 +127,7 @@ def run(self):
Run backtest
"""
self._setup()

self._events_engine.run()
# explicitly update last day/time
self._performance_manager.update_performance(self._current_time, self._position_manager, self._data_board)
Expand Down
160 changes: 160 additions & 0 deletions quanttrading2/backtest_gym_engine.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Gym trading env
https://github.com/openai/gym/blob/master/gym/envs/classic_control/cartpole.py
1. obs <- reset() # env
2. action <- pi(obs) # agent
3. news_obs <- step(action) # env
repeat 2, and 3 for interactions between agent and env
"""
import os
import numpy as np
import pandas as pd
import gym
from datetime import datetime, date
import logging

from .event import EventType
from .event.backtest_event_engine import BacktestEventEngine
from .data.backtest_data_feed import BacktestDataFeed
from .data.data_board import DataBoard
from .brokerage.backtest_brokerage import BacktestBrokerage
from .position.position_manager import PositionManager
from .performance.performance_manager import PerformanceManager
from .risk.risk_manager import PassThroughRiskManager

_logger = logging.getLogger(__name__)


class BacktestGymEngine(gym.Env):
"""
Description:
backtest gym engine
it doesn't normalize; and expects a normalization layer
Observation:
Type: Box(lookback_window, n_assets*5+2)
lookback_window x (n_assets*(ohlcv) + cash+npv)
TODO: append trades, commissions, standing orders, etc
TODO: stop/limit orders
Actions:
Type: Box(n_assets + 1)
portfolio weights [w1,w2...w_k, cash_weight], add up to one
Reward:
cumulative pnl in run_window
Starting State:
random timestamp between start_date and (end_date - run_window)
Episode Termination:
after predefined window
If broke, no orders will send
"""
def __init__(self, n_assets, lookback_window=15, run_window=252*2, start_date=None, end_date=None):
self._current_time = None
self._start_date = start_date
self._end_date = end_date
self._data_feed = BacktestDataFeed(self._start_date, self._end_date)
self._data_board = DataBoard()
self._performance_manager = PerformanceManager()
self._position_manager = PositionManager()
self._risk_manager = PassThroughRiskManager()
self.multiplier_dict = {}
self._strategy = None # no strategy; strategy is to be learned

self._n_assets = n_assets
self.action_space = gym.spaces.Box(low=0.0, high=1.0, shape=(n_assets + 1,), dtype=np.float32)

self._lookback_window = lookback_window
self._run_window = run_window
self.observation_space = gym.spaces.Box(low=-np.inf, high=np.inf,
shape=(self._lookback_window, self._n_assets*5+2), dtype=np.float32)

self._setup()

def set_capital(self, capital):
self._position_manager.set_capital(capital)

def set_fvp(self):
self._performance_manager.set_fvp(self.multiplier_dict)
self._position_manager.set_fvp(self.multiplier_dict)

def add_data(self, data_key, data_source, watch=True):
"""
Add data for backtest
:param data_key: AAPL or CL; if it is followed by number, assumed to be multiplier
:param data_source: dataframe, datetimeindex
:param watch: track position or not
:return:
"""
keys = data_key.split(' ')
if keys[-1].isdigit(): # multiplier
data_key = ' '.join(keys[:-1])
self.multiplier_dict[data_key] = int(keys[-1])

self._data_feed.set_data_source(data_source) # get iter(datetimeindex)
self._data_board.initialize_hist_data(data_key, data_source)
if watch:
self._performance_manager.add_watch(data_key, data_source)

def _setup(self):
"""
Tis can be initialzied before data is loaded
because it doesn't subscribe
"""
## 2. event engine
self._events_engine = BacktestEventEngine(self._data_feed)

## 3. brokerage
self._backtest_brokerage = BacktestBrokerage(
self._events_engine, self._data_board
)

## 6. wire up event handlers
self._events_engine.register_handler(EventType.TICK, self._tick_event_handler)
# to be consistent with current live, order is placed directly
# self._events_engine.register_handler(EventType.ORDER, self._order_event_handler)
self._events_engine.register_handler(EventType.FILL, self._fill_event_handler)

def _tick_event_handler(self, tick_event):
self._current_time = tick_event.timestamp

# performance updates after one step run
self._position_manager.mark_to_market(tick_event.timestamp, tick_event.full_symbol, tick_event.price, self._data_board)
self._data_board.on_tick(tick_event)
self._strategy.on_tick(tick_event)

def _order_event_handler(self, order_event):
"""
backtest doesn't send order_event back to strategy. It fills directly and becoems fill_event
"""
self._backtest_brokerage.place_order(order_event)

def _fill_event_handler(self, fill_event):
self._position_manager.on_fill(fill_event)
self._performance_manager.on_fill(fill_event)
self._strategy.on_fill(fill_event)

def reset(self):
## reset performance manager and portfolio manager
self._performance_manager.reset()
self._position_manager.reset()

## reset iterator randomly
self._data_feed.subscribe_market_data()

def step(self, action):
"""
Run backtest
"""
self._events_engine.run(1)
# explicitly update performance; now orders are filled
self._performance_manager.update_performance(self._current_time, self._position_manager, self._data_board)

done = self._current_time is None
state = None
return state

def render(self, mode='human'):
pass

def close(self):
pass
13 changes: 7 additions & 6 deletions quanttrading2/brokerage/ib_brokerage.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,14 +105,15 @@ def place_order(self, order_event):
_logger.error(f'Failed to create order to place {order_event.full_symbol}')
return

order_event.order_id = self.orderid
if order_event.order_id < 0:
order_event.order_id = self.orderid
self.orderid += 1
order_event.account = self.account
order_event.timestamp = datetime.now().strftime("%H:%M:%S.%f")
order_event.order_status = OrderStatus.NEWBORN
self.order_dict[self.orderid] = order_event
self.event_engine.put(copy(order_event))
self.api.placeOrder(self.orderid, ib_contract, ib_order)
self.orderid += 1
order_event.order_status = OrderStatus.NEWBORN # NEWBORN?
self.order_dict[order_event.order_id] = order_event
self.event_engine.put(copy(order_event)) # acknowledged
self.api.placeOrder(order_event.order_id, ib_contract, ib_order)

def cancel_order(self, order_id):
if not self.api.connected:
Expand Down
4 changes: 2 additions & 2 deletions quanttrading2/data/data_board.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def get_last_price(self, symbol):
"""
Returns the most recent price for a given ticker
"""
if symbol in self._current_data_dict:
if symbol in self._current_data_dict.keys():
return self._current_data_dict[symbol].price
elif symbol in self._hist_data_dict.keys():
return self._hist_data_dict[symbol].loc[self._current_time, 'Close']
Expand All @@ -42,7 +42,7 @@ def get_last_timestamp(self, symbol):
"""
Returns the most recent timestamp for a given ticker
"""
if symbol in self._current_data_dict:
if symbol in self._current_data_dict.keys():
return self._current_data_dict[symbol].timestamp
elif self._PLACEHOLDER in self._current_data_dict:
return self._current_data_dict[self._PLACEHOLDER].timestamp
Expand Down
2 changes: 1 addition & 1 deletion quanttrading2/position/position_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

class PositionEvent(Event):
"""
position event
position event directly from live broker
"""
def __init__(self):
"""
Expand Down
23 changes: 15 additions & 8 deletions quanttrading2/position/position_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,19 @@ def on_fill(self, fill_event):
else:
self.positions[fill_event.full_symbol] = fill_event.to_position()

def mark_to_market(self, current_time, symbol, last_price, data_board):
#for sym, pos in self.positions.items():
sym = symbol
multiplier = self.dict_multipliers.get(sym, 1)
if symbol in self.positions:
# TODO: for place holder case, nothing updated
def mark_to_market(self, time_stamp, symbol, last_price, data_board):
"""
from previous timestamp to current timestamp. Pnl from holdings
"""
if symbol == 'PLACEHOLDER': # backtest placeholder, update all
for sym, pos in self.positions.items():
multiplier = self.dict_multipliers.get(sym, 1)
real_last_price = data_board.get_hist_price(sym, time_stamp).Close.iloc[-1] # not PLACEHOLDER
pos.mark_to_market(real_last_price, multiplier)
# data board not updated yet; get_last_time return previous time_stamp
self.current_total_capital += self.positions[sym].size * (real_last_price - data_board.get_last_price(sym)) * multiplier
elif symbol in self.positions:
# this is a quick way based on one symbol; actual pnl should sum up across positions
multiplier = self.dict_multipliers.get(symbol, 1)
self.positions[symbol].mark_to_market(last_price, multiplier)
# data board not updated yet
self.current_total_capital += self.positions[symbol].size * (last_price - data_board.get_last_price(sym)) * multiplier
self.current_total_capital += self.positions[symbol].size * (last_price - data_board.get_last_price(symbol)) * multiplier
Loading

0 comments on commit c8516cc

Please sign in to comment.