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