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
5 changes: 3 additions & 2 deletions pytrade/broker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
Expand Down
5 changes: 3 additions & 2 deletions pytrade/interfaces/broker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down
42 changes: 42 additions & 0 deletions pytrade/interfaces/position.py
Original file line number Diff line number Diff line change
@@ -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()
59 changes: 0 additions & 59 deletions pytrade/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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"<Position[{self.__instrument}]: {self.size} ({len(self.trades)} trades)>"
)
1 change: 0 additions & 1 deletion pytrade/strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
MINUTES_MAP,
UPDATE_MAP,
CandleSubscription,
FxInstrument,
Granularity,
Instrument,
)
Expand Down
100 changes: 1 addition & 99 deletions tests/unit/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down Expand Up @@ -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)