test_cli.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. import json
  2. from dataclasses import asdict
  3. from pathlib import Path
  4. import pytest
  5. from okx_codex_trader.backtest import run_backtest
  6. from okx_codex_trader.cli import main_factory
  7. from okx_codex_trader.config import Config
  8. from okx_codex_trader.models import Candle, OrderResult, Position, TradeSignal
  9. def sample_config() -> Config:
  10. return Config(api_key="key", api_secret="secret", api_passphrase="passphrase")
  11. def sample_candles(limit: int = 60, symbol: str = "BTC-USDT-SWAP") -> list[Candle]:
  12. candles = []
  13. for index in range(limit):
  14. price = 100.0 + index
  15. candles.append(
  16. Candle(
  17. symbol=symbol,
  18. ts=index,
  19. open=price,
  20. high=price + 1.0,
  21. low=price - 1.0,
  22. close=price + 0.5,
  23. volume=1_000.0 + index,
  24. )
  25. )
  26. return candles
  27. def valid_signal() -> dict[str, object]:
  28. return {
  29. "action": "long",
  30. "confidence": 0.8,
  31. "leverage": 2,
  32. "entry_price": 123.5,
  33. "take_profit_price": 130.0,
  34. "stop_loss_price": 119.0,
  35. "reason": "trend",
  36. }
  37. def fake_analyze_with_codex(candles: list[Candle], symbol: str, bar: str) -> TradeSignal:
  38. assert candles
  39. assert symbol == "BTC-USDT-SWAP"
  40. assert bar == "1H"
  41. return TradeSignal(**valid_signal())
  42. def real_write_text(path: str, text: str) -> None:
  43. Path(path).write_text(text)
  44. class FakeClient:
  45. def __init__(self):
  46. self.get_candles_called_with: tuple[str, str, int] | None = None
  47. self.get_positions_called_with: str | None = None
  48. self.place_demo_order_called = False
  49. self.place_demo_order_called_with: tuple[str, TradeSignal, float] | None = None
  50. def get_candles(self, symbol: str, bar: str, limit: int) -> list[Candle]:
  51. self.get_candles_called_with = (symbol, bar, limit)
  52. return sample_candles(limit=limit, symbol=symbol)
  53. def place_demo_order(self, symbol: str, signal: TradeSignal, margin_usdt: float) -> OrderResult:
  54. self.place_demo_order_called = True
  55. self.place_demo_order_called_with = (symbol, signal, margin_usdt)
  56. return OrderResult(
  57. status="placed",
  58. order_id="demo-order-1",
  59. symbol=symbol,
  60. side="buy",
  61. pos_side="long",
  62. order_type="limit",
  63. size=1.0,
  64. )
  65. def get_positions(self, symbol: str) -> list[Position]:
  66. self.get_positions_called_with = symbol
  67. return [Position(symbol=symbol, pos_side="long", size=2.0, avg_price=123.5)]
  68. def fake_client() -> FakeClient:
  69. return FakeClient()
  70. def build_main_with_stubs():
  71. client = fake_client()
  72. main = main_factory(
  73. load_config=lambda: sample_config(),
  74. client_factory=lambda config: client,
  75. analyze_fn=fake_analyze_with_codex,
  76. write_text=real_write_text,
  77. )
  78. return main, client
  79. def test_fetch_history_prints_candle_json(capsys):
  80. main, client = build_main_with_stubs()
  81. expected = [asdict(candle) for candle in sample_candles(limit=20)]
  82. exit_code = main(["fetch-history", "--symbol", "BTC-USDT-SWAP", "--bar", "1H", "--limit", "20"])
  83. assert exit_code == 0
  84. assert client.get_candles_called_with == ("BTC-USDT-SWAP", "1H", 20)
  85. assert json.loads(capsys.readouterr().out) == expected
  86. def test_backtest_prints_summary_json(capsys):
  87. main, client = build_main_with_stubs()
  88. expected = run_backtest(candles=sample_candles(limit=50), leverage=2).to_dict()
  89. exit_code = main(["backtest", "--symbol", "BTC-USDT-SWAP", "--bar", "1H", "--limit", "50", "--leverage", "2"])
  90. assert exit_code == 0
  91. assert client.get_candles_called_with == ("BTC-USDT-SWAP", "1H", 50)
  92. assert json.loads(capsys.readouterr().out) == expected
  93. def test_analyze_writes_output_file_and_stdout(tmp_path, capsys):
  94. main, client = build_main_with_stubs()
  95. output_file = tmp_path / "signal.json"
  96. exit_code = main(
  97. [
  98. "analyze",
  99. "--symbol",
  100. "BTC-USDT-SWAP",
  101. "--bar",
  102. "1H",
  103. "--limit",
  104. "20",
  105. "--output-file",
  106. str(output_file),
  107. ]
  108. )
  109. assert exit_code == 0
  110. assert output_file.exists()
  111. assert client.get_candles_called_with == ("BTC-USDT-SWAP", "1H", 20)
  112. stdout = capsys.readouterr().out.strip()
  113. file_text = output_file.read_text()
  114. assert stdout == file_text
  115. assert json.loads(stdout) == valid_signal()
  116. def test_paper_order_reads_signal_file_and_outputs_order_json(tmp_path, capsys):
  117. main, client = build_main_with_stubs()
  118. signal_file = tmp_path / "signal.json"
  119. signal_file.write_text(json.dumps(valid_signal()))
  120. expected = {
  121. "status": "placed",
  122. "order_id": "demo-order-1",
  123. "symbol": "BTC-USDT-SWAP",
  124. "side": "buy",
  125. "pos_side": "long",
  126. "order_type": "limit",
  127. "size": 1.0,
  128. }
  129. exit_code = main(
  130. [
  131. "paper-order",
  132. "--symbol",
  133. "BTC-USDT-SWAP",
  134. "--signal-file",
  135. str(signal_file),
  136. "--margin-usdt",
  137. "100",
  138. ]
  139. )
  140. assert exit_code == 0
  141. assert client.place_demo_order_called
  142. assert client.place_demo_order_called_with is not None
  143. assert client.place_demo_order_called_with[0] == "BTC-USDT-SWAP"
  144. assert asdict(client.place_demo_order_called_with[1]) == valid_signal()
  145. assert client.place_demo_order_called_with[2] == 100.0
  146. assert json.loads(capsys.readouterr().out) == expected
  147. def test_positions_prints_position_json(capsys):
  148. main, client = build_main_with_stubs()
  149. expected = [{"symbol": "BTC-USDT-SWAP", "pos_side": "long", "size": 2.0, "avg_price": 123.5}]
  150. exit_code = main(["positions", "--symbol", "BTC-USDT-SWAP"])
  151. assert exit_code == 0
  152. assert client.get_positions_called_with == "BTC-USDT-SWAP"
  153. assert json.loads(capsys.readouterr().out) == expected
  154. def test_cli_rejects_unsupported_symbol():
  155. main, _ = build_main_with_stubs()
  156. with pytest.raises(SystemExit):
  157. main(["fetch-history", "--symbol", "SOL-USDT-SWAP", "--bar", "1H", "--limit", "20"])
  158. def test_cli_rejects_leverage_out_of_range():
  159. main, _ = build_main_with_stubs()
  160. with pytest.raises(SystemExit):
  161. main(["backtest", "--symbol", "BTC-USDT-SWAP", "--bar", "1H", "--limit", "50", "--leverage", "4"])