Skip to main content

ek_ui/
plugin.rs

1//! `UiPlugin` —— 总线面:`eLog` 入环形日志、`ePlugUp/Down` 记事件 + rev、
2//! `eBar`./`eBarUpdate.` 记行情库(K线窗数据面)、`eTick.` 记 tick 库(v4 行情带
3//! 唯一源,S7 上屏地面)、
4//! `eTimer`(1s 宿主自带)拉一次 `svctl.list` + 逐插件服务目录组快照 +
5//! ask oms 全量查询组交易监控帧(S5),
6//! 并辞行窗口面的操作指令队列(svctl 起停 + dm.reload 回灌 + oms 五段管线
7//! 下单/守卫撤单/流控查询)。
8//!
9//! 纪律:UI 不直调任何插件;唯一管制面是 ask 契约(svctl / dm.reload /
10//! oms 查询与 submit_order·cancel_order / sim 流控 / ctp 配置订阅,全部与
11//! headless 同源;`PluginContext` 只在 worker 可用)。0.1.1 BD-UI-30:
12//! 下单/撤单直发网关的旁路口删除——风控与守卫不可绕过。
13
14use std::sync::OnceLock;
15use std::time::Duration;
16
17use ek_core::context::PluginContext;
18use ek_core::plugin::Plugin;
19use ek_core::service::{PlugNotice, ServiceDesc};
20use ek_core::{EVENT_PLUG_DOWN, EVENT_PLUG_UP};
21use ek_ctp::plugin::{
22    ConfigureReply, ConfigureReq, SubscribeReply as CtpSubscribeReply,
23    SubscribeReq as CtpSubscribeReq,
24};
25use ek_data::account::AccountData;
26use ek_data::event::{EVENT_BAR, EVENT_CONTRACT, EVENT_LOG, EVENT_TICK};
27use ek_data::log::LogData;
28use ek_data::order::OrderData;
29use ek_data::position::PositionData;
30use ek_data::trade::TradeData;
31use ek_dm::plugin::{EVENT_BAR_UPDATE, ReloadReq};
32use ek_event::EVENT_TIMER;
33use ek_oms::plugin::{
34    GetAllAccountsReq, GetAllOrdersReq, GetAllPositionsReq, GetAllTradesReq, OmsCancelOrderReq,
35    OmsCancelReply, SubmitOrderReq,
36};
37use ek_oms::{SubmitReply, SubmitStatus};
38use ek_station::wire::{
39    ListReq, PlugRow, RemoveReq, RemoveRes, SVC_LIST, SVC_REMOVE, SVC_START, StartReq, StartRes,
40};
41
42use crate::state::{BusSnap, UiShared};
43
44/// 注册名 = 工厂名 = `"ui"`(BD-U5:两者一致)。
45pub const UI_NAME: &str = "ui";
46
47/// 常驻观测插件(窗口面在 [`crate::runtime`] 持 [`UiShared`] 渲染)。
48pub struct UiPlugin {
49    shared: std::sync::Arc<UiShared>,
50    ctx: OnceLock<PluginContext>,
51}
52
53impl UiPlugin {
54    /// 与窗口面共用的共享状态句柄。
55    pub fn new(shared: std::sync::Arc<UiShared>) -> Self {
56        Self {
57            shared,
58            ctx: OnceLock::new(),
59        }
60    }
61
62    /// eTimer 节流回调:先辞行操作指令(有则记录),再组一次快照。
63    fn refresh(&self) {
64        let Some(ctx) = self.ctx.get() else {
65            return; // 未 init 就吃到 eTimer:忽略,等下轮
66        };
67        self.run_cmds(ctx);
68        match ctx.ask_service::<ListReq, Vec<PlugRow>>(SVC_LIST, ListReq, Duration::from_secs(2)) {
69            Ok(rows) => {
70                let mut services = Vec::with_capacity(rows.len());
71                for r in &rows {
72                    let ids = ctx
73                        .services_of(&r.name)
74                        .map(|v| v.iter().map(|s| s.id.clone()).collect::<Vec<_>>())
75                        .unwrap_or_default();
76                    services.push((r.name.clone(), ids));
77                }
78                self.shared.set_snapshot(BusSnap { rows, services });
79            }
80            Err(e) => self.shared.log_line(format!("[ui] svctl.list 失败: {e:?}")),
81        }
82        // S5 交易监控:ask oms 全量查询组帧(oms 不在/超时 → 空帧,不抛错)。
83        let orders = ctx
84            .ask_service::<GetAllOrdersReq, Vec<OrderData>>(
85                "oms.get_all_orders",
86                GetAllOrdersReq,
87                Duration::from_secs(2),
88            )
89            .unwrap_or_default();
90        let trades = ctx
91            .ask_service::<GetAllTradesReq, Vec<TradeData>>(
92                "oms.get_all_trades",
93                GetAllTradesReq,
94                Duration::from_secs(2),
95            )
96            .unwrap_or_default();
97        let positions = ctx
98            .ask_service::<GetAllPositionsReq, Vec<PositionData>>(
99                "oms.get_all_positions",
100                GetAllPositionsReq,
101                Duration::from_secs(2),
102            )
103            .unwrap_or_default();
104        let accounts = ctx
105            .ask_service::<GetAllAccountsReq, Vec<AccountData>>(
106                "oms.get_all_accounts",
107                GetAllAccountsReq,
108                Duration::from_secs(2),
109            )
110            .unwrap_or_default();
111        self.shared.set_trade_snap(crate::state::TradeSnap {
112            orders,
113            trades,
114            positions,
115            accounts,
116        });
117    }
118
119    /// 辞行指令队列(S2 svctl 操作面;回执进日志,面板语义可断言)。
120    fn run_cmds(&self, ctx: &PluginContext) {
121        for cmd in self.shared.drain_cmds() {
122            match cmd {
123                crate::state::UiCmd::Start { factory } => {
124                    match ctx.ask_service::<StartReq, StartRes>(
125                        SVC_START,
126                        StartReq {
127                            factory: factory.clone(),
128                        },
129                        Duration::from_secs(3),
130                    ) {
131                        Ok(StartRes::Started { name }) => {
132                            self.shared
133                                .log_line(format!("[ops] svctl.start {factory} → {name} 上线"));
134                        }
135                        Ok(StartRes::Rejected { reason }) => {
136                            self.shared
137                                .log_line(format!("[ops] start {factory} 被拒: {reason}"));
138                        }
139                        Err(e) => {
140                            self.shared
141                                .log_line(format!("[ops] start {factory} 失败: {e:?}"));
142                        }
143                    }
144                }
145                crate::state::UiCmd::Remove { name } => {
146                    match ctx.ask_service::<RemoveReq, RemoveRes>(
147                        SVC_REMOVE,
148                        RemoveReq { name: name.clone() },
149                        Duration::from_secs(3),
150                    ) {
151                        Ok(RemoveRes::Removed { name }) => {
152                            self.shared
153                                .log_line(format!("[ops] svctl.remove {name} 完成"));
154                        }
155                        Ok(RemoveRes::NoSuchName { name }) => {
156                            self.shared
157                                .log_line(format!("[ops] remove {name} 不存在/不可摘"));
158                        }
159                        Err(e) => {
160                            self.shared
161                                .log_line(format!("[ops] remove {name} 失败: {e:?}"));
162                        }
163                    }
164                }
165                crate::state::UiCmd::Reload {
166                    interval,
167                    symbol,
168                    bars,
169                } => {
170                    // 回灌走 dm.reload ask(push=true 逐条广播 → 行情板实时见回执面)。
171                    match ctx.ask_service::<ReloadReq, usize>(
172                        "dm.reload",
173                        ReloadReq {
174                            interval,
175                            symbol: symbol.clone(),
176                            bars: bars.clone(),
177                            size: Some(bars.len()),
178                            push: true,
179                        },
180                        Duration::from_secs(5),
181                    ) {
182                        Ok(n) => {
183                            self.shared.market().note_reload();
184                            self.shared.log_line(format!(
185                                "[ops] dm.reload {symbol}@{interval}s 回灌 {n} 条"
186                            ));
187                        }
188                        Err(e) => {
189                            self.shared
190                                .log_line(format!("[ops] dm.reload {symbol} 失败: {e:?}"));
191                        }
192                    }
193                }
194                crate::state::UiCmd::SendOrder { req, gateway, mode } => {
195                    // 0.1.1 BD-UI-30 薄下单者:五段管线全走 oms.submit_order
196                    // (拆单/风控/逐腿发送/记账;直发 {gw}.send_order 旁路口删除,
197                    // 守卫不可绕过)。parent 聚合逐腿显形进日志。
198                    match ctx.ask_service::<SubmitOrderReq, SubmitReply>(
199                        "oms.submit_order",
200                        SubmitOrderReq {
201                            req: req.clone(),
202                            gateway_name: gateway.clone(),
203                            mode,
204                        },
205                        Duration::from_secs(10),
206                    ) {
207                        Ok(reply) => {
208                            let tag = match reply.status {
209                                SubmitStatus::Sent => "SENT",
210                                SubmitStatus::Partial => "PARTIAL",
211                                SubmitStatus::Rejected => "REJECTED",
212                            };
213                            for leg in &reply.legs {
214                                match (leg.ok, &leg.en_orderid, &leg.reason) {
215                                    (true, Some(id), _) => self.shared.log_line(format!(
216                                        "[ops] submit {} {} {}x{} → {id} (parent {})",
217                                        leg.req.symbol,
218                                        leg.req.direction.as_str(),
219                                        leg.req.offset.as_str(),
220                                        leg.req.volume,
221                                        reply.parent_id
222                                    )),
223                                    _ => self.shared.log_line(format!(
224                                        "[ops] submit {} {} {}x{} 被拒: {} (parent {})",
225                                        leg.req.symbol,
226                                        leg.req.direction.as_str(),
227                                        leg.req.offset.as_str(),
228                                        leg.req.volume,
229                                        leg.reason.as_deref().unwrap_or("?"),
230                                        reply.parent_id
231                                    )),
232                                }
233                            }
234                            if reply.legs.is_empty() {
235                                self.shared.log_line(format!(
236                                    "[ops] submit {} {}x{} {tag}:可平不足零腿 (parent {})",
237                                    req.symbol,
238                                    req.offset.as_str(),
239                                    req.volume,
240                                    reply.parent_id
241                                ));
242                            }
243                        }
244                        Err(e) => {
245                            self.shared
246                                .log_line(format!("[ops] oms.submit_order 失败: {e:?}"));
247                        }
248                    }
249                }
250                crate::state::UiCmd::CancelOrder { orderid, gateway } => {
251                    // 0.1.1 BD-UI-30:守卫撤单(尝试 ≤4000 在 oms;直发
252                    // {gw}.cancel_order 旁路口删除)。en_orderid 组式与
253                    // OrderData.en_orderid() 同式。
254                    match ctx.ask_service::<OmsCancelOrderReq, OmsCancelReply>(
255                        "oms.cancel_order",
256                        OmsCancelOrderReq {
257                            en_orderid: format!("{gateway}.{orderid}"),
258                            gateway_name: gateway.clone(),
259                        },
260                        Duration::from_secs(10),
261                    ) {
262                        Ok(OmsCancelReply { ok: true, msg }) => {
263                            self.shared.log_line(format!("[ops] {msg}"));
264                        }
265                        Ok(OmsCancelReply { ok: false, msg }) => {
266                            self.shared.log_line(format!("[ops] 撤单被拒: {msg}"));
267                        }
268                        Err(e) => {
269                            self.shared
270                                .log_line(format!("[ops] oms.cancel_order 失败: {e:?}"));
271                        }
272                    }
273                }
274                crate::state::UiCmd::QueryRate => {
275                    match ctx.ask_service::<crate::sim::QueryRateReq, crate::sim::QueryRateReply>(
276                        "sim.query_rate",
277                        crate::sim::QueryRateReq,
278                        Duration::from_secs(2),
279                    ) {
280                        Ok(r) => {
281                            self.shared.log_line(format!(
282                                "[ops] sim.query_rate used={}/{} 余{}",
283                                r.used, r.limit, r.remaining
284                            ));
285                        }
286                        Err(e) => {
287                            self.shared
288                                .log_line(format!("[ops] sim.query_rate 失败: {e:?}"));
289                        }
290                    }
291                }
292                crate::state::UiCmd::CtpConfigure(form) => {
293                    // S6 定盘口径:界面手工输入 → 插件自带配置口(插件不读文件)。
294                    match ctx.ask_service::<ConfigureReq, ConfigureReply>(
295                        "ctp.configure",
296                        form.to_configure_req(),
297                        Duration::from_secs(5),
298                    ) {
299                        Ok(ConfigureReply { ok: true, msg }) => {
300                            self.shared
301                                .log_line(format!("[ops] ctp.configure 受理:{msg}"));
302                        }
303                        Ok(ConfigureReply { ok: false, msg }) => {
304                            self.shared
305                                .log_line(format!("[ops] ctp.configure 被拒:{msg}"));
306                        }
307                        Err(e) => {
308                            self.shared
309                                .log_line(format!("[ops] ctp.configure 失败: {e:?}"));
310                        }
311                    }
312                }
313                crate::state::UiCmd::CtpSubscribe { symbols } => {
314                    match ctx.ask_service::<CtpSubscribeReq, CtpSubscribeReply>(
315                        "ctp.subscribe",
316                        CtpSubscribeReq {
317                            symbols: symbols.clone(),
318                        },
319                        Duration::from_secs(5),
320                    ) {
321                        Ok(CtpSubscribeReply { ok: true, msg }) => {
322                            self.shared.log_line(format!(
323                                "[ops] ctp.subscribe {} 受理:{}",
324                                symbols.join(","),
325                                msg
326                            ));
327                        }
328                        Ok(CtpSubscribeReply { ok: false, msg }) => {
329                            self.shared.log_line(format!(
330                                "[ops] ctp.subscribe {} 被拒:{}",
331                                symbols.join(","),
332                                msg
333                            ));
334                        }
335                        Err(e) => {
336                            self.shared
337                                .log_line(format!("[ops] ctp.subscribe 失败: {e:?}"));
338                        }
339                    }
340                }
341            }
342        }
343    }
344}
345
346impl Plugin for UiPlugin {
347    fn name(&self) -> &str {
348        UI_NAME
349    }
350
351    fn deps(&self) -> &[&str] {
352        &[] // 纯观测;谁先上都能看到(bus 契约不依赖启动序)
353    }
354
355    fn routes(&self) -> Vec<std::borrow::Cow<'static, str>> {
356        vec![
357            std::borrow::Cow::Borrowed(EVENT_LOG),
358            std::borrow::Cow::Borrowed(EVENT_PLUG_UP),
359            std::borrow::Cow::Borrowed(EVENT_PLUG_DOWN),
360            std::borrow::Cow::Borrowed(EVENT_BAR),
361            std::borrow::Cow::Borrowed(EVENT_BAR_UPDATE),
362            std::borrow::Cow::Borrowed(EVENT_TICK),
363            std::borrow::Cow::Borrowed(EVENT_CONTRACT),
364            std::borrow::Cow::Borrowed("eContractFinish"),
365            std::borrow::Cow::Borrowed(EVENT_TIMER),
366        ]
367    }
368
369    fn mailbox_cap(&self) -> usize {
370        1024
371    }
372
373    fn version(&self) -> std::borrow::Cow<'static, str> {
374        std::borrow::Cow::Borrowed(env!("CARGO_PKG_VERSION"))
375    }
376
377    fn services(&self) -> Vec<ServiceDesc> {
378        Vec::new() // 纯视图零服务;管制面在 svctl
379    }
380
381    fn on_init(&self, ctx: &PluginContext) {
382        let _ = self.ctx.set(ctx.clone());
383    }
384
385    fn on_start(&self) {
386        self.shared.log_line("[ui] 界面插件上线");
387    }
388
389    fn on_stop(&self) {
390        self.shared.log_line("[ui] 界面插件下线");
391    }
392
393    fn on_mail(&self, event: &ek_event::Event) {
394        let ty = event.event_type.as_str();
395        if ty == EVENT_LOG {
396            if let Some(d) = event.data.downcast_ref::<LogData>() {
397                let tag = if d.is_warning_or_above() {
398                    "WARN"
399                } else {
400                    "INFO"
401                };
402                self.shared
403                    .log_line(format!("[{tag}][{}] {}", d.gateway_name, d.msg));
404            } else {
405                self.shared.log_line("[eLog] 载荷类型异常(非 LogData)");
406            }
407            return;
408        }
409        if ty == EVENT_PLUG_UP || ty == EVENT_PLUG_DOWN {
410            if let Some(n) = event.data.downcast_ref::<PlugNotice>() {
411                let verb = if ty == EVENT_PLUG_UP {
412                    "上线"
413                } else {
414                    "下线"
415                };
416                self.shared
417                    .log_line(format!("[host] 插件{verb} {} v{}", n.name, n.version));
418            }
419            self.shared.bump();
420            return;
421        }
422        if ty == EVENT_BAR {
423            if let Some(f) = event.data.downcast_ref::<ek_dm::wire::BarsFrame>() {
424                self.shared.market().on_frame(f);
425            }
426            self.shared.bump();
427            return;
428        }
429        if ty == EVENT_BAR_UPDATE {
430            if let Some(b) = event.data.downcast_ref::<ek_data::bar::BarData>() {
431                self.shared.market().on_update(b);
432            }
433            self.shared.bump();
434            return;
435        }
436        if ty == EVENT_TICK {
437            if let Some(t) = event.data.downcast_ref::<ek_data::tick::TickData>() {
438                self.shared.update_tick(t);
439            }
440            return;
441        }
442        if ty == EVENT_CONTRACT {
443            if let Some(c) = event.data.downcast_ref::<ek_data::contract::ContractData>() {
444                self.shared.update_contract(c);
445            }
446            return;
447        }
448        if ty == "eContractFinish" {
449            let gw = event
450                .data
451                .downcast_ref::<String>()
452                .cloned()
453                .unwrap_or_else(|| "?".to_string());
454            self.shared
455                .log_line(format!("[ctp] 合约流完成 {gw}(查询合约窗可勾选订阅)"));
456            return;
457        }
458        if ty == EVENT_TIMER {
459            self.refresh();
460        }
461    }
462}