Skip to content

Commit

Permalink
Fix duplicate stop ID error.
Browse files Browse the repository at this point in the history
Fixes duplicate stop ID error when there are multiple symbols. Removes ExecContext#cancel_stop method.
  • Loading branch information
edtechre committed Apr 11, 2023
1 parent 0b90a55 commit 22997a7
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 100 deletions.
6 changes: 1 addition & 5 deletions src/pybroker/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -909,10 +909,6 @@ def cancel_all_pending_orders(self, symbol: Optional[str] = None):
"""
self._pending_order_scope.remove_all(symbol)

def cancel_stop(self, stop_id: int) -> bool:
"""Cancels a :class:`pybroker.portfolio.Stop` with ``stop_id``."""
return self._portfolio.remove_stop(stop_id)

def cancel_stops(
self,
val: Union[str, Position, Entry],
Expand Down Expand Up @@ -963,7 +959,7 @@ def _create_stop(
points_dec = to_decimal(points)
if limit_price is not None:
limit_price_dec = to_decimal(limit_price)
self._stop_id += 1
ExecContext._stop_id += 1
return Stop(
id=self._stop_id,
symbol=self._get_symbol(),
Expand Down
7 changes: 3 additions & 4 deletions src/pybroker/portfolio.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,10 +301,6 @@ class Portfolio:
loss_rate: Running loss rate of trades.
"""

_order_id: int = 0
_entry_id: int = 0
_trade_id: int = 0

