From 22997a716658dd947bb99222eb96ecbeecc2dcba Mon Sep 17 00:00:00 2001 From: edtechre Date: Tue, 11 Apr 2023 00:44:18 -0700 Subject: [PATCH] Fix duplicate stop ID error. Fixes duplicate stop ID error when there are multiple symbols. Removes ExecContext#cancel_stop method. --- src/pybroker/context.py | 6 +- src/pybroker/portfolio.py | 7 +-- tests/test_context.py | 112 +++++++++++++++++--------------------- tests/test_strategy.py | 57 ++++++++++--------- 4 files changed, 82 insertions(+), 100 deletions(-) diff --git a/src/pybroker/context.py b/src/pybroker/context.py index d497ff8..4d99502 100644 --- a/src/pybroker/context.py +++ b/src/pybroker/context.py @@ -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], @@ -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(), diff --git a/src/pybroker/portfolio.py b/src/pybroker/portfolio.py index 1696484..f70cbdc 100644 --- a/src/pybroker/portfolio.py +++ b/src/pybroker/portfolio.py @@ -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, @@ -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() diff --git a/tests/test_context.py b/tests/test_context.py index de06b69..b654cc1 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -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() @@ -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"]) @@ -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( diff --git a/tests/test_strategy.py b/tests/test_strategy.py index 2028b69..2508139 100644 --- a/tests/test_strategy.py +++ b/tests/test_strategy.py @@ -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" @@ -1837,7 +1837,17 @@ 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" @@ -1845,12 +1855,25 @@ def exec_fn(ctx): 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): @@ -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: