test_cli.py 5.8 KB

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