def __init__(
self,
cash: float,
Expand Down Expand Up @@ -340,6 +336,9 @@ def __init__(
self._wins: Decimal = Decimal()
self._logger = StaticScope.instance().logger
self._stop_data: dict[int, _StopData] = {}
self._order_id: int = 0
self._entry_id: int = 0
self._trade_id: int = 0

def _calculate_fees(self, fill_price: Decimal, shares: Decimal) -> Decimal:
fees = Decimal()
Expand Down
112 changes: 50 additions & 62 deletions tests/test_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
set_exec_ctx_data,
set_pos_size_ctx_data,
)
from pybroker.portfolio import Order, Portfolio, Position, Stop, Trade
from pybroker.portfolio import Order, Portfolio, Position, Trade


@pytest.fixture()
Expand Down Expand Up @@ -472,34 +472,27 @@ def test_to_result(ctx, symbol, date):
ctx.hold_bars = 2
ctx.score = 7
result = ctx.to_result()
assert result == ExecResult(
symbol=symbol,
date=date,
buy_fill_price=PriceType.AVERAGE,
buy_shares=20,
buy_limit_price=Decimal("99.99"),
sell_fill_price=PriceType.HIGH,
sell_shares=20,
sell_limit_price=Decimal("110.11"),
hold_bars=2,
score=7,
long_stops=frozenset(
[
Stop(
id=1,
symbol=symbol,
stop_type=StopType.BAR,
pos_type="long",
percent=None,
points=None,
bars=2,
fill_price=PriceType.HIGH,
limit_price=None,
)
]
),
short_stops=None,
)
assert result.symbol == symbol
assert result.date == date
assert result.buy_fill_price == PriceType.AVERAGE
assert result.buy_shares == 20
assert result.buy_limit_price == Decimal("99.99")
assert result.sell_fill_price == PriceType.HIGH
assert result.sell_shares == 20
assert result.sell_limit_price == Decimal("110.11")
assert result.hold_bars == 2
assert result.score == 7
assert len(result.long_stops) == 1
assert result.short_stops is None
stop = next(iter(result.long_stops))
assert stop.symbol == symbol
assert stop.stop_type == StopType.BAR
assert stop.pos_type == "long"
assert stop.percent is None
assert stop.points is None
assert stop.bars == 2
assert stop.fill_price == PriceType.HIGH
assert stop.limit_price is None


@pytest.mark.parametrize("pos_type", ["long", "short"])
Expand All @@ -525,50 +518,45 @@ def test_to_result_when_stop(
percent = stop_amount
else:
points = stop_amount
expected_stops = frozenset(
[
Stop(
id=1,
symbol=symbol,
stop_type=expected_stop_type,
pos_type=pos_type,
percent=percent,
points=points,
bars=None,
fill_price=None,
limit_price=stop_limit,
)
]
)
buy_shares = None
sell_shares = None
long_stops = None
short_stops = None
if pos_type == "long":
buy_shares = 100
long_stops = expected_stops
else:
sell_shares = 100
short_stops = expected_stops
ctx.buy_shares = buy_shares
ctx.sell_shares = sell_shares
setattr(ctx, stop_attr, stop_amount)
setattr(ctx, f"{stop_attr.replace('_pct', '')}_limit", stop_limit)
result = ctx.to_result()
assert result == ExecResult(
symbol=symbol,
date=date,
buy_fill_price=PriceType.MIDDLE,
buy_shares=buy_shares,
buy_limit_price=None,
sell_fill_price=PriceType.MIDDLE,
sell_shares=sell_shares,
sell_limit_price=None,
hold_bars=None,
score=None,
long_stops=long_stops,
short_stops=short_stops,
)
assert result.symbol == symbol
assert result.date == date
assert result.buy_fill_price == PriceType.MIDDLE
assert result.buy_limit_price is None
assert result.sell_fill_price == PriceType.MIDDLE
assert result.sell_limit_price is None
assert result.hold_bars is None
assert result.score is None
if pos_type == "long":
assert result.buy_shares == 100
assert result.sell_shares is None
assert len(result.long_stops) == 1
assert result.short_stops is None
stop = next(iter(result.long_stops))
else:
assert result.sell_shares == 100
assert result.buy_shares is None
assert len(result.short_stops) == 1
assert result.long_stops is None
stop = next(iter(result.short_stops))
assert stop.symbol == symbol
assert stop.stop_type == expected_stop_type
assert stop.pos_type == pos_type
assert stop.percent == percent
assert stop.points == points
assert stop.bars is None
assert stop.fill_price is None
assert stop.limit_price == stop_limit


@pytest.mark.parametrize(
Expand Down
57 changes: 28 additions & 29 deletions tests/test_strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -1820,13 +1820,13 @@ def exec_fn(ctx):
ctx.buy_shares = 100
ctx.stop_loss = 10

df = data_source_df[data_source_df["symbol"] == "SPY"]
df = data_source_df[data_source_df["symbol"].isin(["SPY", "AAPL"])]
dates = df["date"].unique()
dates = dates[dates <= np.datetime64(END_DATE)]
strategy = Strategy(data_source_df, START_DATE, END_DATE)
strategy.add_execution(exec_fn, "SPY")
strategy.add_execution(exec_fn, ["SPY", "AAPL"])
result = strategy.backtest(calc_bootstrap=False)
assert len(result.trades) == 1
assert len(result.trades) == 2
trade = result.trades.iloc[0]
assert trade["type"] == "long"
assert trade["symbol"] == "SPY"
Expand All @@ -1837,20 +1837,43 @@ def exec_fn(ctx):
assert trade["agg_pnl"] == -1000
assert trade["pnl_per_bar"] == round(-1000 / trade["bars"], 2)
assert trade["stop"] == "loss"
assert len(result.orders) == 2
trade = result.trades.iloc[1]
assert trade["type"] == "long"
assert trade["symbol"] == "AAPL"
assert trade["entry_date"] == dates[1]
assert trade["exit"] == trade["entry"] - 10
assert trade["shares"] == 100
assert trade["pnl"] == -1000
assert trade["agg_pnl"] == -2000
assert trade["pnl_per_bar"] == round(-1000 / trade["bars"], 2)
assert trade["stop"] == "loss"
assert len(result.orders) == 4
buy_order = result.orders.iloc[0]
assert buy_order["type"] == "buy"
assert buy_order["symbol"] == "SPY"
assert buy_order["date"] == dates[1]
assert buy_order["shares"] == 100
assert np.isnan(buy_order["limit_price"])
assert buy_order["fees"] == 0
sell_order = result.orders.iloc[1]
buy_order = result.orders.iloc[1]
assert buy_order["type"] == "buy"
assert buy_order["symbol"] == "AAPL"
assert buy_order["date"] == dates[1]
assert buy_order["shares"] == 100
assert np.isnan(buy_order["limit_price"])
assert buy_order["fees"] == 0
sell_order = result.orders.iloc[2]
assert sell_order["type"] == "sell"
assert sell_order["symbol"] == "SPY"
assert sell_order["shares"] == 100
assert np.isnan(sell_order["limit_price"])
assert sell_order["fees"] == 0
sell_order = result.orders.iloc[3]
assert sell_order["type"] == "sell"
assert sell_order["symbol"] == "AAPL"
assert sell_order["shares"] == 100
assert np.isnan(sell_order["limit_price"])
assert sell_order["fees"] == 0

def test_backtest_when_sell_before_stop_loss(self, data_source_df):
def exec_fn(ctx):
Expand Down Expand Up @@ -1890,30 +1913,6 @@ def exec_fn(ctx):
assert np.isnan(sell_order["limit_price"])
assert sell_order["fees"] == 0

def test_backtest_when_cancel_stop(self, data_source_df):
def exec_fn(ctx):
if ctx.bars == 1:
ctx.buy_shares = 100
ctx.stop_loss = 10
elif ctx.bars == 10:
assert ctx.cancel_stop(stop_id=1)

df = data_source_df[data_source_df["symbol"] == "SPY"]
dates = df["date"].unique()
dates = dates[dates <= np.datetime64(END_DATE)]
strategy = Strategy(data_source_df, START_DATE, END_DATE)
strategy.add_execution(exec_fn, "SPY")
result = strategy.backtest(calc_bootstrap=False)
assert not len(result.trades)
assert len(result.orders) == 1
buy_order = result.orders.iloc[0]
assert buy_order["type"] == "buy"
assert buy_order["symbol"] == "SPY"
assert buy_order["date"] == dates[1]
assert buy_order["shares"] == 100
assert np.isnan(buy_order["limit_price"])
assert buy_order["fees"] == 0

def test_backtest_when_cancel_stops(self, data_source_df):
def exec_fn(ctx):
if ctx.bars == 1:
Expand Down

0 comments on commit 22997a7

Please sign in to comment.