Skip to main content

ek_ui/
sim.rs

1//! S5 SimGatewayPlugin —— 与 ek-ctp 四口 ask 契约同构的**纸面网关**。
2//!
3//! 四口(`sim.subscribe` / `sim.send_order` / `sim.cancel_order` /
4//! `sim.query_rate`):前三口直接复用 ek-ctp 的 Req/Reply 类型(同构 =
5//! 同一类型面,接线换 sim 即插真网关);第四口 `query_rate` 为 sim 自定
6//! 流控查询(ek-ctp 0.2.0 无此口;CTP 流控语义:1s 窗内已用/上限/余量)。
7//!
8//! 撮合(纸面):由 `eBar.`(BarsFrame)/`eTick.`(TickData)回放驱动 ——
9//! - 限价单:买(bar.low ≤ 委托价) / 卖(bar.high ≥ 委托价)成交于委托价;
10//!   市价单成交于 bar.close / tick.last;
11//! - **手数排队**:每根 bar 的 volume 是本拍成交预算,挂单按到达序(FIFO)
12//!   吃预算,先挂先成交,吃满即排队到下一拍;
13//! - 成交产出:eOrder.(状态单)+ eTrade.(成交笔)+ ePosition.(方向位行)
14//!   → ek-oms 被动入账 → 监控页 ask 全量查询上屏。
15//!
16//! 纪律:锁内撮合造快照,锁外 put(不持锁发事件,与 DmPlugin 同形)。
17
18use std::collections::BTreeMap;
19use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
20use std::time::{Duration, Instant};
21
22use ek_core::context::PluginContext;
23use ek_core::envelope::{Envelope, ReplyErr, e_req_type};
24use ek_core::plugin::Plugin;
25use ek_core::service::ServiceDesc;
26use ek_ctp::plugin::{
27    CancelOrderReply, CancelOrderReq, SendOrderReply, SendOrderReq, SubscribeReply, SubscribeReq,
28};
29use ek_data::bar::BarData;
30use ek_data::constant::{Direction, Offset, OrderType, Status};
31use ek_data::event::{EVENT_BAR, EVENT_ORDER, EVENT_POSITION, EVENT_TICK, EVENT_TRADE};
32use ek_data::order::OrderData;
33use ek_data::order_request::OrderRequest;
34use ek_data::position::PositionData;
35use ek_data::tick::TickData;
36use ek_data::trade::TradeData;
37use ek_dm::wire::BarsFrame;
38use ek_event::Event;
39
40/// 网关注册名(工厂名与注册名一致,BD-U5)。
41pub const SIM_NAME: &str = "sim";
42/// 流控上限(CTP 缺省口径的纸面镜像)。
43pub const RATE_LIMIT: u32 = 6;
44
45/// `sim.query_rate`(第四口;sim 自定,其余三口与 ek-ctp 同型)。
46#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
47pub struct QueryRateReq;
48
49/// 流控回包:1s 窗内已用 / 上限 / 余量。
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct QueryRateReply {
52    pub used: u32,
53    pub limit: u32,
54    pub remaining: u32,
55}
56
57/// 一次成交的三件套产出(订单状态单 + 成交笔 + 方向位行)。
58#[derive(Debug, Clone)]
59pub struct FillOut {
60    pub order: OrderData,
61    pub trade: TradeData,
62    pub position: PositionData,
63}
64
65/// 1s 流控窗。
66#[derive(Debug)]
67struct RateWindow {
68    used: u32,
69    started: Instant,
70}
71
72impl RateWindow {
73    fn new() -> Self {
74        Self {
75            used: 0,
76            started: Instant::now(),
77        }
78    }
79    fn roll(&mut self) {
80        if self.started.elapsed() >= Duration::from_secs(1) {
81            self.used = 0;
82            self.started = Instant::now();
83        }
84    }
85    fn hit(&mut self) -> bool {
86        self.roll();
87        if self.used >= RATE_LIMIT {
88            // 超限拒绝(T7 注错面:CTP 流控语义的纸面镜像,超限请求显形被拒)。
89            return false;
90        }
91        self.used += 1;
92        true
93    }
94    fn snapshot(&mut self) -> QueryRateReply {
95        self.roll();
96        QueryRateReply {
97            used: self.used,
98            limit: RATE_LIMIT,
99            remaining: RATE_LIMIT.saturating_sub(self.used),
100        }
101    }
102}
103
104/// 纸面撮合器(纯逻辑零总线;金样本直断此层)。
105#[derive(Debug, Default)]
106pub struct SimMatcher {
107    /// 在挂订单(到达序 = FIFO;终态即摘除)。
108    pub resting: Vec<OrderData>,
109    /// 已成交笔号序列。
110    pub trade_seq: u64,
111    /// 委托引用计数(订单号尾段)。
112    pub order_ref: u64,
113    /// 方向位行(key = symbol × 方向原文;Direction 无 Ord → 键用 as_str)。
114    pub positions: BTreeMap<(String, String), PositionData>,
115    /// 上次 bar 收口时刻(key = symbol;严格递增守卫 —— 重复/重放帧不重复撮合)。
116    pub last_bar_dt: BTreeMap<String, ek_data::time::DtNs>,
117    /// 本段已发生的 tick 成交量(key = symbol;bar 收口时从 bar 预算扣回 ——
118    /// 防 tick 与 bar 对同一笔市场量双重计价)。
119    pub tick_filled: BTreeMap<String, f64>,
120}
121
122impl SimMatcher {
123    /// 下单校验 + 入挂(拒单口径与 ek-ctp 对齐:开平 NET / 类型不支持 / 量非正)。
124    pub fn place(&mut self, req: &OrderRequest) -> Result<OrderData, String> {
125        if req.offset == Offset::NONE {
126            return Err("请选择开平方向".to_string());
127        }
128        if !matches!(req.order_type, OrderType::LIMIT | OrderType::MARKET) {
129            return Err(format!(
130                "当前接口不支持该类型的委托{}",
131                req.order_type.as_str()
132            ));
133        }
134        if req.volume <= 0.0 {
135            return Err("委托量必须为正".to_string());
136        }
137        self.order_ref += 1;
138        let orderid = format!("SIM_0_{}", self.order_ref);
139        let od = req.create_order_data(&orderid, SIM_NAME);
140        self.resting.push(od.clone());
141        Ok(od)
142    }
143
144    /// 撤单(接受本地号或 `sim.` 全号;命中即 CANCELLED 摘挂)。
145    pub fn cancel(&mut self, orderid: &str) -> Option<OrderData> {
146        let local = orderid.strip_prefix("sim.").unwrap_or(orderid);
147        let idx = self.resting.iter().position(|o| o.orderid == local)?;
148        let mut od = self.resting.remove(idx);
149        od.status = Status::CANCELLED;
150        Some(od)
151    }
152
153    /// eBar. 驱动:逐 bar 撮合(预算 = bar.volume − 本段 tick 已成;FIFO)。
154    ///
155    /// 单调守卫:bar.datetime 必须严格大于该 symbol 上次收口时刻(重复帧/
156    /// 重放帧直接忽略,不重复撮合);预算扣回本段 tick 成交量(tick 与 bar
157    /// 不双重计价同一笔市场量)。
158    pub fn on_bar(&mut self, bar: &BarData) -> Vec<FillOut> {
159        if self
160            .last_bar_dt
161            .get(&bar.symbol)
162            .is_some_and(|&last| bar.datetime <= last)
163        {
164            return Vec::new();
165        }
166        self.last_bar_dt.insert(bar.symbol.clone(), bar.datetime);
167        let pending = self.tick_filled.remove(&bar.symbol).unwrap_or(0.0);
168        let mut budget = (bar.volume - pending).max(0.0);
169        let mut outs = Vec::new();
170        let mut i = 0;
171        while i < self.resting.len() && budget > 0.0 {
172            let (fillable, price) = {
173                let o = &self.resting[i];
174                if o.symbol != bar.symbol {
175                    (false, 0.0)
176                } else {
177                    match o.direction {
178                        Some(Direction::LONG) => match o.order_type {
179                            OrderType::MARKET => (true, bar.close_price),
180                            _ => (bar.low_price <= o.price, o.price),
181                        },
182                        Some(Direction::SHORT) => match o.order_type {
183                            OrderType::MARKET => (true, bar.close_price),
184                            _ => (bar.high_price >= o.price, o.price),
185                        },
186                        _ => (false, 0.0),
187                    }
188                }
189            };
190            if fillable {
191                if let Some(out) = self.fill_at(i, price, &mut budget, bar.datetime) {
192                    outs.push(out);
193                }
194            }
195            // 全成交即摘(索引不前移);其余前移。
196            if self
197                .resting
198                .get(i)
199                .is_some_and(|o| o.status == Status::ALLTRADED)
200            {
201                self.resting.remove(i);
202            } else {
203                i += 1;
204            }
205        }
206        outs
207    }
208
209    /// eTick. 驱动:单 tick 撮合(预算 = last_volume;FIFO)。
210    /// tick 成交量记入本段账(bar 收口时扣回,见 on_bar)。
211    pub fn on_tick(&mut self, t: &TickData) -> Vec<FillOut> {
212        let mut budget = t.last_volume.max(0.0);
213        let mut outs = Vec::new();
214        let mut i = 0;
215        while i < self.resting.len() && budget > 0.0 {
216            let (fillable, price) = {
217                let o = &self.resting[i];
218                if o.symbol != t.symbol {
219                    (false, 0.0)
220                } else {
221                    match o.direction {
222                        Some(Direction::LONG) => match o.order_type {
223                            OrderType::MARKET => (true, t.last_price),
224                            _ => (t.last_price <= o.price, o.price),
225                        },
226                        Some(Direction::SHORT) => match o.order_type {
227                            OrderType::MARKET => (true, t.last_price),
228                            _ => (t.last_price >= o.price, o.price),
229                        },
230                        _ => (false, 0.0),
231                    }
232                }
233            };
234            if fillable {
235                if let Some(out) = self.fill_at(i, price, &mut budget, t.datetime) {
236                    outs.push(out);
237                }
238            }
239            if self
240                .resting
241                .get(i)
242                .is_some_and(|o| o.status == Status::ALLTRADED)
243            {
244                self.resting.remove(i);
245            } else {
246                i += 1;
247            }
248        }
249        // tick 来源成交挂账(bar 预算扣回用;bar 来源不挂)。
250        let tick_vol: f64 = outs.iter().map(|o| o.trade.volume).sum();
251        if tick_vol > 0.0 {
252            *self.tick_filled.entry(t.symbol.clone()).or_insert(0.0) += tick_vol;
253        }
254        outs
255    }
256
257    fn fill_at(
258        &mut self,
259        idx: usize,
260        price: f64,
261        budget: &mut f64,
262        dt: ek_data::time::DtNs,
263    ) -> Option<FillOut> {
264        let remaining = self.resting[idx].volume - self.resting[idx].traded;
265        let vol = remaining.min(*budget);
266        if vol <= 0.0 {
267            return None;
268        }
269        *budget -= vol;
270        self.trade_seq += 1;
271        let tradeid = format!("T{}", self.trade_seq);
272        {
273            let o = &mut self.resting[idx];
274            o.traded += vol;
275            o.traded_price = price;
276            o.datetime = Some(dt);
277            o.status = if o.traded >= o.volume {
278                Status::ALLTRADED
279            } else {
280                Status::PARTTRADED
281            };
282        }
283        let order = self.resting[idx].clone();
284        let trade = TradeData {
285            gateway_name: SIM_NAME.to_string(),
286            symbol: order.symbol.clone(),
287            exchange: order.exchange,
288            orderid: order.orderid.clone(),
289            tradeid,
290            direction: order.direction,
291            offset: order.offset,
292            price,
293            volume: vol,
294            datetime: Some(dt),
295            traded_amount: price * vol,
296            ..TradeData::default()
297        };
298        let position = self.bump_position(&trade);
299        Some(FillOut {
300            order,
301            trade,
302            position,
303        })
304    }
305
306    fn bump_position(&mut self, t: &TradeData) -> PositionData {
307        let dir = t.direction.unwrap_or(Direction::NET);
308        let row = self
309            .positions
310            .entry((t.symbol.clone(), dir.as_str().to_string()))
311            .or_insert_with(|| PositionData {
312                gateway_name: SIM_NAME.to_string(),
313                symbol: t.symbol.clone(),
314                exchange: t.exchange,
315                direction: dir,
316                ..PositionData::default()
317            });
318        let before = row.volume;
319        row.volume += t.volume;
320        if row.volume > 0.0 {
321            row.avg_price = (row.avg_price * before + t.price * t.volume) / row.volume;
322        }
323        row.can_use_volume = row.volume;
324        row.last_price = t.price;
325        row.clone()
326    }
327}
328
329/// 纸面网关插件(四口 ask + eBar./eTick. 撮合驱动)。
330pub struct SimGatewayPlugin {
331    inner: Mutex<SimMatcher>,
332    subscribed: Mutex<Vec<String>>,
333    rate: Mutex<RateWindow>,
334    ctx: OnceLock<PluginContext>,
335}
336
337impl SimGatewayPlugin {
338    /// 缺省构造(注册名 "sim")。
339    #[must_use]
340    pub fn new() -> Self {
341        Self {
342            inner: Mutex::new(SimMatcher::default()),
343            subscribed: Mutex::new(Vec::new()),
344            rate: Mutex::new(RateWindow::new()),
345            ctx: OnceLock::new(),
346        }
347    }
348
349    fn lock(&self) -> MutexGuard<'_, SimMatcher> {
350        self.inner.lock().unwrap_or_else(|e| e.into_inner())
351    }
352
353    /// 成交三件套上总线(锁外;纪律同 DmPlugin)。
354    fn emit_fills(&self, outs: &[FillOut]) {
355        let Some(ctx) = self.ctx.get() else { return };
356        for o in outs {
357            let _ = ctx.put(Event::with_data(
358                EVENT_ORDER.to_string(),
359                Arc::new(o.order.clone()),
360            ));
361            let _ = ctx.put(Event::with_data(
362                EVENT_TRADE.to_string(),
363                Arc::new(o.trade.clone()),
364            ));
365            let _ = ctx.put(Event::with_data(
366                EVENT_POSITION.to_string(),
367                Arc::new(o.position.clone()),
368            ));
369        }
370    }
371
372    fn emit_order(&self, od: &OrderData) {
373        let Some(ctx) = self.ctx.get() else { return };
374        let _ = ctx.put(Event::with_data(
375            EVENT_ORDER.to_string(),
376            Arc::new(od.clone()),
377        ));
378    }
379
380    /// eReq.sim:四口 arm 分派(与 ek-dm/ek-ctp 同形)。
381    fn on_request(&self, event: &Event) {
382        let Some(env) = event.data.downcast_ref::<Envelope>() else {
383            return;
384        };
385        macro_rules! arm {
386            ($ty:ty, $res:ty, $body:expr) => {
387                if let Some(req) = env.payload.downcast_ref::<$ty>() {
388                    let res: $res = $body(req);
389                    let _ = env.reply.send(Box::new(Ok::<$res, ReplyErr>(res)));
390                    return;
391                }
392            };
393        }
394        arm!(SubscribeReq, SubscribeReply, |r: &SubscribeReq| {
395            if !self.rate.lock().unwrap().hit() {
396                return SubscribeReply {
397                    ok: false,
398                    msg: format!("流控被拒:1s 窗内请求已达上限 {RATE_LIMIT}"),
399                };
400            }
401            let mut sub = self.subscribed.lock().unwrap();
402            for s in &r.symbols {
403                if !sub.contains(s) {
404                    sub.push(s.clone());
405                }
406            }
407            SubscribeReply {
408                ok: true,
409                msg: format!("已登记 {} 个合约(纸面)", sub.len()),
410            }
411        });
412        arm!(SendOrderReq, SendOrderReply, |r: &SendOrderReq| {
413            if !self.rate.lock().unwrap().hit() {
414                return SendOrderReply {
415                    ok: false,
416                    en_orderid: String::new(),
417                    msg: format!("流控被拒:1s 窗内请求已达上限 {RATE_LIMIT}"),
418                };
419            }
420            // 锁内造单,锁外上总线(纪律同 on_mail 撮合面;match 暂存守卫会跨臂存活,故显式开块)。
421            let placed = { self.lock().place(&r.req) };
422            match placed {
423                Ok(od) => {
424                    self.emit_order(&od);
425                    SendOrderReply {
426                        ok: true,
427                        en_orderid: format!("{SIM_NAME}.{}", od.orderid),
428                        msg: String::new(),
429                    }
430                }
431                Err(msg) => SendOrderReply {
432                    ok: false,
433                    en_orderid: String::new(),
434                    msg,
435                },
436            }
437        });
438        arm!(CancelOrderReq, CancelOrderReply, |r: &CancelOrderReq| {
439            if !self.rate.lock().unwrap().hit() {
440                return CancelOrderReply {
441                    ok: false,
442                    msg: format!("流控被拒:1s 窗内请求已达上限 {RATE_LIMIT}"),
443                };
444            }
445            let cancelled = { self.lock().cancel(&r.req.orderid) };
446            match cancelled {
447                Some(od) => {
448                    self.emit_order(&od);
449                    CancelOrderReply {
450                        ok: true,
451                        msg: format!("已撤 {}", od.orderid),
452                    }
453                }
454                None => CancelOrderReply {
455                    ok: false,
456                    msg: "委托不存在/已终态".to_string(),
457                },
458            }
459        });
460        arm!(QueryRateReq, QueryRateReply, |_r: &QueryRateReq| {
461            self.rate.lock().unwrap().snapshot()
462        });
463    }
464}
465
466impl Default for SimGatewayPlugin {
467    fn default() -> Self {
468        Self::new()
469    }
470}
471
472impl Plugin for SimGatewayPlugin {
473    fn name(&self) -> &str {
474        SIM_NAME
475    }
476
477    fn deps(&self) -> &[&str] {
478        &[] // 纯总线契约;dm/oms 先后皆可(与 UiPlugin 同理)
479    }
480
481    fn routes(&self) -> Vec<std::borrow::Cow<'static, str>> {
482        vec![
483            std::borrow::Cow::Borrowed(EVENT_BAR),
484            std::borrow::Cow::Borrowed(EVENT_TICK),
485            std::borrow::Cow::Owned(e_req_type(SIM_NAME)),
486        ]
487    }
488
489    fn mailbox_cap(&self) -> usize {
490        2048
491    }
492
493    fn version(&self) -> std::borrow::Cow<'static, str> {
494        std::borrow::Cow::Borrowed(env!("CARGO_PKG_VERSION"))
495    }
496
497    fn services(&self) -> Vec<ServiceDesc> {
498        vec![
499            ServiceDesc::describe::<SubscribeReq, SubscribeReply>(
500                format!("{SIM_NAME}.subscribe"),
501                "登记订阅(纸面:仅登记不外发)",
502            ),
503            ServiceDesc::describe::<SendOrderReq, SendOrderReply>(
504                format!("{SIM_NAME}.send_order"),
505                "下单(SUBMITTING 单上总线;撮合由 eBar./eTick. 驱动)",
506            ),
507            ServiceDesc::describe::<CancelOrderReq, CancelOrderReply>(
508                format!("{SIM_NAME}.cancel_order"),
509                "撤单(CANCELLED 单上总线)",
510            ),
511            ServiceDesc::describe::<QueryRateReq, QueryRateReply>(
512                format!("{SIM_NAME}.query_rate"),
513                "流控查询(1s 窗内已用/上限/余量)",
514            ),
515        ]
516    }
517
518    fn on_init(&self, ctx: &PluginContext) {
519        let _ = self.ctx.set(ctx.clone());
520    }
521
522    fn on_mail(&self, event: &Event) {
523        let ty = event.event_type.as_str();
524        if ty == EVENT_BAR {
525            if let Some(f) = event.data.downcast_ref::<BarsFrame>() {
526                let outs: Vec<FillOut> = {
527                    let mut m = self.lock();
528                    f.bars.iter().flat_map(|b| m.on_bar(b)).collect()
529                };
530                self.emit_fills(&outs);
531            }
532        } else if ty == EVENT_TICK {
533            if let Some(t) = event.data.downcast_ref::<TickData>() {
534                let outs = {
535                    let mut m = self.lock();
536                    m.on_tick(t)
537                };
538                self.emit_fills(&outs);
539            }
540        } else if ty == e_req_type(SIM_NAME) {
541            self.on_request(event);
542        }
543    }
544}