ek-ui 0.2.12

ek 生态常驻主界面(egui/eframe):插件即界面——观测页 + 日志页,车头与 UI 同插件
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
//! `UiPlugin` —— 总线面:`eLog` 入环形日志、`ePlugUp/Down` 记事件 + rev、
//! `eBar`./`eBarUpdate.` 记行情库(K线窗数据面)、`eTick.` 记 tick 库(v4 行情带
//! 唯一源,S7 上屏地面)、
//! `eTimer`(1s 宿主自带)拉一次 `svctl.list` + 逐插件服务目录组快照 +
//! ask oms 全量查询组交易监控帧(S5),
//! 并辞行窗口面的操作指令队列(svctl 起停 + dm.reload 回灌 + oms 五段管线
//! 下单/守卫撤单)。
//!
//! 纪律:UI 不直调任何插件;唯一管制面是 ask 契约(svctl / dm.reload /
//! oms 查询与 submit_order·cancel_order / ctp 配置订阅,全部与
//! headless 同源;`PluginContext` 只在 worker 可用)。0.1.1 BD-UI-30:
//! 下单/撤单直发网关的旁路口删除——风控与守卫不可绕过。0.2.10 BD-UI-38:
//! sim 纸面网关退场(流控查询口随之撤)。

use std::sync::OnceLock;
use std::time::Duration;

use ek_core::context::PluginContext;
use ek_core::plugin::Plugin;
use ek_core::service::{PlugNotice, ServiceDesc};
use ek_core::{EVENT_PLUG_DOWN, EVENT_PLUG_UP};
use ek_ctp::plugin::{
    ConfigureReply, ConfigureReq, SubscribeReply as CtpSubscribeReply,
    SubscribeReq as CtpSubscribeReq,
};
use ek_data::account::AccountData;
use ek_data::event::{EVENT_BAR, EVENT_CONTRACT, EVENT_LOG, EVENT_TICK};
use ek_data::log::LogData;
use ek_data::order::OrderData;
use ek_data::position::PositionData;
use ek_data::trade::TradeData;
use ek_dm::plugin::{EVENT_BAR_UPDATE, ReloadReq};
use ek_event::EVENT_TIMER;
use ek_oms::plugin::{
    GetAllAccountsReq, GetAllOrdersReq, GetAllPositionsReq, GetAllTradesReq, OmsCancelOrderReq,
    OmsCancelReply, SubmitOrderReq,
};
use ek_oms::{SubmitReply, SubmitStatus};
use ek_station::wire::{
    ListReq, PlugRow, RemoveReq, RemoveRes, SVC_LIST, SVC_REMOVE, SVC_START, StartReq, StartRes,
};

use crate::state::{BusSnap, UiShared};

/// 注册名 = 工厂名 = `"ui"`(BD-U5:两者一致)。
pub const UI_NAME: &str = "ui";

/// 常驻观测插件(窗口面在 [`crate::runtime`] 持 [`UiShared`] 渲染)。
pub struct UiPlugin {
    shared: std::sync::Arc<UiShared>,
    ctx: OnceLock<PluginContext>,
}

impl UiPlugin {
    /// 与窗口面共用的共享状态句柄。
    pub fn new(shared: std::sync::Arc<UiShared>) -> Self {
        Self {
            shared,
            ctx: OnceLock::new(),
        }
    }

