"""Caller-supplied OHLC, causal signals and the real shared daily ledger.""" from __future__ import annotations import numpy as np import pandas as pd import pytest from quant_engine import strategy_research as research def bars(closes, opens=None): close = np.asarray(closes, dtype=float) opening = np.asarray(opens if opens is not None else closes, dtype=float) return pd.DataFrame( { "open": opening, "high": np.maximum(close, opening), "low": np.minimum(close, opening), "close": close, }, index=pd.date_range("2026-01-01", periods=len(close), freq="B"), ) def run(name, feed, **kwargs): return research.run_strategy_research( name, feed, asset="SYNTHETIC", initial_cash=1000, commission=0, stamp_duty=0, **kwargs ) def test_buy_hold_signal_close_next_open_and_last_close_value(): result = research.run_strategy_research( "BuyAndHold", bars([10, 11, 12, 13], [10, 10, 11, 12]), asset="SYNTHETIC", initial_cash=1000, commission=0.01, stamp_duty=0, params={"buy_pct": 1}, ) assert result.ledger.nav_series.tolist() == pytest.approx( [1000, 1100 / 1.01, 1200 / 1.01, 1300 / 1.01] ) assert result.ledger.total_rebalances == 1 trade = result.ledger.trades_frame.iloc[0] assert trade["trade_date"] == "2026-01-02" assert trade["price"] == 10 assert trade["fee"] == pytest.approx(1000 - 1000 / 1.01) assert result.signals[0].decision_date == "2026-01-01" assert result.signals[0].execution_date == "2026-01-02" assert result.signals[0].status == "partial_fill" assert result.metrics["total_return"] == pytest.approx(1300 / 1.01 / 1000 - 1) assert result.pairing.win_rate is None @pytest.mark.parametrize( "name,params,closes", [ ("SmaCross", {"fast": 1, "slow": 2}, [10, 8, 12, 6, 14, 5, 12]), ("MACross", {"fast": 1, "slow": 2}, [10, 8, 12, 6, 14, 5, 12]), ("RSI", {"period": 2}, [10, 8, 6, 10, 14, 8, 6, 10]), ("BollingerBreakout", {"period": 2, "std_mult": 0.5}, [10, 10, 12, 8, 12, 8]), ("DualThrust", {"period": 2, "k1": 0.5, "k2": 0.5}, [10, 10, 12, 8, 12, 8]), ("TurtleBreakout", {"entry_period": 2, "exit_period": 2}, [10, 10, 12, 8, 12, 8]), ], ) def test_each_strategy_has_real_entry_exit_and_sparse_execution(name, params, closes): opening = [closes[0], *closes[:-1]] result = run(name, bars(closes, opening), params=params) trades = result.ledger.trades_frame assert trades["side"].iloc[:2].tolist() == ["buy", "sell"] assert result.signals[0].decision_date == "2026-01-05" assert trades["trade_date"].iloc[0] == "2026-01-06" for signal in result.signals: if signal.execution_date: assert signal.execution_date > signal.decision_date assert len(result.ledger.positions) == len(closes) assert all(position.cash >= -1e-9 for position in result.ledger.positions) assert result.pairing.closed_lots def test_final_day_signal_is_recorded_without_same_close_execution(): result = run("SmaCross", bars([10, 8, 12]), params={"fast": 1, "slow": 2}) assert result.ledger.trades_frame.empty assert len(result.signals) == 1 assert result.signals[0].status == "no_next_session" assert result.signals[0].execution_date is None def test_atr_stop_uses_prior_peak_and_prior_atr_and_can_trigger(): feed = bars([10, 8, 8, 10, 20, 19, 18], [10, 10, 8, 8, 10, 20, 19]) params = {"fast": 2, "slow": 3, "atr_period": 1, "atr_mult": 0.1} result = run("MACross", feed, params=params) stop = next(signal for signal in result.signals if signal.reason == "atr_stop") assert stop.decision_date == "2026-01-08" assert stop.execution_date == "2026-01-09" assert result.ledger.trades_frame.iloc[-1]["side"] == "sell" disabled = run("MACross", feed, params=params | {"atr_period": 0}) assert all(signal.reason != "atr_stop" for signal in disabled.signals) def test_flat_rsi_is_neutral_and_does_not_create_artificial_trades(): result = run("RSI", bars([10] * 8), params={"period": 2, "oversold": 40, "overbought": 60}) assert result.ledger.trades_frame.empty assert result.metrics["sharpe"] is None assert result.metric_unavailable["sharpe"] == "zero_volatility" @pytest.mark.parametrize( "name", [ "BuyAndHold", "SmaCross", "MACross", "RSI", "BollingerBreakout", "DualThrust", "TurtleBreakout", ], ) def test_default_parameters_and_future_perturbation_preserve_observed_prefix(name): values = 10 + np.sin(np.arange(80) / 2) * 2 original = bars(values, np.r_[values[0], values[:-1]]) changed = original.copy() changed.iloc[55:] *= 7 first = run(name, original) second = run(name, changed) assert first.ledger.positions[:55] == second.ledger.positions[:55] assert first.ledger.daily_executions[:55] == second.ledger.daily_executions[:55] observed = original.index[53].strftime("%Y-%m-%d") assert [s for s in first.signals if s.decision_date <= observed] == [ s for s in second.signals if s.decision_date <= observed ] assert first.parameters == research.strategy_parameters(name) @pytest.mark.parametrize( "mutation", [ "missing_open", "missing_high", "missing_low", "null", "boolean", "string", "infinite", "bad_bounds", "duplicate", "unsorted", ], ) def test_invalid_ohlc_fails_before_ledger(monkeypatch, mutation): feed = bars([10] * 40) if mutation.startswith("missing_"): feed = feed.drop(columns=mutation[8:]) elif mutation == "duplicate": feed.index = [feed.index[0]] * len(feed) elif mutation == "unsorted": feed = feed.iloc[::-1] elif mutation == "bad_bounds": feed.iloc[0, feed.columns.get_loc("high")] = 9 else: feed = feed.astype(object) feed.iloc[0, 0] = {"null": None, "boolean": True, "string": "10", "infinite": float("inf")}[ mutation ] calls = [] monkeypatch.setattr( research, "simulate_daily_ledger_with_audit", lambda *a, **kw: calls.append(1) ) with pytest.raises(ValueError, match=r"OHLC|prices|DatetimeIndex"): run("DualThrust", feed) assert calls == [] @pytest.mark.parametrize( "name,params", [ ("SmaCross", {"fast": True}), ("SmaCross", {"fast": 20}), ("RSI", {"oversold": 70}), ("MACross", {"atr_period": -1}), ("TurtleBreakout", {"entry_period": "20"}), ("BuyAndHold", {"buy_pct": float("nan")}), ("DualThrust", {"unknown": 1}), ], ) def test_invalid_parameters_before_ledger(monkeypatch, name, params): calls = [] monkeypatch.setattr( research, "simulate_daily_ledger_with_audit", lambda *a, **kw: calls.append(1) ) with pytest.raises(ValueError, match=r"parameter|finite|less than|bounds|integer"): run(name, bars([10] * 40), params=params) assert calls == [] def test_insufficient_history_is_failure_before_ledger(monkeypatch): calls = [] monkeypatch.setattr( research, "simulate_daily_ledger_with_audit", lambda *a, **kw: calls.append(1) ) with pytest.raises(ValueError, match=r"history"): run("MACross", bars([10] * 30)) assert calls == [] def test_zero_allocation_is_no_change_not_a_filled_position(): result = run("BuyAndHold", bars([10, 11, 12]), params={"buy_pct": 0}) assert result.ledger.trades_frame.empty assert result.signals[0].status == "no_change" assert result.ledger.nav_series.tolist() == [1000, 1000, 1000] def test_exit_below_minimum_is_not_filled_and_state_keeps_actual_holdings(): result = run( "SmaCross", bars([10, 8, 12, 6, 14, 5, 12], [10, 10, 8, 12, 6, 14, 5]), params={"fast": 1, "slow": 2}, min_trade_amount=900, ) exit_signal = result.signals[1] assert exit_signal.target_weight == 0 assert exit_signal.status == "not_filled" assert result.ledger.positions[4].holdings == {"SYNTHETIC": pytest.approx(1000 / 12)} assert len(result.ledger.trades_frame) == 1 def test_total_loss_from_explicit_full_sell_fee_is_reported_as_zero_nav(): result = research.run_strategy_research( "SmaCross", bars([10, 8, 12, 6, 14, 5]), asset="SYNTHETIC", initial_cash=1000, commission=0, stamp_duty=1, params={"fast": 1, "slow": 2}, ) assert result.ledger.nav_series.iloc[-1] == 0 assert result.metrics["total_return"] == -1 assert result.pairing.win_rate == 0 def test_turtle_entry_does_not_wait_for_longer_exit_lookback(): result = run( "TurtleBreakout", bars([10, 10, 12, 13, 14, 15, 16]), params={"entry_period": 2, "exit_period": 5}, ) assert result.signals[0].decision_date == "2026-01-05" def test_ma_entry_does_not_wait_for_optional_atr_calibration(): result = run( "MACross", bars([10, 8, 12, 6, 14, 5, 12]), params={"fast": 1, "slow": 2, "atr_period": 5} ) assert result.signals[0].decision_date == "2026-01-05" def test_benchmark_exact_calendar_and_distinct_empty_unrequested_states(): feed = bars([10, 10, 10]) benchmark = pd.Series([100.0, 90.0, 99.0], index=feed.index) result = run("BuyAndHold", feed, benchmark=research.BenchmarkInput("present", benchmark)) assert result.benchmark_status == "present" assert result.benchmark_nav.tolist() == pytest.approx([1, 0.9, 0.99]) assert result.benchmark_metrics["total_return"] == pytest.approx(-0.01) empty = run( "BuyAndHold", feed, benchmark=research.BenchmarkInput("empty", pd.Series(dtype=float)) ) none = run("BuyAndHold", feed) assert empty.benchmark_status == "empty" assert empty.benchmark_nav is None assert none.benchmark_status == "not_requested" assert none.benchmark_nav is None @pytest.mark.parametrize("status", ["missing_day", "duplicate", "source_error"]) def test_bad_benchmark_fails_before_any_strategy_execution(monkeypatch, status): feed = bars([10, 10, 10]) series = pd.Series([100.0, 90.0, 99.0], index=feed.index) if status == "missing_day": series = series.iloc[:2] elif status == "duplicate": series.index = [feed.index[0]] * 3 observation = research.BenchmarkInput( "source_error" if status == "source_error" else "present", series if status != "source_error" else None, ) calls = [] monkeypatch.setattr( research, "simulate_daily_ledger_with_audit", lambda *a, **kw: calls.append(1) ) with pytest.raises(ValueError, match=r"Benchmark|benchmark|DatetimeIndex"): run("BuyAndHold", feed, benchmark=observation) assert calls == [] def test_benchmark_numeric_underflow_is_not_filled_as_zero_return(monkeypatch): feed = bars([10] * 4) benchmark = research.BenchmarkInput( "present", pd.Series([1e300, 1e300, 1e-300, 1e-300], index=feed.index) ) calls = [] real_ledger = research.simulate_daily_ledger_with_audit def observed(*args, **kwargs): calls.append(1) return real_ledger(*args, **kwargs) monkeypatch.setattr(research, "simulate_daily_ledger_with_audit", observed) with pytest.raises(ValueError, match=r"benchmark.*numeric|Benchmark.*numeric"): run("BuyAndHold", feed, benchmark=benchmark) assert calls == [] def test_nonfinite_portfolio_return_cannot_be_silently_dropped_from_metrics(): with pytest.raises(ValueError, match=r"return.*finite|return.*numeric"): run("BuyAndHold", bars([10, 10, 1e-300, 1e300]), params={"buy_pct": 1}) @pytest.mark.parametrize( "name,params,closes,expected_nav,expected_cash,prices,quantities,pnl", [ ( "SmaCross", {"fast": 1, "slow": 2}, [10, 8, 12, 6, 14, 5, 12], [1000, 1000, 1000, 500, 500, 1250 / 7, 1250 / 7], [1000, 1000, 1000, 0, 500, 0, 1250 / 7], [12, 6, 14, 5], [250 / 3, 250 / 3, 250 / 7, 250 / 7], -5750 / 7, ), ( "MACross", {"fast": 1, "slow": 2}, [10, 8, 12, 6, 14, 5, 12], [1000, 1000, 1000, 500, 500, 1250 / 7, 1250 / 7], [1000, 1000, 1000, 0, 500, 0, 1250 / 7], [12, 6, 14, 5], [250 / 3, 250 / 3, 250 / 7, 250 / 7], -5750 / 7, ), ( "RSI", {"period": 2}, [10, 8, 6, 10, 14, 8, 6, 10], [1000, 1000, 1000, 5000 / 3, 7000 / 3, 7000 / 3, 7000 / 3, 35000 / 9], [1000, 1000, 1000, 0, 0, 7000 / 3, 7000 / 3, 0], [6, 14, 6], [500 / 3, 500 / 3, 3500 / 9], 4000 / 3, ), ( "BollingerBreakout", {"period": 2, "std_mult": 0.5}, [10, 10, 12, 8, 12, 8], [1000, 1000, 1000, 2000 / 3, 2000 / 3, 4000 / 9], [1000, 1000, 1000, 0, 2000 / 3, 0], [12, 8, 12], [250 / 3, 250 / 3, 500 / 9], -1000 / 3, ), ( "DualThrust", {"period": 2, "k1": 0.5, "k2": 0.5}, [10, 10, 12, 8, 12, 8], [1000, 1000, 1000, 2000 / 3, 2000 / 3, 4000 / 9], [1000, 1000, 1000, 0, 2000 / 3, 0], [12, 8, 12], [250 / 3, 250 / 3, 500 / 9], -1000 / 3, ), ( "TurtleBreakout", {"entry_period": 2, "exit_period": 2}, [10, 10, 12, 8, 12, 8], [1000, 1000, 1000, 2000 / 3, 2000 / 3, 2000 / 3], [1000, 1000, 1000, 0, 2000 / 3, 2000 / 3], [12, 8], [250 / 3, 250 / 3], -1000 / 3, ), ], ) def test_hand_calculated_strategy_cash_nav_and_every_fill( name, params, closes, expected_nav, expected_cash, prices, quantities, pnl ): # These rational constants were calculated from the expected sparse trades, # independently of the signal and ledger implementation. result = run(name, bars(closes, [closes[0], *closes[:-1]]), params=params) assert result.ledger.nav_series.tolist() == pytest.approx(expected_nav) assert [position.cash for position in result.ledger.positions] == pytest.approx(expected_cash) assert result.ledger.trades_frame["price"].tolist() == prices assert result.ledger.trades_frame["qty"].tolist() == pytest.approx(quantities) assert result.pairing.realized_net_pnl == pytest.approx(pnl) assert result.metrics["total_return"] == pytest.approx(expected_nav[-1] / 1000 - 1) @pytest.mark.parametrize( "field,value", [ ("initial_cash", True), ("initial_cash", 0), ("commission", "0.01"), ("commission", -1), ("stamp_duty", float("nan")), ("stamp_duty", 2), ], ) def test_money_contract_fails_before_ledger(monkeypatch, field, value): calls = [] monkeypatch.setattr( research, "simulate_daily_ledger_with_audit", lambda *a, **kw: calls.append(1) ) with pytest.raises(ValueError, match=r"number|bounds|fee|Fee"): research.run_strategy_research( "BuyAndHold", bars([10, 10]), asset="SYNTHETIC", **{field: value} ) assert calls == [] @pytest.mark.parametrize("scale", [1e-200, 1e200]) def test_bollinger_signal_is_invariant_to_representable_price_scaling(scale): original = bars([10, 10, 12, 8, 12, 8]) normal = run("BollingerBreakout", original, params={"period": 2, "std_mult": 2}) scaled = run("BollingerBreakout", original * scale, params={"period": 2, "std_mult": 2}) assert scaled.signals == normal.signals assert scaled.ledger.nav_series.tolist() == normal.ledger.nav_series.tolist() def test_no_downside_sortino_is_explicitly_unavailable(): result = run("BuyAndHold", bars([10, 10, 11, 12]), params={"buy_pct": 0.5}) assert result.metrics["sortino"] is None assert result.metric_unavailable["sortino"] == "no_downside_deviation"