Skip to content

Commit

Permalink
backtesting updates
Browse files Browse the repository at this point in the history
  • Loading branch information
robswc committed Mar 16, 2023
1 parent 8e5d198 commit 575cf11
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 94 deletions.
2 changes: 1 addition & 1 deletion app/api/api_v1/endpoints/strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ async def run_strategy(request: RunStrategyRequest):
name = request.strategy
data_adapter_name = request.adapter
data = str(request.data)
parameters = {p['name']: p['value'] for p in request.parameters}
parameters = {p['name']: p['value'] for p in request.parameters} if request.parameters else {}

# get strategy and data adapter
da = DataAdapter.objects.get(name=data_adapter_name)
Expand Down
31 changes: 5 additions & 26 deletions app/components/backtest/backtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,33 +46,12 @@ def _sort_orders(self, orders: List[Order]):
def test(self):
logger.debug(f'Starting backtest for strategy {self.strategy.name}')

positions = self.strategy.positions.all()
orders = self.strategy.orders.all()

# sort orders by timestamp
sorted_orders = self._sort_orders(self.strategy.orders.all())

# positions
positions = []
last_position = None

# build the positions
for order in sorted_orders:
# main logic
try:
last_position.add_order(order)
except PositionClosedException:
p = Position()
p.add_order(order)
positions.append(p)
last_position = p
except AttributeError:
p = Position()
p.add_order(order)
positions.append(p)
last_position = p

# loop through each position
for position in positions:
position.test(ohlc=self.data)
for p in positions:
p.test(ohlc=self.data)
print(str(p))

# calculate win/loss ratio
losing_trades = len([position for position in positions if position.pnl < 0])
Expand Down
106 changes: 44 additions & 62 deletions app/components/orders/position.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,90 +9,72 @@
class PositionValidationException(Exception):
pass


class PositionClosedException(Exception):
pass


class PositionEffect:
REDUCE = 'reduce'
ADD = 'add'


class Position(BaseModel):
orders: List[Order] = []
closed: bool = False
cost_basis: Optional[float] = None
average_entry_price: Optional[float] = None
average_exit_price: Optional[float] = None
size: Optional[int] = None
size: Optional[int] = 0
side: Optional[str] = None
unrealized_pnl: Optional[float] = None
pnl: Optional[float] = None
timestamp: Optional[int] = None

# TODO: restructure position to be more efficient
def _get_effect(self, order: Order):
print('getting effect...', abs(self.size), abs(self.size + order.qty))
if abs(self.size) < abs(self.size + order.qty):
return PositionEffect.ADD
else:
return PositionEffect.REDUCE

def test(self, ohlc: 'OHLC'):
self.pnl = self.calc_pnl()
for o in self.orders:
print(self.handle_order(o))

def add_order(self, order: Order):
def add_closing_order(self):
# creates and adds a closing order to the position
if self.closed:
raise PositionClosedException('Position is already closed')

# else we add the order
self.orders.append(order)

long_qty = sum([o.qty for o in self.orders if o.side == 'buy'])
short_qty = sum([o.qty for o in self.orders if o.side == 'sell'])
self.closed = long_qty == short_qty

def get_size(self):
return sum([order.qty for order in self.orders])
# create order
order = Order(
order_type='market',
side='buy' if self.side == 'sell' else 'sell',
quantity=self.size,
symbol=self.orders[0].symbol,
timestamp=self.timestamp,
)

def get_all_buy_orders(self):
return [order for order in self.orders if order.side == 'buy']

def get_all_sell_orders(self):
return [order for order in self.orders if order.side == 'sell']

def get_average_entry_price(self):
if self.orders[0].side == 'buy':
return sum([o.filled_avg_price for o in self.get_all_buy_orders()]) / len(self.get_all_buy_orders())
else:
return sum([o.filled_avg_price for o in self.get_all_sell_orders()]) / len(self.get_all_sell_orders())
self.orders.append(order)

def get_average_exit_price(self):
if self.orders[0].side == 'buy':
return sum([o.filled_avg_price for o in self.get_all_sell_orders()]) / len(self.get_all_sell_orders())
else:
return sum([o.filled_avg_price for o in self.get_all_buy_orders()]) / len(self.get_all_buy_orders())
def handle_order(self, order):
effect = self._get_effect(order)

def get_side(self):
return self.orders[0].side
if effect == PositionEffect.ADD:
print('order is add')
# since the position is added, we need to calculate the cost basis
self.cost_basis += order.price * order.qty
self.average_entry_price = self.cost_basis / (self.size + order.qty)

def get_timestamp(self):
return self.orders[-1].timestamp
if effect == PositionEffect.REDUCE:
print('order is reduce')
# since the position is reduced, we need to calculate the realized pnl
realized_pnl = (order.price - self.average_entry_price) * (order.qty * -1 if self.size > 0 else order.qty)
self.pnl += realized_pnl

def calc_pnl(self):
if self.closed:
# root order direction
root_order_side = self.orders[0].side

# calculate pnl
if root_order_side == 'buy':
return (self.get_average_exit_price() - self.get_average_entry_price()) * self.get_size()
else:
return (self.get_average_entry_price() - self.get_average_exit_price()) * self.get_size()
return 0

def dict(self, **kwargs):
d = super().dict(**kwargs)
if self.closed:
d['average_entry_price'] = self.get_average_entry_price()
d['average_exit_price'] = self.get_average_exit_price()
d['size'] = self.get_size() / 2
d['side'] = self.get_side()
d['timestamp'] = self.get_timestamp()
d['pnl'] = self.calc_pnl()
else:
d['average_entry_price'] = self.get_average_entry_price()
d['size'] = self.get_size()
d['side'] = self.get_side()
d['timestamp'] = self.get_timestamp()
d['pnl'] = self.calc_pnl()
return d
self.size += order.qty
self.closed = self.size == 0 # if size is 0, position is closed


class BracketPosition(BaseModel):
Expand All @@ -106,4 +88,4 @@ class BracketPosition(BaseModel):
# raise PositionValidationException('Both take_profit and stop_loss cannot be None.')
# # ensure that all orders are of the same symbol
# if self.take_profit.symbol != self.stop_loss.symbol:
# raise PositionValidationException('All orders must be of the same symbol.')
# raise PositionValidationException('All orders must be of the same symbol.')
35 changes: 35 additions & 0 deletions app/components/orders/position_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from typing import List

from components.orders.order import Order
from components.orders.position import Position


class PositionManager:
def __init__(self, strategy: 'BaseStrategy'):
self._strategy = strategy
self.positions: List[Position] = []

def open(self, order_type: str, side: str, quantity: int):
"""Opens a new position"""

# create order
order = Order(
type=order_type,
side=side,
qty=quantity,
symbol=self._strategy.symbol.symbol,
timestamp=self._strategy.data.timestamp,
)

self.positions.append(
Position(
orders=[order],
)
)

def close(self):
"""Closes the most recent position"""
self.positions[-1].add_closing_order()

def all(self):
return self.positions
6 changes: 4 additions & 2 deletions app/components/strategy/strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from components.manager.manager import ComponentManager
from components.ohlc import OHLC
from components.orders.order_manager import OrderManager
from components.orders.position_manager import PositionManager
from components.parameter import BaseParameter, Parameter, ParameterModel
from components.strategy.decorators import extract_decorators

Expand Down Expand Up @@ -76,8 +77,9 @@ def __init__(self, data: Union[OHLC, None] = None):
self._step_methods = steps
self._after_methods = afters

# each strategy gets a new order manager
self.orders = OrderManager(self)
# each strategy gets a new order and position manager
self.orders = OrderManager(self) # eventually all orders will be converted to positions
self.positions = PositionManager(self)

# each strategy gets plots
self.plots = []
Expand Down
6 changes: 4 additions & 2 deletions app/storage/strategies/examples/sma_cross_over.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ def check_for_crossover(self):
cross_over = ta.logic.crossover(self.sma_fast, self.sma_slow)
cross_under = ta.logic.crossunder(self.sma_fast, self.sma_slow)
if cross_over:
self.orders.market_order(side='buy', quantity=1)
# self.orders.market_order(side='buy', quantity=1)
self.positions.open(side='buy', quantity=1)
elif cross_under:
self.orders.market_order(side='sell', quantity=1)
# self.orders.market_order(side='sell', quantity=1)
self.positions.close()

@after
def create_plots(self):
Expand Down
12 changes: 11 additions & 1 deletion app/tests/test_backtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
STRATEGY = SMACrossOver(data=OHLC)

class TestBacktest:
def test_backtest(self):
def test_backtest_orders(self):

# add orders
STRATEGY.orders.market_order(side='buy', quantity=1)
Expand All @@ -15,3 +15,13 @@ def test_backtest(self):
backtest.test()

assert backtest.result is not None

def test_backtest_positions(self):

# add positions
STRATEGY.positions.open(order_type='market', side='buy', quantity=1)

backtest = Backtest(strategy=STRATEGY, data=OHLC)
backtest.test()

assert backtest.result is not None

0 comments on commit 575cf11

Please sign in to comment.