From 7aedb92e7c5d6cbb4009e8abfca2228155933a08 Mon Sep 17 00:00:00 2001 From: Kyle Widmann Date: Mon, 10 Feb 2025 14:05:10 -0500 Subject: [PATCH] Moving position to an interface --- pytrade/broker.py | 5 +- pytrade/interfaces/broker.py | 5 +- pytrade/interfaces/position.py | 42 ++++++++++++++ pytrade/models.py | 59 ------------------- pytrade/strategy.py | 1 - tests/unit/test_models.py | 100 +-------------------------------- 6 files changed, 49 insertions(+), 163 deletions(-) create mode 100644 pytrade/interfaces/position.py diff --git a/pytrade/broker.py b/pytrade/broker.py index 578e606..2823aaf 100644 --- a/pytrade/broker.py +++ b/pytrade/broker.py @@ -3,7 +3,8 @@ from pytrade.instruments import Granularity, Instrument from pytrade.interfaces.broker import IBroker from pytrade.interfaces.client import IClient -from pytrade.models import Order, Position +from pytrade.interfaces.position import IPosition +from pytrade.models import Order class Broker(IBroker): @@ -24,7 +25,7 @@ def margin_available(self) -> float: def leverage(self) -> float: raise NotImplementedError() - def get_position(self, instrument: Instrument) -> Position: + def get_position(self, instrument: Instrument) -> IPosition: raise NotImplementedError() def close_position(self, instrument: Instrument): diff --git a/pytrade/interfaces/broker.py b/pytrade/interfaces/broker.py index 94bdb4d..e30bef1 100644 --- a/pytrade/interfaces/broker.py +++ b/pytrade/interfaces/broker.py @@ -2,7 +2,8 @@ from pytrade.instruments import Granularity, Instrument from pytrade.interfaces.data import IInstrumentData -from pytrade.models import Order, Position +from pytrade.interfaces.position import IPosition +from pytrade.models import Order class IBroker(metaclass=abc.ABCMeta): @@ -33,7 +34,7 @@ def leverage(self) -> float: raise NotImplementedError() @abc.abstractmethod - def get_position(self, instrument: Instrument) -> Position: + def get_position(self, instrument: Instrument) -> IPosition: raise NotImplementedError() @abc.abstractmethod diff --git a/pytrade/interfaces/position.py b/pytrade/interfaces/position.py new file mode 100644 index 0000000..ace5c7f --- /dev/null +++ b/pytrade/interfaces/position.py @@ -0,0 +1,42 @@ +from abc import abstractmethod + + +class 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 + """ + + @property + @abstractmethod + def size(self) -> float: + raise NotImplementedError() + + @property + @abstractmethod + def pl(self) -> float: + """Profit (positive) or loss (negative) of the current position in cash units.""" + raise NotImplementedError() + + @property + @abstractmethod + def pl_pct(self) -> float: + """Profit (positive) or loss (negative) of the current position in percent.""" + raise NotImplementedError() + + @property + @abstractmethod + def is_long(self) -> bool: + """True if the position is long (position size is positive).""" + raise NotImplementedError() + + @property + @abstractmethod + def is_short(self) -> bool: + """True if the position is short (position size is negative).""" + raise NotImplementedError() diff --git a/pytrade/models.py b/pytrade/models.py index 0416bfa..3ce9ebb 100644 --- a/pytrade/models.py +++ b/pytrade/models.py @@ -2,7 +2,6 @@ from math import copysign from typing import Any, Optional -import numpy as np import pandas as pd from pandas import Timestamp @@ -266,61 +265,3 @@ def tp(self) -> Optional[Order]: @tp.setter def tp(self, order: Order): self.__tp_order = order - - -class Position: - """ - 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"" - ) diff --git a/pytrade/strategy.py b/pytrade/strategy.py index 5d2cd9e..afc6133 100644 --- a/pytrade/strategy.py +++ b/pytrade/strategy.py @@ -9,7 +9,6 @@ MINUTES_MAP, UPDATE_MAP, CandleSubscription, - FxInstrument, Granularity, Instrument, ) diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py index e4a11bd..ea135d7 100644 --- a/tests/unit/test_models.py +++ b/tests/unit/test_models.py @@ -2,10 +2,9 @@ from unittest.mock import MagicMock import pandas as pd -import pytest from pandas import Timestamp -from pytrade.models import Order, Position, TimeInForce, Trade +from pytrade.models import Order, TimeInForce, Trade def test_order(): @@ -109,100 +108,3 @@ def test_trade_reduce(): trade.reduce(50) assert trade.size == 50 - - -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)