Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ RUN curl -sSL https://install.python-poetry.org | python3 - \

WORKDIR /app/${PROJECT_DIR}
COPY ${PROJECT_DIR}/pyproject.toml ${PROJECT_DIR}/poetry.lock ./
COPY lib lib/
COPY libs libs/
RUN poetry install --only main

##################################################
Expand Down
3 changes: 2 additions & 1 deletion pytradebacktest/broker.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
from pytrade.instruments import MINUTES_MAP, Granularity, Instrument
from pytrade.interfaces.broker import IBroker
from pytrade.interfaces.data import IInstrumentData
from pytrade.models import Order, Position, Trade
from pytrade.models import Order, Trade

from pytradebacktest.data import MarketData
from pytradebacktest.exceptions import OutOfMoneyError
from pytradebacktest.order import OrderContext
from pytradebacktest.position import Position


class BacktestBroker(IBroker):
Expand Down
62 changes: 62 additions & 0 deletions pytradebacktest/position.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import numpy as np
from pytrade.instruments import Instrument
from pytrade.interfaces.position import IPosition
from pytrade.models import Trade


class Position(IPosition):
"""
Currently held asset position, available as
`backtesting.backtesting.Strategy.position` within
`backtesting.backtesting.Strategy.next`.
Can be used in boolean contexts, e.g.

if self.position:
... # we have a position, either long or short
"""

def __init__(self, instrument: Instrument, trades: list[Trade]):
self.__instrument = instrument
self.__trades = trades

def __bool__(self):
return self.size != 0

@property
def trades(self):
return [
trade for trade in self.__trades if trade.instrument == self.__instrument
]

@property
def size(self) -> float:
"""Position size in units of asset. Negative if position is short."""
return sum(trade.size for trade in self.trades)

@property
def pl(self) -> float:
"""Profit (positive) or loss (negative) of the current position in cash units."""
return sum(trade.pl for trade in self.trades)

@property
def pl_pct(self) -> float:
"""Profit (positive) or loss (negative) of the current position in percent."""
weights = np.abs([trade.size for trade in self.trades])
weights = weights / weights.sum()
pl_pcts = np.array([trade.pl_pct for trade in self.trades])
return (pl_pcts * weights).sum()

@property
def is_long(self) -> bool:
"""True if the position is long (position size is positive)."""
return self.size > 0

@property
def is_short(self) -> bool:
"""True if the position is short (position size is negative)."""
return self.size < 0

def __repr__(self):
return (
f"<Position[{self.__instrument}]: {self.size} ({len(self.trades)} trades)>"
)
2 changes: 1 addition & 1 deletion pytradebacktest/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ def worst_trade_return(self):

@property
def avg_trade_return(self):
return self._geometric_mean(self.returns/100) * 100
return self._geometric_mean(self.returns / 100) * 100

@property
def max_trade_duration(self):
Expand Down
106 changes: 106 additions & 0 deletions tests/unit/test_backtest_position.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
from datetime import datetime, timedelta
from unittest.mock import MagicMock

import pandas as pd
import pytest
from pandas import Timestamp
from pytrade.models import Trade

from pytradebacktest.position import Position


def test_position():
instrument_1 = "ABC"
instrument_2 = "GOOG"

entry_price = 98.90
last_price = 105
time1 = Timestamp(datetime.now())
exit_price = 110.50
time2 = time1 + timedelta(days=1, hours=2)
goog_data = MagicMock()
goog_data.last_price = last_price
goog_df = pd.DataFrame(
{"Timestamp": [time1, time2], "Close": [entry_price, exit_price]}
)
goog_df = goog_df.set_index("Timestamp")
goog_data.df = goog_df

abc_data = MagicMock()
abc_data.last_price = last_price
abc_df = pd.DataFrame(
{"Timestamp": [time1, time2], "Close": [entry_price, exit_price]}
)
abc_df = abc_df.set_index("Timestamp")
abc_data.df = abc_df

abc_trades = [
Trade(instrument_1, 100, 90, time1, abc_data),
Trade(instrument_1, 150, 85, time2, abc_data),
]
goog_trades = [
Trade(instrument_2, 100, 90, time1, goog_data),
Trade(instrument_2, 200, 100, time2, goog_data),
]
trades = abc_trades + goog_trades

position = Position(instrument_2, trades)

assert len(position.trades) == 2
assert position.size == 300
assert position.pl == 2500
trade1_pl_pct = (last_price / 90 - 1) * 100
trade2_pl_pct = (last_price / 100 - 1) * 100
position_pl_pct = (
0.3333333333333333 * trade1_pl_pct + 0.6666666666666666 * trade2_pl_pct
)
assert position.pl_pct == position_pl_pct
assert position.is_long is True
assert position.is_short is False


def test_position_close():
instrument_1 = "ABC"
instrument_2 = "GOOG"

entry_price = 98.90
last_price = 105
time1 = Timestamp(datetime.now())
exit_price = 110.50
time2 = time1 + timedelta(days=1, hours=2)
goog_data = MagicMock()
goog_data.last_price = last_price
goog_df = pd.DataFrame(
{"Timestamp": [time1, time2], "Close": [entry_price, exit_price]}
)
goog_df = goog_df.set_index("Timestamp")
goog_data.df = goog_df

abc_data = MagicMock()
abc_data.last_price = last_price
abc_df = pd.DataFrame(
{"Timestamp": [time1, time2], "Close": [entry_price, exit_price]}
)
abc_df = abc_df.set_index("Timestamp")
abc_data.df = abc_df

abc_trades = [
Trade(instrument_1, 100, 90, time1, abc_data),
Trade(instrument_1, 150, 85, time2, abc_data),
]
trade_1 = Trade(instrument_2, 100, 90, time1, goog_data)
trade_2 = Trade(instrument_2, 200, 100, time1, goog_data)
goog_trades = [
trade_1,
trade_2,
]
trades = abc_trades + goog_trades

position = Position("GOOG", trades)

trade_1.close(exit_price, time2)
trade_2.close(exit_price, time2)

assert position.size == 300
assert position.pl == 4150
assert position.pl_pct == pytest.approx(14.59, 1e-2)