test_live_execution.py 7.8 KB

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