test_live_execution.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. import pytest
  2. from okx_codex_trader.live_execution import (
  3. RuntimeState,
  4. TargetPosition,
  5. current_position_from_okx,
  6. plan_position_delta,
  7. render_market_order_bodies,
  8. market_client_order_id,
  9. target_from_signal,
  10. )
  11. from okx_codex_trader.models import InstrumentMeta, Position
  12. def nextgen_payload(*, candle_ts=1000, first_signal=False, first_exit=False, second_signal=False, second_exit=False):
  13. return {
  14. "decision": {"active_engine": "nextgen"},
  15. "nextgen": {
  16. "decision": {"decision_candle_ts": candle_ts},
  17. "legs": [
  18. {"leg_id": "a", "suggested_weight": 0.5, "signal": first_signal, "exit_signal": first_exit},
  19. {"leg_id": "b", "suggested_weight": 0.5, "signal": second_signal, "exit_signal": second_exit},
  20. ],
  21. },
  22. }
  23. def test_nextgen_target_opens_one_virtual_leg():
  24. state, target = target_from_signal(nextgen_payload(first_signal=True), RuntimeState(None, (), None))
  25. assert state.nextgen_active_legs == ("a",)
  26. assert target == TargetPosition(side="long", unit=0.5, known=True, reason="active nextgen virtual legs net to one long ETH target")
  27. def test_nextgen_target_nets_two_virtual_legs_to_one_unit():
  28. state, target = target_from_signal(nextgen_payload(first_signal=True, second_signal=True), RuntimeState(None, (), None))
  29. assert state.nextgen_active_legs == ("a", "b")
  30. assert target.side == "long"
  31. assert target.unit == 1.0
  32. def test_nextgen_exit_removes_only_active_leg():
  33. previous = RuntimeState(last_candle_ts=1000, nextgen_active_legs=("a", "b"), micro_side=None)
  34. state, target = target_from_signal(nextgen_payload(candle_ts=2000, first_exit=True), previous)
  35. assert state.nextgen_active_legs == ("b",)
  36. assert target.side == "long"
  37. assert target.unit == 0.5
  38. def test_repeated_candle_is_idempotent():
  39. previous = RuntimeState(last_candle_ts=1000, nextgen_active_legs=("a",), micro_side=None)
  40. state, target = target_from_signal(nextgen_payload(candle_ts=1000, first_signal=True, second_signal=True), previous)
  41. assert state == previous
  42. assert target.side == "long"
  43. assert target.unit == 0.5
  44. def test_micro_target_is_blocked_until_exit_state_exists():
  45. payload = {"decision": {"active_engine": "micro"}, "micro": {"decision_candle_ts": 1000}}
  46. _, target = target_from_signal(payload, RuntimeState(None, (), None))
  47. assert target.known is False
  48. assert target.unit == 0.0
  49. def test_position_delta_plans_reduce_only_close_before_reverse():
  50. current = TargetPosition(side="long", unit=1.0, known=True, reason="current")
  51. target = TargetPosition(side="short", unit=0.5, known=True, reason="target")
  52. plan = plan_position_delta(current, target)
  53. assert [(action.action, action.side, action.unit, action.reduce_only) for action in plan.actions] == [
  54. ("close", "long", 1.0, True),
  55. ("reverse", "short", 0.5, False),
  56. ]
  57. def test_position_delta_does_not_plan_when_current_position_is_unknown():
  58. current = TargetPosition(side="flat", unit=0.0, known=False, reason="unknown")
  59. target = TargetPosition(side="long", unit=0.5, known=True, reason="target")
  60. plan = plan_position_delta(current, target)
  61. assert plan.actions == ()
  62. def test_current_position_from_okx_normalizes_contracts_to_strategy_units():
  63. current = current_position_from_okx(
  64. positions=[Position(symbol="ETH-USDT-SWAP", pos_side="long", size=10.0, avg_price=3000.0)],
  65. mark_price=3000.0,
  66. metadata=InstrumentMeta(ct_val=0.1, lot_sz=1.0, min_sz=1.0),
  67. leverage=3,
  68. margin_per_unit_usdt=1000.0,
  69. )
  70. assert current.side == "long"
  71. assert current.unit == pytest.approx(1.0)
  72. assert current.known is True
  73. def test_current_position_from_okx_blocks_when_both_hedge_sides_are_open():
  74. current = current_position_from_okx(
  75. positions=[
  76. Position(symbol="ETH-USDT-SWAP", pos_side="long", size=1.0, avg_price=3000.0),
  77. Position(symbol="ETH-USDT-SWAP", pos_side="short", size=1.0, avg_price=3000.0),
  78. ],
  79. mark_price=3000.0,
  80. metadata=InstrumentMeta(ct_val=0.1, lot_sz=1.0, min_sz=1.0),
  81. leverage=3,
  82. margin_per_unit_usdt=1000.0,
  83. )
  84. assert current.known is False
  85. def test_render_market_order_bodies_builds_open_order_body():
  86. plan = plan_position_delta(
  87. TargetPosition(side="flat", unit=0.0, known=True, reason="current"),
  88. TargetPosition(side="long", unit=0.5, known=True, reason="target"),
  89. )
  90. orders = render_market_order_bodies(
  91. plan=plan,
  92. symbol="ETH-USDT-SWAP",
  93. mark_price=3000.0,
  94. metadata=InstrumentMeta(ct_val=0.1, lot_sz=1.0, min_sz=1.0),
  95. leverage=3,
  96. margin_per_unit_usdt=1000.0,
  97. max_new_margin_usdt=500.0,
  98. max_total_margin_usdt=1000.0,
  99. client_order_id_prefix="eth-1000",
  100. )
  101. assert len(orders) == 1
  102. assert orders[0].margin_usdt == 500.0
  103. assert orders[0].body == {
  104. "instId": "ETH-USDT-SWAP",
  105. "tdMode": "isolated",
  106. "side": "buy",
  107. "posSide": "long",
  108. "ordType": "market",
  109. "sz": "5",
  110. "clOrdId": "eth10001open",
  111. }
  112. def test_render_market_order_bodies_builds_reduce_only_close_before_reverse():
  113. plan = plan_position_delta(
  114. TargetPosition(side="long", unit=1.0, known=True, reason="current"),
  115. TargetPosition(side="short", unit=0.5, known=True, reason="target"),
  116. )
  117. orders = render_market_order_bodies(
  118. plan=plan,
  119. symbol="ETH-USDT-SWAP",
  120. mark_price=3000.0,
  121. metadata=InstrumentMeta(ct_val=0.1, lot_sz=1.0, min_sz=1.0),
  122. leverage=3,
  123. margin_per_unit_usdt=1000.0,
  124. max_new_margin_usdt=500.0,
  125. max_total_margin_usdt=1000.0,
  126. client_order_id_prefix="eth-2000",
  127. )
  128. assert [order.body for order in orders] == [
  129. {
  130. "instId": "ETH-USDT-SWAP",
  131. "tdMode": "isolated",
  132. "side": "sell",
  133. "posSide": "long",
  134. "ordType": "market",
  135. "sz": "10",
  136. "clOrdId": "eth20001close",
  137. "reduceOnly": "true",
  138. },
  139. {
  140. "instId": "ETH-USDT-SWAP",
  141. "tdMode": "isolated",
  142. "side": "sell",
  143. "posSide": "short",
  144. "ordType": "market",
  145. "sz": "5",
  146. "clOrdId": "eth20002reverse",
  147. },
  148. ]
  149. def test_market_client_order_id_removes_unsupported_characters_and_caps_length():
  150. assert market_client_order_id("bbsq-1778508900000", 1, "open") == "bbsq17785089000001open"
  151. assert len(market_client_order_id("x" * 40, 1, "open")) == 32
  152. def test_render_market_order_bodies_enforces_new_margin_cap():
  153. plan = plan_position_delta(
  154. TargetPosition(side="flat", unit=0.0, known=True, reason="current"),
  155. TargetPosition(side="long", unit=1.0, known=True, reason="target"),
  156. )
  157. with pytest.raises(ValueError, match="new margin exceeds max_new_margin_usdt"):
  158. render_market_order_bodies(
  159. plan=plan,
  160. symbol="ETH-USDT-SWAP",
  161. mark_price=3000.0,
  162. metadata=InstrumentMeta(ct_val=0.1, lot_sz=1.0, min_sz=1.0),
  163. leverage=3,
  164. margin_per_unit_usdt=1000.0,
  165. max_new_margin_usdt=500.0,
  166. max_total_margin_usdt=1000.0,
  167. client_order_id_prefix="eth-3000",
  168. )
  169. def test_render_market_order_bodies_enforces_total_margin_cap():
  170. plan = plan_position_delta(
  171. TargetPosition(side="long", unit=0.5, known=True, reason="current"),
  172. TargetPosition(side="long", unit=1.0, known=True, reason="target"),
  173. )
  174. with pytest.raises(ValueError, match="target margin exceeds max_total_margin_usdt"):
  175. render_market_order_bodies(
  176. plan=plan,
  177. symbol="ETH-USDT-SWAP",
  178. mark_price=3000.0,
  179. metadata=InstrumentMeta(ct_val=0.1, lot_sz=1.0, min_sz=1.0),
  180. leverage=3,
  181. margin_per_unit_usdt=1000.0,
  182. max_new_margin_usdt=500.0,
  183. max_total_margin_usdt=500.0,
  184. client_order_id_prefix="eth-4000",
  185. )