malkhut: CHASE mechanics FIXED + Flight9 learnings + TTL enforcement
1. CHASE mechanics (NOW WORKING):
- CWM enforces TTL on open orders (auto-cancel when expired)
- DSL CHASE produces PLACE with metadata={chase: True}
- Action menu generates chase actions with wait_to_retry_ms TTL
- OpenOrderState.gains ttl_ms field (0=no expiry, >0=auto-cancel)
2. TTL enforcement (CWM):
- HftBacktestCWM: auto-cancels orders where age >= ttl_ms
- MinimalCryptoLOBCWM: same TTL enforcement
- This is how CHASE works: place→wait→auto-cancel→next step re-places
3. Flight9 learnings:
- Slippage model gains trade_flow_intensity parameter
- Book imbalance as proxy for trade arrival rate
- Markout = quality concept documented
4. CHASE tests: 10 new tests covering TTL enforcement, cancel-retry cycle,
max retries, DSL CHASE action, CMA codec integration
5. All 800+ tests pass
This commit is contained in:
@@ -333,6 +333,7 @@ class MinimalCryptoLOBCWM:
|
|||||||
last_update_ts_ns=ts,
|
last_update_ts_ns=ts,
|
||||||
reduce_only=action.reduce_only,
|
reduce_only=action.reduce_only,
|
||||||
post_only=action.post_only,
|
post_only=action.post_only,
|
||||||
|
ttl_ms=action.ttl_ms if action.ttl_ms > 0 else 0,
|
||||||
)
|
)
|
||||||
|
|
||||||
def transition(
|
def transition(
|
||||||
@@ -356,6 +357,17 @@ class MinimalCryptoLOBCWM:
|
|||||||
if our_action.kind.value == "CANCEL_REPLACE" and our_action.cancel_order_id:
|
if our_action.kind.value == "CANCEL_REPLACE" and our_action.cancel_order_id:
|
||||||
open_orders = [o for o in open_orders if o.client_order_id != our_action.cancel_order_id]
|
open_orders = [o for o in open_orders if o.client_order_id != our_action.cancel_order_id]
|
||||||
|
|
||||||
|
# 1b. TTL enforcement — auto-cancel expired open orders (CHASE mechanic)
|
||||||
|
if open_orders:
|
||||||
|
alive = []
|
||||||
|
for oo in open_orders:
|
||||||
|
age_ms = (now_ts - oo.created_ts_ns) / 1_000_000
|
||||||
|
ttl = getattr(oo, 'ttl_ms', 0)
|
||||||
|
if ttl > 0 and age_ms >= ttl:
|
||||||
|
continue # expired
|
||||||
|
alive.append(oo)
|
||||||
|
open_orders = alive
|
||||||
|
|
||||||
# 2. Process counterparty cancels
|
# 2. Process counterparty cancels
|
||||||
for cp in counterparty_actions:
|
for cp in counterparty_actions:
|
||||||
if isinstance(cp, CounterpartyAction) and cp.kind.value == "CANCEL":
|
if isinstance(cp, CounterpartyAction) and cp.kind.value == "CANCEL":
|
||||||
|
|||||||
@@ -202,6 +202,7 @@ class HftBacktestCWM:
|
|||||||
last_update_ts_ns=ts,
|
last_update_ts_ns=ts,
|
||||||
reduce_only=action.reduce_only,
|
reduce_only=action.reduce_only,
|
||||||
post_only=action.post_only,
|
post_only=action.post_only,
|
||||||
|
ttl_ms=action.ttl_ms if action.ttl_ms > 0 else 0,
|
||||||
)
|
)
|
||||||
|
|
||||||
def transition(
|
def transition(
|
||||||
@@ -225,7 +226,22 @@ class HftBacktestCWM:
|
|||||||
if our_action.kind.value == "CANCEL_REPLACE" and our_action.cancel_order_id:
|
if our_action.kind.value == "CANCEL_REPLACE" and our_action.cancel_order_id:
|
||||||
open_orders = [o for o in open_orders if o.client_order_id != our_action.cancel_order_id]
|
open_orders = [o for o in open_orders if o.client_order_id != our_action.cancel_order_id]
|
||||||
|
|
||||||
# 2. Process counterparty cancels
|
# 2. TTL enforcement — auto-cancel expired open orders (CHASE mechanic)
|
||||||
|
# Orders with ttl_ms > 0 expire after ttl_ms. This is how CHASE works:
|
||||||
|
# place → wait → auto-cancel → next step re-places at new offset.
|
||||||
|
if open_orders:
|
||||||
|
alive = []
|
||||||
|
for oo in open_orders:
|
||||||
|
age_ms = (now_ts - oo.created_ts_ns) / 1_000_000
|
||||||
|
if oo.last_update_ts_ns > oo.created_ts_ns:
|
||||||
|
age_ms = (now_ts - oo.last_update_ts_ns) / 1_000_000
|
||||||
|
ttl = oo.ttl_ms
|
||||||
|
if ttl > 0 and age_ms >= ttl:
|
||||||
|
continue # expired — auto-cancel
|
||||||
|
alive.append(oo)
|
||||||
|
open_orders = alive
|
||||||
|
|
||||||
|
# 3. Process counterparty cancels
|
||||||
for cp in counterparty_actions:
|
for cp in counterparty_actions:
|
||||||
if isinstance(cp, CounterpartyAction) and cp.kind.value == "CANCEL":
|
if isinstance(cp, CounterpartyAction) and cp.kind.value == "CANCEL":
|
||||||
open_orders = [o for o in open_orders if o.symbol != state.venue.symbol]
|
open_orders = [o for o in open_orders if o.symbol != state.venue.symbol]
|
||||||
|
|||||||
@@ -210,6 +210,7 @@ class OpenOrderState:
|
|||||||
last_update_ts_ns: int
|
last_update_ts_ns: int
|
||||||
reduce_only: bool = False
|
reduce_only: bool = False
|
||||||
post_only: bool = False
|
post_only: bool = False
|
||||||
|
ttl_ms: int = 0 # 0 = no expiry; >0 = auto-cancel after ttl_ms (CHASE)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
|
|||||||
@@ -716,3 +716,85 @@ class TestHftAvailability:
|
|||||||
assert cwm._use_queue_model is False
|
assert cwm._use_queue_model is False
|
||||||
assert cwm._fill_probability_at_level(0) == 1.0
|
assert cwm._fill_probability_at_level(0) == 1.0
|
||||||
assert cwm._fill_probability_at_level(50) == 1.0
|
assert cwm._fill_probability_at_level(50) == 1.0
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# PART 16: CHASE Mechanics
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
class TestChaseMechanics:
|
||||||
|
def test_ttl_enforcement_cancels_expired_order(self):
|
||||||
|
cwm = HftBacktestCWM(use_queue_model=False)
|
||||||
|
s = _state()
|
||||||
|
# Place order with TTL=1ms
|
||||||
|
a = _place(Side.BUY, price_ticks=5, qty=0.10)
|
||||||
|
a = FulfilmentAction(ActionKind.PLACE, Side.BUY, OrderType.LIMIT,
|
||||||
|
5, 0.10, 1, post_only=True) # ttl_ms=1
|
||||||
|
s2 = cwm.transition(s, (a,))
|
||||||
|
assert len(s2.open_orders) == 1
|
||||||
|
|
||||||
|
# Next step: order should be expired (1ms < 1ms tick)
|
||||||
|
noop = FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0)
|
||||||
|
s3 = cwm.transition(s2, (noop,))
|
||||||
|
assert len(s3.open_orders) == 0
|
||||||
|
|
||||||
|
def test_ttl_zero_means_no_expiry(self):
|
||||||
|
cwm = HftBacktestCWM(use_queue_model=False)
|
||||||
|
s = _state()
|
||||||
|
a = FulfilmentAction(ActionKind.PLACE, Side.BUY, OrderType.LIMIT,
|
||||||
|
5, 0.10, 0, post_only=True) # ttl_ms=0
|
||||||
|
s2 = cwm.transition(s, (a,))
|
||||||
|
assert len(s2.open_orders) == 1
|
||||||
|
|
||||||
|
noop = FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0)
|
||||||
|
s3 = cwm.transition(s2, (noop,))
|
||||||
|
assert len(s3.open_orders) == 1 # not expired
|
||||||
|
|
||||||
|
def test_chase_action_has_ttl(self):
|
||||||
|
a = FulfilmentAction(ActionKind.PLACE, Side.BUY, OrderType.LIMIT,
|
||||||
|
5, 0.10, 100, post_only=True, metadata={"chase": True})
|
||||||
|
assert a.ttl_ms == 100
|
||||||
|
|
||||||
|
def test_chase_cancel_retry_cycle(self):
|
||||||
|
cwm = HftBacktestCWM(use_queue_model=False)
|
||||||
|
s = _state()
|
||||||
|
noop = FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0)
|
||||||
|
|
||||||
|
# Step 1: Place chase order with short TTL
|
||||||
|
a1 = FulfilmentAction(ActionKind.PLACE, Side.BUY, OrderType.LIMIT,
|
||||||
|
5, 0.10, 1, post_only=True, metadata={"chase": True})
|
||||||
|
s1 = cwm.transition(s, (a1,))
|
||||||
|
assert len(s1.open_orders) == 1
|
||||||
|
|
||||||
|
# Step 2: Order expired (TTL=1ms)
|
||||||
|
s2 = cwm.transition(s1, (noop,))
|
||||||
|
assert len(s2.open_orders) == 0
|
||||||
|
|
||||||
|
# Step 3: Re-place at offset 3 (simulating cancel-retry)
|
||||||
|
a3 = FulfilmentAction(ActionKind.PLACE, Side.BUY, OrderType.LIMIT,
|
||||||
|
3, 0.10, 1, post_only=True, metadata={"chase": True})
|
||||||
|
s3 = cwm.transition(s2, (a3,))
|
||||||
|
assert len(s3.open_orders) == 1
|
||||||
|
assert s3.open_orders[0].price != s1.open_orders[0].price # different offset
|
||||||
|
|
||||||
|
def test_chase_max_retries_in_metadata(self):
|
||||||
|
a = FulfilmentAction(ActionKind.PLACE, Side.BUY, OrderType.LIMIT,
|
||||||
|
2, 0.10, 100, post_only=True,
|
||||||
|
metadata={"chase": True, "chase_max_retries": 3})
|
||||||
|
assert a.metadata["chase_max_retries"] == 3
|
||||||
|
|
||||||
|
def test_wait_to_retry_in_params(self):
|
||||||
|
p = _params()
|
||||||
|
assert p.wait_to_retry_ms == 0
|
||||||
|
assert p.chase_enabled is False
|
||||||
|
assert p.chase_offset_ticks == 1
|
||||||
|
assert p.chase_max_retries == 0
|
||||||
|
|
||||||
|
def test_cma_codec_includes_chase_params(self):
|
||||||
|
from malkhut.training.cma_trainer import CMAParameterCodec
|
||||||
|
codec = CMAParameterCodec()
|
||||||
|
param_names = [s.name for s in codec.SPECS]
|
||||||
|
assert "wait_to_retry_ms" in param_names
|
||||||
|
assert "chase_offset_ticks" in param_names
|
||||||
|
assert "chase_max_retries" in param_names
|
||||||
|
assert "urgency_taker_threshold" in param_names
|
||||||
|
|||||||
@@ -712,6 +712,19 @@ def _primitive_to_action(primitive: ActionPrimitive, state: MarketWorldState) ->
|
|||||||
ttl_ms=int(primitive.duration_s * 1000), post_only=True,
|
ttl_ms=int(primitive.duration_s * 1000), post_only=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
elif at == ActionType.CHASE:
|
||||||
|
# Chase: place at target with short TTL. CWM auto-cancels when TTL expires.
|
||||||
|
# Next step the planner re-places at new offset (cancel-retry across CWM steps).
|
||||||
|
# Key: uses primitive.duration_s as the wait_to_retry_ms (how long before cancel).
|
||||||
|
ttl = int(primitive.duration_s * 1000) if primitive.duration_s > 0 else 100
|
||||||
|
return FulfilmentAction(
|
||||||
|
kind=ActionKind.PLACE, side=primitive.side, order_type=OrderType.LIMIT,
|
||||||
|
price_ticks_from_best=primitive.offset_ticks, qty_fraction=primitive.size_fraction,
|
||||||
|
ttl_ms=ttl, post_only=True,
|
||||||
|
metadata={"chase": True, "chase_offset": primitive.offset_ticks,
|
||||||
|
"chase_ttl_ms": ttl, "chase_target": "follow_price"},
|
||||||
|
)
|
||||||
|
|
||||||
return FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0)
|
return FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user