feat: add strategy and backtest services
- strategy_service.py combines opportunity + portfolio signals into unified buy/sell/hold recommendations - backtest_service.py runs walk-forward backtests on historical datasets with virtual cash and positions - CLI adds `strategy` and `backtest` commands with `--decision-interval` and other tuning parameters - Add tests for both services and CLI dispatch - Update CLAUDE.md with new architecture docs - Optimize model weights via opportunity optimizer Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -336,6 +336,76 @@ class CLITestCase(unittest.TestCase):
|
||||
max_examples=5,
|
||||
)
|
||||
|
||||
def test_strategy_dispatches(self):
|
||||
captured = {}
|
||||
with (
|
||||
patch.object(
|
||||
cli, "load_config", return_value={"binance": {"spot_base_url": "https://test", "recv_window": 5000}, "market": {"default_quote": "USDT"}, "opportunity": {"top_n": 10}}
|
||||
),
|
||||
patch.object(cli, "get_binance_credentials", return_value={"api_key": "k", "api_secret": "s"}),
|
||||
patch.object(cli, "SpotBinanceClient"),
|
||||
patch.object(
|
||||
cli.strategy_service,
|
||||
"generate_trade_signals",
|
||||
return_value={"buy": [{"symbol": "BTCUSDT", "score": 0.82}], "sell": [], "hold": []},
|
||||
),
|
||||
patch.object(
|
||||
cli, "print_output", side_effect=lambda payload, **kwargs: captured.setdefault("payload", payload)
|
||||
),
|
||||
):
|
||||
result = cli.main(["strategy", "-s", "BTCUSDT"])
|
||||
self.assertEqual(result, 0)
|
||||
self.assertEqual(captured["payload"]["buy"][0]["symbol"], "BTCUSDT")
|
||||
|
||||
def test_backtest_dispatches_without_private_client(self):
|
||||
captured = {}
|
||||
config = {"market": {"default_quote": "USDT"}, "opportunity": {}}
|
||||
with (
|
||||
patch.object(cli, "load_config", return_value=config),
|
||||
patch.object(cli, "_load_spot_client", side_effect=AssertionError("backtest should use dataset only")),
|
||||
patch.object(
|
||||
cli.backtest_service,
|
||||
"run_backtest",
|
||||
return_value={"summary": {"total_return_pct": 5.0, "win_rate": 0.6}, "trades": []},
|
||||
) as backtest_mock,
|
||||
patch.object(
|
||||
cli,
|
||||
"print_output",
|
||||
side_effect=lambda payload, **kwargs: captured.update({"payload": payload, "agent": kwargs["agent"]}),
|
||||
),
|
||||
):
|
||||
result = cli.main(
|
||||
[
|
||||
"backtest",
|
||||
"/tmp/dataset.json",
|
||||
"--initial-cash",
|
||||
"5000",
|
||||
"--max-positions",
|
||||
"3",
|
||||
"--position-size-pct",
|
||||
"20",
|
||||
"--commission-pct",
|
||||
"0.1",
|
||||
"--lookback",
|
||||
"12",
|
||||
"--agent",
|
||||
]
|
||||
)
|
||||
|
||||
self.assertEqual(result, 0)
|
||||
self.assertEqual(captured["payload"]["summary"]["total_return_pct"], 5.0)
|
||||
self.assertTrue(captured["agent"])
|
||||
backtest_mock.assert_called_once_with(
|
||||
config,
|
||||
dataset_path="/tmp/dataset.json",
|
||||
initial_cash=5000.0,
|
||||
max_positions=3,
|
||||
position_size_pct=0.2,
|
||||
commission_pct=0.001,
|
||||
lookback=12,
|
||||
decision_interval_minutes=None,
|
||||
)
|
||||
|
||||
def test_opportunity_optimize_dispatches_without_private_client(self):
|
||||
captured = {}
|
||||
config = {"market": {"default_quote": "USDT"}, "opportunity": {}}
|
||||
|
||||
Reference in New Issue
Block a user