    /// eTimer 节流回调:先辞行操作指令(有则记录),再组一次快照。
    fn refresh(&self) {
        let Some(ctx) = self.ctx.get() else {
            return; // 未 init 就吃到 eTimer:忽略,等下轮
        };
        self.run_cmds(ctx);
        match ctx.ask_service::<ListReq, Vec<PlugRow>>(SVC_LIST, ListReq, Duration::from_secs(2)) {
            Ok(rows) => {
                let mut services = Vec::with_capacity(rows.len());
                for r in &rows {
                    let ids = ctx
                        .services_of(&r.name)
                        .map(|v| v.iter().map(|s| s.id.clone()).collect::<Vec<_>>())
                        .unwrap_or_default();
                    services.push((r.name.clone(), ids));
                }
                self.shared.set_snapshot(BusSnap { rows, services });
            }
            Err(e) => self.shared.log_line(format!("[ui] svctl.list 失败: {e:?}")),
        }
        // S5 交易监控:ask oms 全量查询组帧(oms 不在/超时 → 空帧,不抛错)。
        let orders = ctx
            .ask_service::<GetAllOrdersReq, Vec<OrderData>>(
                "oms.get_all_orders",
                GetAllOrdersReq,
                Duration::from_secs(2),
            )
            .unwrap_or_default();
        let trades = ctx
            .ask_service::<GetAllTradesReq, Vec<TradeData>>(
                "oms.get_all_trades",
                GetAllTradesReq,
                Duration::from_secs(2),
            )
            .unwrap_or_default();
        let positions = ctx
            .ask_service::<GetAllPositionsReq, Vec<PositionData>>(
                "oms.get_all_positions",
                GetAllPositionsReq,
                Duration::from_secs(2),
            )
            .unwrap_or_default();
        let accounts = ctx
            .ask_service::<GetAllAccountsReq, Vec<AccountData>>(
                "oms.get_all_accounts",
                GetAllAccountsReq,
                Duration::from_secs(2),
            )
            .unwrap_or_default();
        self.shared.set_trade_snap(crate::state::TradeSnap {
            orders,
            trades,
            positions,
            accounts,
        });
    }

