//! Host-side order representation for the LOB backtest harness. //! See docs/superpowers/specs/2026-05-18-real-lob-integration-design.md §3. //! //! - `OrderIntent` is the ergonomic enum used by policy YAML config + the //! trade-log audit reconstruction. //! - `OrderEvent` is the flat 24-byte device-side record written by the //! decision-policy kernel into the per-block mapped-pinned audit ring. //! - `TradeRecord` is the flat 40-byte device-side record written by the //! pnl_track kernel into the per-block mapped-pinned trade-log buffer //! when a position returns to flat. //! - `SlotTag` packs `{kind, index, gen_counter}` into a u32 so the //! device-side kernel can emit it as a single field; the gen_counter //! distinguishes a new slot occupant from stale handles after reuse. use serde::{Deserialize, Serialize}; #[repr(u8)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum Side { Buy = 0, Sell = 1, } #[repr(u8)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum LimitKind { Limit = 0, Ioc = 1, Fok = 2, } #[repr(u8)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum SlotKind { Limit = 0, Stop = 1, } /// Stable handle for cancel/modify. Packed into u32 (host) so the /// device-side decision kernel can emit it as a single field. /// /// Layout (LSB → MSB): /// bits 0.. 7 : slot_kind (SlotKind as u8) /// bits 8.. 15 : slot_index (u8) /// bits 16.. 31 : gen_counter (u16) — incremented on every slot reuse #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct SlotTag(pub u32); impl SlotTag { pub fn pack(kind: SlotKind, idx: u8, generation: u16) -> Self { SlotTag((kind as u32) | ((idx as u32) << 8) | ((generation as u32) << 16)) } pub fn kind(self) -> SlotKind { match (self.0 & 0xFF) as u8 { 0 => SlotKind::Limit, 1 => SlotKind::Stop, other => panic!("SlotTag.kind() out of range: {other}"), } } pub fn index(self) -> u8 { ((self.0 >> 8) & 0xFF) as u8 } pub fn generation(self) -> u16 { ((self.0 >> 16) & 0xFFFF) as u16 } } /// Limit-order parameters; reused as both legs of an OCO submission. #[derive(Clone, Copy, Debug, Serialize, Deserialize)] pub struct LimitParams { pub side: Side, pub size: u16, pub px: i32, // price in ticks pub kind: LimitKind, } /// Host-side ergonomic enum. Used for configuration sources (policy YAML) /// AND for the trade-log audit representation reconstructed from the /// on-device `OrderEvent` records. /// /// OCO is two limit legs (not recursive); on-device they are stored as /// two slots linked by `oco_pair`. See spec §3. #[derive(Clone, Debug, Serialize, Deserialize)] pub enum OrderIntent { SubmitMarket { side: Side, size: u16, }, SubmitLimit { side: Side, size: u16, px: i32, kind: LimitKind, }, SubmitStop { side: Side, size: u16, trigger_px: i32, limit_px: Option, }, SubmitOcoLimits { leg_a: LimitParams, leg_b: LimitParams, }, Cancel { slot_tag: SlotTag, }, Modify { slot_tag: SlotTag, new_size: Option, new_px: Option, }, } /// Flat 24-byte record emitted by the device-side decision kernel into /// the per-block mapped-pinned audit ring. Host reads + reconstructs /// `OrderIntent` for the trade log. #[repr(C)] #[derive( Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable, Serialize, Deserialize, )] pub struct OrderEvent { pub ts_ns: u64, // 8 pub slot_tag: u32, // 4 — SlotTag.0 pub kind: u8, // 1 — OrderEventKind discriminant pub side: u8, // 1 pub size: u16, // 2 pub px: i32, // 4 (ticks; 0 for market) pub trigger_px: i32, // 4 (ticks; 0 if not stop) // total: 24 bytes } #[repr(u8)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum OrderEventKind { SubmitMarket = 0, SubmitLimit = 1, SubmitIoc = 2, SubmitFok = 3, SubmitStopMarket = 4, SubmitStopLimit = 5, Cancel = 6, Modify = 7, } /// Closed-trade record emitted when a position returns to flat (by /// counter-fill or stop). Written by the pnl_track kernel into the /// per-block trade-log mapped-pinned buffer. Pinned at 40 bytes. /// /// USD amounts are fixed-point ×100 (e.g. $1.23 → 123) to keep the /// device-side write purely integer-aligned. #[repr(C)] #[derive( Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable, Serialize, Deserialize, )] pub struct TradeRecord { pub entry_ts_ns: u64, // 8 pub exit_ts_ns: u64, // 8 pub entry_px_ticks: i32, // 4 pub exit_px_ticks: i32, // 4 pub size_lots: i32, // 4 signed (+long, −short) pub fees_usd_fp: i32, // 4 pub realised_pnl_usd_fp: i32, // 4 pub horizon_idx: u8, // 1 pub strategy_id: u8, // 1 pub _pad: [u8; 2], // total: 40 bytes } #[cfg(test)] mod tests { use super::*; #[test] fn slot_tag_roundtrips() { let t = SlotTag::pack(SlotKind::Stop, 7, 1234); assert_eq!(t.kind(), SlotKind::Stop); assert_eq!(t.index(), 7); assert_eq!(t.generation(), 1234); } #[test] fn slot_tag_max_gen_fits() { let t = SlotTag::pack(SlotKind::Limit, 31, u16::MAX); assert_eq!(t.generation(), u16::MAX); assert_eq!(t.index(), 31); assert_eq!(t.kind(), SlotKind::Limit); } #[test] fn slot_tag_packs_distinct_kinds() { let a = SlotTag::pack(SlotKind::Limit, 0, 0); let b = SlotTag::pack(SlotKind::Stop, 0, 0); assert_ne!(a.0, b.0); } #[test] fn order_event_is_24_bytes_pod() { assert_eq!(std::mem::size_of::(), 24); // Pod requires the type be Copy + 'static + no-padding; bytemuck // already enforces this at derive time. This test pins the size // so a future field-add forces the kernel-side struct to change too. } #[test] fn trade_record_is_40_bytes_pod() { assert_eq!(std::mem::size_of::(), 40); } #[test] fn intent_serde_roundtrip_for_oco() { let oco = OrderIntent::SubmitOcoLimits { leg_a: LimitParams { side: Side::Buy, size: 1, px: 550_000, kind: LimitKind::Limit, }, leg_b: LimitParams { side: Side::Sell, size: 1, px: 551_000, kind: LimitKind::Limit, }, }; let j = serde_json::to_string(&oco).expect("ser"); let back: OrderIntent = serde_json::from_str(&j).expect("de"); match back { OrderIntent::SubmitOcoLimits { leg_a, leg_b } => { assert_eq!(leg_a.px, 550_000); assert_eq!(leg_b.px, 551_000); } _ => panic!("wrong variant after roundtrip"), } } #[test] fn intent_serde_roundtrip_for_cancel() { let tag = SlotTag::pack(SlotKind::Stop, 5, 42); let intent = OrderIntent::Cancel { slot_tag: tag }; let j = serde_json::to_string(&intent).expect("ser"); let back: OrderIntent = serde_json::from_str(&j).expect("de"); match back { OrderIntent::Cancel { slot_tag } => { assert_eq!(slot_tag.kind(), SlotKind::Stop); assert_eq!(slot_tag.index(), 5); assert_eq!(slot_tag.generation(), 42); } _ => panic!("wrong variant"), } } }