test_search_btc_eth_bbmr_risk_variants.py 3.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. import importlib.util
  2. import sys
  3. from pathlib import Path
  4. import pytest
  5. from okx_codex_trader.models import Candle
  6. def load_module():
  7. path = Path(__file__).resolve().parents[1] / "scripts" / "search_btc_eth_bbmr_risk_variants.py"
  8. spec = importlib.util.spec_from_file_location("search_btc_eth_bbmr_risk_variants", path)
  9. assert spec is not None
  10. module = importlib.util.module_from_spec(spec)
  11. assert spec.loader is not None
  12. sys.modules[spec.name] = module
  13. spec.loader.exec_module(module)
  14. return module
  15. def test_add_cost_columns_records_net_metrics():
  16. module = load_module()
  17. row = {
  18. "total_return": 0.12,
  19. "annualized_return": 0.04,
  20. "max_drawdown": 0.20,
  21. "calmar": 0.20,
  22. }
  23. module.add_cost_columns(row, "maker_taker", 0.0021)
  24. assert row["cost_model"] == "maker_taker"
  25. assert row["roundtrip_cost_on_margin"] == pytest.approx(0.0021)
  26. assert row["net_total_return"] == pytest.approx(0.12)
  27. assert row["net_annualized_return"] == pytest.approx(0.04)
  28. assert row["net_max_drawdown"] == pytest.approx(0.20)
  29. assert row["net_calmar"] == pytest.approx(0.20)
  30. def test_risk_qualified_keeps_recent_horizons_per_cost_model():
  31. module = load_module()
  32. pandas = pytest.importorskip("pandas")
  33. rows = []
  34. for cost_model, ret_1y in (("maker_taker", 0.11), ("taker_taker", 0.07)):
  35. for horizon, total_return in (("full", 0.30), ("3y", 0.20), ("1y", ret_1y), ("6m", 0.08), ("3m", 0.06)):
  36. rows.append(
  37. {
  38. "cost_model": cost_model,
  39. "symbol": "BTC",
  40. "label": "bbmr-test",
  41. "horizon": horizon,
  42. "total_return": total_return,
  43. "annualized_return": 0.05,
  44. "max_drawdown": 0.20,
  45. "calmar": 0.25,
  46. "trades_per_month": 12.0,
  47. }
  48. )
  49. qualified = module.risk_qualified(pandas.DataFrame(rows)).sort_values("cost_model").reset_index(drop=True)
  50. assert qualified["cost_model"].tolist() == ["maker_taker", "taker_taker"]
  51. assert qualified.loc[0, "ret_1y"] == pytest.approx(0.11)
  52. assert qualified.loc[1, "ret_1y"] == pytest.approx(0.07)
  53. def test_exit_price_for_risk_hit_uses_open_when_long_stop_is_gapped():
  54. module = load_module()
  55. position = {"side": "long", "entry_price": 100.0, "stop_price": 99.0}
  56. candle = Candle(symbol="BTC-USDT-SWAP", ts=1, open=98.0, high=100.0, low=97.5, close=99.5, volume=1.0)
  57. assert module.exit_price_for_risk_hit(position, candle, None) == pytest.approx(98.0)
  58. def test_exit_price_for_risk_hit_uses_open_when_short_stop_is_gapped():
  59. module = load_module()
  60. position = {"side": "short", "entry_price": 100.0, "stop_price": 101.0}
  61. candle = Candle(symbol="ETH-USDT-SWAP", ts=1, open=102.0, high=103.0, low=99.0, close=100.0, volume=1.0)
  62. assert module.exit_price_for_risk_hit(position, candle, None) == pytest.approx(102.0)
  63. def test_exit_price_for_risk_hit_prefers_open_take_profit_before_intrabar_stop():
  64. module = load_module()
  65. position = {"side": "long", "entry_price": 100.0, "stop_price": 99.0}
  66. candle = Candle(symbol="BTC-USDT-SWAP", ts=1, open=102.0, high=103.0, low=98.0, close=100.0, volume=1.0)
  67. assert module.exit_price_for_risk_hit(position, candle, 101.0) == pytest.approx(102.0)