    /// 辞行指令队列(S2 svctl 操作面;回执进日志,面板语义可断言)。
    fn run_cmds(&self, ctx: &PluginContext) {
        for cmd in self.shared.drain_cmds() {
            match cmd {
                crate::state::UiCmd::Start { factory } => {
                    match ctx.ask_service::<StartReq, StartRes>(
                        SVC_START,
                        StartReq {
                            factory: factory.clone(),
                        },
                        Duration::from_secs(3),
                    ) {
                        Ok(StartRes::Started { name }) => {
                            self.shared
                                .log_line(format!("[ops] svctl.start {factory} → {name} 上线"));
                        }
                        Ok(StartRes::Rejected { reason }) => {
                            self.shared
                                .log_line(format!("[ops] start {factory} 被拒: {reason}"));
                        }
                        Err(e) => {
                            self.shared
                                .log_line(format!("[ops] start {factory} 失败: {e:?}"));
                        }
                    }
                }
                crate::state::UiCmd::Remove { name } => {
                    match ctx.ask_service::<RemoveReq, RemoveRes>(
                        SVC_REMOVE,
                        RemoveReq { name: name.clone() },
                        Duration::from_secs(3),
                    ) {
                        Ok(RemoveRes::Removed { name }) => {
                            self.shared
                                .log_line(format!("[ops] svctl.remove {name} 完成"));
                        }
                        Ok(RemoveRes::NoSuchName { name }) => {
                            self.shared
                                .log_line(format!("[ops] remove {name} 不存在/不可摘"));
                        }
                        Err(e) => {
                            self.shared
                                .log_line(format!("[ops] remove {name} 失败: {e:?}"));
                        }
                    }
                }
                crate::state::UiCmd::Reload {
                    interval,
                    symbol,
                    bars,
                } => {
                    // 回灌走 dm.reload ask(push=true 逐条广播 → 行情板实时见回执面)。
                    match ctx.ask_service::<ReloadReq, usize>(
                        "dm.reload",
                        ReloadReq {
                            interval,
                            symbol: symbol.clone(),
                            bars: bars.clone(),
                            size: Some(bars.len()),
                            push: true,
                        },
                        Duration::from_secs(5),
                    ) {
                        Ok(n) => {
                            self.shared.market().note_reload();
                            self.shared.log_line(format!(
                                "[ops] dm.reload {symbol}@{interval}s 回灌 {n} 条"
                            ));
                        }
                        Err(e) => {
                            self.shared
                                .log_line(format!("[ops] dm.reload {symbol} 失败: {e:?}"));
                        }
                    }
                }
                crate::state::UiCmd::SendOrder { req, gateway, mode } => {
                    // 0.1.1 BD-UI-30 薄下单者:五段管线全走 oms.submit_order
                    // (拆单/风控/逐腿发送/记账;直发 {gw}.send_order 旁路口删除,
                    // 守卫不可绕过)。parent 聚合逐腿显形进日志。
                    match ctx.ask_service::<SubmitOrderReq, SubmitReply>(
                        "oms.submit_order",
                        SubmitOrderReq {
                            req: req.clone(),
                            gateway_name: gateway.clone(),
                            mode,
                        },
                        Duration::from_secs(10),
                    ) {
                        Ok(reply) => {
                            let tag = match reply.status {
                                SubmitStatus::Sent => "SENT",
                                SubmitStatus::Partial => "PARTIAL",
                                SubmitStatus::Rejected => "REJECTED",
                            };
                            for leg in &reply.legs {
                                match (leg.ok, &leg.en_orderid, &leg.reason) {
                                    (true, Some(id), _) => self.shared.log_line(format!(
                                        "[ops] submit {} {} {}x{} → {id} (parent {})",
                                        leg.req.symbol,
                                        leg.req.direction.as_str(),
                                        leg.req.offset.as_str(),
                                        leg.req.volume,
                                        reply.parent_id
                                    )),
                                    _ => self.shared.log_line(format!(
                                        "[ops] submit {} {} {}x{} 被拒: {} (parent {})",
                                        leg.req.symbol,
                                        leg.req.direction.as_str(),
                                        leg.req.offset.as_str(),
                                        leg.req.volume,
                                        leg.reason.as_deref().unwrap_or("?"),
                                        reply.parent_id
                                    )),
                                }
                            }
                            if reply.legs.is_empty() {
                                self.shared.log_line(format!(
                                    "[ops] submit {} {}x{} {tag}:可平不足零腿 (parent {})",
                                    req.symbol,
                                    req.offset.as_str(),
                                    req.volume,
                                    reply.parent_id
                                ));
                            }
                        }
                        Err(e) => {
                            self.shared
                                .log_line(format!("[ops] oms.submit_order 失败: {e:?}"));
                        }
                    }
                }
                crate::state::UiCmd::CancelOrder { orderid, gateway } => {
                    // 0.1.1 BD-UI-30:守卫撤单(尝试 ≤4000 在 oms;直发
                    // {gw}.cancel_order 旁路口删除)。en_orderid 组式与
                    // OrderData.en_orderid() 同式。
                    match ctx.ask_service::<OmsCancelOrderReq, OmsCancelReply>(
                        "oms.cancel_order",
                        OmsCancelOrderReq {
                            en_orderid: format!("{gateway}.{orderid}"),
                            gateway_name: gateway.clone(),
                        },
                        Duration::from_secs(10),
                    ) {
                        Ok(OmsCancelReply { ok: true, msg }) => {
                            self.shared.log_line(format!("[ops] {msg}"));
                        }
                        Ok(OmsCancelReply { ok: false, msg }) => {
                            self.shared.log_line(format!("[ops] 撤单被拒: {msg}"));
                        }
                        Err(e) => {
                            self.shared
                                .log_line(format!("[ops] oms.cancel_order 失败: {e:?}"));
                        }
                    }
                }
                crate::state::UiCmd::CtpConfigure(form) => {
                    // S6 定盘口径:界面手工输入 → 插件自带配置口(插件不读文件)。
                    match ctx.ask_service::<ConfigureReq, ConfigureReply>(
                        "ctp.configure",
                        form.to_configure_req(),
                        Duration::from_secs(5),
                    ) {
                        // `..`:ConfigureReply 0.6.0 起带 product_classes(实际生效值)。
                        // 这里不回填表单勾选 —— 被拒时回带的是「网关当前仍在跑」的那一份,
                        // 用它覆盖用户刚点的勾选会把他的意图吃掉;日志里网关已写明收录产品类。
                        Ok(ConfigureReply { ok: true, msg, .. }) => {
                            self.shared
                                .log_line(format!("[ops] ctp.configure 受理:{msg}"));
                        }
                        Ok(ConfigureReply { ok: false, msg, .. }) => {
                            self.shared
                                .log_line(format!("[ops] ctp.configure 被拒:{msg}"));
                        }
                        Err(e) => {
                            self.shared
                                .log_line(format!("[ops] ctp.configure 失败: {e:?}"));
                        }
                    }
                }
                crate::state::UiCmd::CtpSubscribe { symbols } => {
                    match ctx.ask_service::<CtpSubscribeReq, CtpSubscribeReply>(
                        "ctp.subscribe",
                        CtpSubscribeReq {
                            symbols: symbols.clone(),
                        },
                        Duration::from_secs(5),
                    ) {
                        Ok(CtpSubscribeReply { ok: true, msg }) => {
                            self.shared.log_line(format!(
                                "[ops] ctp.subscribe {} 受理:{}",
                                symbols.join(","),
                                msg
                            ));
                        }
                        Ok(CtpSubscribeReply { ok: false, msg }) => {
                            self.shared.log_line(format!(
                                "[ops] ctp.subscribe {} 被拒:{}",
                                symbols.join(","),
                                msg
                            ));
                        }
                        Err(e) => {
                            self.shared
                                .log_line(format!("[ops] ctp.subscribe 失败: {e:?}"));
                        }
                    }
                }
            }
        }
    }
}

impl Plugin for UiPlugin {
    fn name(&self) -> &str {
        UI_NAME
    }

    fn deps(&self) -> &[&str] {
        &[] // 纯观测;谁先上都能看到(bus 契约不依赖启动序)
    }

    fn routes(&self) -> Vec<std::borrow::Cow<'static, str>> {
        vec![
            std::borrow::Cow::Borrowed(EVENT_LOG),
            std::borrow::Cow::Borrowed(EVENT_PLUG_UP),
            std::borrow::Cow::Borrowed(EVENT_PLUG_DOWN),
            std::borrow::Cow::Borrowed(EVENT_BAR),
            std::borrow::Cow::Borrowed(EVENT_BAR_UPDATE),
            std::borrow::Cow::Borrowed(EVENT_TICK),
            std::borrow::Cow::Borrowed(EVENT_CONTRACT),
            std::borrow::Cow::Borrowed("eContractFinish"),
            std::borrow::Cow::Borrowed(EVENT_TIMER),
        ]
    }

    fn mailbox_cap(&self) -> usize {
        1024
    }

    fn version(&self) -> std::borrow::Cow<'static, str> {
        std::borrow::Cow::Borrowed(env!("CARGO_PKG_VERSION"))
    }

    fn services(&self) -> Vec<ServiceDesc> {
        Vec::new() // 纯视图零服务;管制面在 svctl
    }

    fn on_init(&self, ctx: &PluginContext) {
        let _ = self.ctx.set(ctx.clone());
    }

    fn on_start(&self) {
        self.shared.log_line("[ui] 界面插件上线");
    }

    fn on_stop(&self) {
        self.shared.log_line("[ui] 界面插件下线");
    }

    fn on_mail(&self, event: &ek_event::Event) {
        let ty = event.event_type.as_str();
        if ty == EVENT_LOG {
            if let Some(d) = event.data.downcast_ref::<LogData>() {
                let tag = if d.is_warning_or_above() {
                    "WARN"
                } else {
                    "INFO"
                };
                self.shared
                    .log_line(format!("[{tag}][{}] {}", d.gateway_name, d.msg));
            } else {
                self.shared.log_line("[eLog] 载荷类型异常(非 LogData)");
            }
            return;
        }
        if ty == EVENT_PLUG_UP || ty == EVENT_PLUG_DOWN {
            if let Some(n) = event.data.downcast_ref::<PlugNotice>() {
                let verb = if ty == EVENT_PLUG_UP {
                    "上线"
                } else {
                    "下线"
                };
                self.shared
                    .log_line(format!("[host] 插件{verb} {} v{}", n.name, n.version));
            }
            self.shared.bump();
            return;
        }
        if ty == EVENT_BAR {
            if let Some(f) = event.data.downcast_ref::<ek_dm::wire::BarsFrame>() {
                self.shared.market().on_frame(f);
            }
            self.shared.bump();
            return;
        }
        if ty == EVENT_BAR_UPDATE {
            if let Some(b) = event.data.downcast_ref::<ek_data::bar::BarData>() {
                self.shared.market().on_update(b);
            }
            self.shared.bump();
            return;
        }
        if ty == EVENT_TICK {
            if let Some(t) = event.data.downcast_ref::<ek_data::tick::TickData>() {
                self.shared.update_tick(t);
            }
            return;
        }
        if ty == EVENT_CONTRACT {
            if let Some(c) = event.data.downcast_ref::<ek_data::contract::ContractData>() {
                self.shared.update_contract(c);
            }
            return;
        }
        if ty == "eContractFinish" {
            let gw = event
                .data
                .downcast_ref::<String>()
                .cloned()
                .unwrap_or_else(|| "?".to_string());
            self.shared
                .log_line(format!("[ctp] 合约流完成 {gw}(查询合约窗可勾选订阅)"));
            return;
        }
        if ty == EVENT_TIMER {
            self.refresh();
        }
    }
}