1use std::sync::OnceLock;
16use std::time::Duration;
17
18use ek_core::context::PluginContext;
19use ek_core::plugin::Plugin;
20use ek_core::service::{PlugNotice, ServiceDesc};
21use ek_core::{EVENT_PLUG_DOWN, EVENT_PLUG_UP};
22use ek_ctp::plugin::{
23 ConfigureReply, ConfigureReq, DisconnectReply, DisconnectReq, GatewayStatus, StatusReq,
24 SubscribeReply as CtpSubscribeReply, SubscribeReq as CtpSubscribeReq,
25};
26use ek_data::account::AccountData;
27use ek_data::event::{EVENT_BAR, EVENT_CONTRACT, EVENT_LOG, EVENT_TICK};
28use ek_data::log::LogData;
29use ek_data::order::OrderData;
30use ek_data::position::PositionData;
31use ek_data::trade::TradeData;
32use ek_dm::plugin::{EVENT_BAR_UPDATE, ReloadReq};
33use ek_event::EVENT_TIMER;
34use ek_oms::plugin::{
35 GetAllAccountsReq, GetAllOrdersReq, GetAllPositionsReq, GetAllTradesReq, OmsCancelOrderReq,
36 OmsCancelReply, PipelineStatusReq, SubmitOrderReq,
37};
38use ek_oms::{SubmitReply, SubmitStatus};
39use ek_station::wire::{
40 ListReq, PlugRow, RemoveReq, RemoveRes, SVC_LIST, SVC_REMOVE, SVC_START, StartReq, StartRes,
41};
42
43use crate::state::{BusSnap, UiShared};
44
45pub const UI_NAME: &str = "ui";
47
48pub const OMS_ASK_TIMEOUT: Duration = Duration::from_secs(1);
57
58pub fn merge_frame<T>(res: Result<Vec<T>, ek_core::error::AskError>, prev: Vec<T>) -> Vec<T> {
71 match res {
72 Ok(v) => v,
73 Err(ek_core::error::AskError::Timeout { .. }) => prev,
74 Err(_) => Vec::new(),
75 }
76}
77
78pub struct UiPlugin {
80 shared: std::sync::Arc<UiShared>,
81 ctx: OnceLock<PluginContext>,
82}
83
84impl UiPlugin {
85 pub fn new(shared: std::sync::Arc<UiShared>) -> Self {
87 Self {
88 shared,
89 ctx: OnceLock::new(),
90 }
91 }
92
93 fn refresh(&self) {
95 let Some(ctx) = self.ctx.get() else {
96 return; };
98 self.run_cmds(ctx);
99 match ctx.ask_service::<ListReq, Vec<PlugRow>>(SVC_LIST, ListReq, Duration::from_secs(2)) {
100 Ok(rows) => {
101 let mut services = Vec::with_capacity(rows.len());
102 for r in &rows {
103 let ids = ctx
104 .services_of(&r.name)
105 .map(|v| v.iter().map(|s| s.id.clone()).collect::<Vec<_>>())
106 .unwrap_or_default();
107 services.push((r.name.clone(), ids));
108 }
109 let newly = self.shared.sync_accounts_connected(&rows);
111 self.shared.note_plugin_spill(&rows);
114 self.shared.set_snapshot(BusSnap { rows, services });
115 let md = self.shared.md_source();
116 let subs = self.shared.subscriptions();
117 for gw in newly {
118 if !md.is_empty() && gw == md && !subs.is_empty() {
119 self.shared.push_cmd(crate::state::UiCmd::CtpSubscribe {
120 gateway: gw.clone(),
121 symbols: subs.clone(),
122 });
123 self.shared
124 .log_line(format!("[ops] 行情源 {gw} 上线,重放订阅 {} 项", subs.len()));
125 }
126 }
127 }
128 Err(e) => self.shared.log_line(format!("[ui] svctl.list 失败: {e:?}")),
129 }
130 let orders = ctx.ask_service::<GetAllOrdersReq, Vec<OrderData>>(
133 "oms.get_all_orders",
134 GetAllOrdersReq,
135 OMS_ASK_TIMEOUT,
136 );
137 let trades = ctx.ask_service::<GetAllTradesReq, Vec<TradeData>>(
138 "oms.get_all_trades",
139 GetAllTradesReq,
140 OMS_ASK_TIMEOUT,
141 );
142 let positions = ctx.ask_service::<GetAllPositionsReq, Vec<PositionData>>(
143 "oms.get_all_positions",
144 GetAllPositionsReq,
145 OMS_ASK_TIMEOUT,
146 );
147 let accounts = ctx.ask_service::<GetAllAccountsReq, Vec<AccountData>>(
148 "oms.get_all_accounts",
149 GetAllAccountsReq,
150 OMS_ASK_TIMEOUT,
151 );
152 let timed_out =
153 |e: &ek_core::error::AskError| matches!(e, ek_core::error::AskError::Timeout { .. });
154 let stale = orders.as_ref().err().is_some_and(timed_out)
155 || trades.as_ref().err().is_some_and(timed_out)
156 || positions.as_ref().err().is_some_and(timed_out)
157 || accounts.as_ref().err().is_some_and(timed_out);
158 let prev = self.shared.trade_snap();
159 self.shared.set_trade_snap(crate::state::TradeSnap {
160 orders: merge_frame(orders, prev.orders.clone()),
161 trades: merge_frame(trades, prev.trades.clone()),
162 positions: merge_frame(positions, prev.positions.clone()),
163 accounts: merge_frame(accounts, prev.accounts.clone()),
164 });
165 if self.shared.note_trade_frame_stale(stale) {
168 self.shared.log_line(if stale {
169 "[ui] oms 四问有超时:交易监控显示的是上一帧(不是此刻没有委托)".to_string()
170 } else {
171 "[ui] oms 四问恢复:交易监控回到当前帧".to_string()
172 });
173 }
174 if self.shared.status_poll_due() {
181 for a in self.shared.accounts() {
182 if a.stage != crate::state::AccountStage::Assembled || !a.connected {
183 continue;
184 }
185 let svc = format!("{}.status", a.gateway);
186 match ctx.ask_service::<StatusReq, GatewayStatus>(
187 &svc,
188 StatusReq,
189 Duration::from_secs(2),
190 ) {
191 Ok(st) => self.shared.note_gateway_status(&a.gateway, &st),
192 Err(e) => self
193 .shared
194 .note_status_unreachable(&a.gateway, &format!("{e:?}")),
195 }
196 }
197 }
198 if self.shared.pipeline_poll_due() {
206 match ctx.ask_service::<PipelineStatusReq, Vec<String>>(
207 "oms.pipeline_status",
208 PipelineStatusReq,
209 Duration::from_secs(2),
210 ) {
211 Ok(lines) => self.shared.set_pipeline_lines(lines),
212 Err(e) => {
213 if !self.shared.pipeline_lines().is_empty() {
217 self.shared
218 .log_line(format!("[ui] oms.pipeline_status 失手: {e:?}"));
219 }
220 self.shared.set_pipeline_lines(Vec::new());
221 }
222 }
223 }
224 }
225
226 fn run_cmds(&self, ctx: &PluginContext) {
228 for cmd in self.shared.drain_cmds() {
229 match cmd {
230 crate::state::UiCmd::Start { factory, name } => {
231 match ctx.ask_service::<StartReq, StartRes>(
232 SVC_START,
233 StartReq {
234 factory: factory.clone(),
235 name: name.clone(),
236 },
237 Duration::from_secs(3),
238 ) {
239 Ok(StartRes::Started { name }) => {
240 self.shared
241 .log_line(format!("[ops] svctl.start {factory} → {name} 上线"));
242 }
243 Ok(StartRes::Rejected { reason }) => {
244 self.shared
245 .log_line(format!("[ops] start {factory} 被拒: {reason}"));
246 }
247 Err(e) => {
248 self.shared
249 .log_line(format!("[ops] start {factory} 失败: {e:?}"));
250 }
251 }
252 }
253 crate::state::UiCmd::Remove { name } => {
254 if name == UI_NAME {
260 self.shared.log_line(
261 "[ops] 拒绝移除 ui:观测面自身不可自摘(摘掉后没有下一拍执行任何指令)"
262 .to_string(),
263 );
264 continue;
265 }
266 match ctx.ask_service::<RemoveReq, RemoveRes>(
267 SVC_REMOVE,
268 RemoveReq { name: name.clone() },
269 Duration::from_secs(3),
270 ) {
271 Ok(RemoveRes::Removed { name }) => {
272 self.shared
273 .log_line(format!("[ops] svctl.remove {name} 完成"));
274 }
275 Ok(RemoveRes::NoSuchName { name }) => {
276 self.shared
277 .log_line(format!("[ops] remove {name} 不存在/不可摘"));
278 }
279 Ok(RemoveRes::Rejected { name, reason }) => {
283 self.shared
284 .log_line(format!("[ops] remove {name} 被拒: {reason}"));
285 }
286 Err(e) => {
287 self.shared
288 .log_line(format!("[ops] remove {name} 失败: {e:?}"));
289 }
290 }
291 }
292 crate::state::UiCmd::Reload {
293 interval,
294 symbol,
295 bars,
296 } => {
297 match ctx.ask_service::<ReloadReq, usize>(
299 "dm.reload",
300 ReloadReq {
301 interval,
302 symbol: symbol.clone(),
303 bars: bars.clone(),
304 size: Some(bars.len()),
305 push: true,
306 },
307 Duration::from_secs(5),
308 ) {
309 Ok(n) => {
310 self.shared.market().note_reload();
311 self.shared.log_line(format!(
312 "[ops] dm.reload {symbol}@{interval}s 回灌 {n} 条"
313 ));
314 }
315 Err(e) => {
316 self.shared
317 .log_line(format!("[ops] dm.reload {symbol} 失败: {e:?}"));
318 }
319 }
320 }
321 crate::state::UiCmd::SendOrder { req, gateway, mode } => {
322 match ctx.ask_service::<SubmitOrderReq, SubmitReply>(
326 "oms.submit_order",
327 SubmitOrderReq {
328 req: req.clone(),
329 gateway_name: gateway.clone(),
330 mode,
331 },
332 Duration::from_secs(10),
333 ) {
334 Ok(reply) => {
335 let tag = match reply.status {
336 SubmitStatus::Sent => "SENT",
337 SubmitStatus::Partial => "PARTIAL",
338 SubmitStatus::Rejected => "REJECTED",
339 };
340 for leg in &reply.legs {
341 match (leg.ok, &leg.en_orderid, &leg.reason) {
342 (true, Some(id), _) => self.shared.log_line(format!(
343 "[ops] submit {} {} {}x{} → {id} (parent {})",
344 leg.req.symbol,
345 leg.req.direction.as_str(),
346 leg.req.offset.as_str(),
347 leg.req.volume,
348 reply.parent_id
349 )),
350 _ => self.shared.log_line(format!(
351 "[ops] submit {} {} {}x{} 被拒: {} (parent {})",
352 leg.req.symbol,
353 leg.req.direction.as_str(),
354 leg.req.offset.as_str(),
355 leg.req.volume,
356 leg.reason.as_deref().unwrap_or("?"),
357 reply.parent_id
358 )),
359 }
360 }
361 if reply.legs.is_empty() {
362 self.shared.log_line(format!(
363 "[ops] submit {} {}x{} {tag}:可平不足零腿 (parent {})",
364 req.symbol,
365 req.offset.as_str(),
366 req.volume,
367 reply.parent_id
368 ));
369 }
370 }
371 Err(e) => {
372 self.shared
373 .log_line(format!("[ops] oms.submit_order 失败: {e:?}"));
374 }
375 }
376 }
377 crate::state::UiCmd::CancelOrder { orderid, gateway } => {
378 match ctx.ask_service::<OmsCancelOrderReq, OmsCancelReply>(
382 "oms.cancel_order",
383 OmsCancelOrderReq {
384 en_orderid: format!("{gateway}.{orderid}"),
385 gateway_name: gateway.clone(),
386 },
387 Duration::from_secs(10),
388 ) {
389 Ok(OmsCancelReply { ok: true, msg }) => {
390 self.shared.log_line(format!("[ops] {msg}"));
391 }
392 Ok(OmsCancelReply { ok: false, msg }) => {
393 self.shared.log_line(format!("[ops] 撤单被拒: {msg}"));
394 }
395 Err(e) => {
396 self.shared
397 .log_line(format!("[ops] oms.cancel_order 失败: {e:?}"));
398 }
399 }
400 }
401 crate::state::UiCmd::CtpConfigure { gateway, form } => {
402 let svc = format!("{gateway}.configure");
405 match ctx.ask_service::<ConfigureReq, ConfigureReply>(
406 svc.as_str(),
407 form.to_configure_req(),
408 Duration::from_secs(5),
409 ) {
410 Ok(ConfigureReply { ok: true, msg, .. }) => {
414 self.shared.log_line(format!("[ops] {svc} 受理:{msg}"));
415 }
416 Ok(ConfigureReply { ok: false, msg, .. }) => {
417 self.shared.log_line(format!("[ops] {svc} 被拒:{msg}"));
418 }
419 Err(e) => {
420 self.shared.log_line(format!("[ops] {svc} 失败: {e:?}"));
421 }
422 }
423 }
424 crate::state::UiCmd::Connect { gateway, form } => {
425 self.shared.note_connect_sent(&gateway);
430 let form = self.shared.prepare_connect_form(&gateway, form);
433 let started = match ctx.ask_service::<StartReq, StartRes>(
434 SVC_START,
435 StartReq {
436 factory: "ctp".to_string(),
437 name: gateway.clone(),
438 },
439 Duration::from_secs(3),
440 ) {
441 Ok(StartRes::Started { name }) => {
442 self.shared
443 .log_line(format!("[ops] svctl.start ctp → {name} 上线"));
444 true
445 }
446 Ok(StartRes::Rejected { reason }) => {
447 if self.shared.account(&gateway).is_some_and(|a| a.connected) {
450 self.shared
451 .log_line(format!("[ops] {gateway} 已在场,重投凭据: {reason}"));
452 true
453 } else {
454 self.shared
455 .log_line(format!("[ops] {gateway} 装配被拒: {reason}"));
456 self.shared.note_connect_result(&gateway, Err(reason));
457 continue;
458 }
459 }
460 Err(e) => {
461 self.shared
462 .log_line(format!("[ops] {gateway} 装配失败: {e:?}"));
463 self.shared
464 .note_connect_result(&gateway, Err(format!("{e:?}")));
465 continue;
466 }
467 };
468 if !started {
469 continue;
470 }
471 let svc = format!("{gateway}.configure");
472 match ctx.ask_service::<ConfigureReq, ConfigureReply>(
473 svc.as_str(),
474 form.to_configure_req(),
475 Duration::from_secs(5),
476 ) {
477 Ok(ConfigureReply {
481 ok: true,
482 msg,
483 product_classes,
484 }) => {
485 self.shared.log_line(format!("[ops] {svc} 受理:{msg}"));
486 self.shared
487 .note_effective_product_classes(&gateway, &product_classes);
488 self.shared.note_connect_result(&gateway, Ok(()));
489 }
490 Ok(ConfigureReply { ok: false, msg, .. }) => {
491 self.shared.log_line(format!("[ops] {svc} 被拒:{msg}"));
492 self.shared.note_connect_result(&gateway, Err(msg));
493 }
494 Err(e) => {
495 self.shared.log_line(format!("[ops] {svc} 失败: {e:?}"));
496 self.shared
497 .note_connect_result(&gateway, Err(format!("{e:?}")));
498 }
499 }
500 }
501 crate::state::UiCmd::CtpSubscribe { gateway, symbols } => {
502 let svc = format!("{gateway}.subscribe");
506 match ctx.ask_service::<CtpSubscribeReq, CtpSubscribeReply>(
507 svc.as_str(),
508 CtpSubscribeReq {
509 symbols: symbols.clone(),
510 },
511 Duration::from_secs(5),
512 ) {
513 Ok(CtpSubscribeReply { ok: true, msg }) => {
514 self.shared.note_subscriptions(&symbols);
515 self.shared.log_line(format!(
516 "[ops] {svc} {} 受理:{}",
517 symbols.join(","),
518 msg
519 ));
520 }
521 Ok(CtpSubscribeReply { ok: false, msg }) => {
522 self.shared.log_line(format!(
523 "[ops] {svc} {} 被拒:{}",
524 symbols.join(","),
525 msg
526 ));
527 }
528 Err(e) => {
529 self.shared.log_line(format!("[ops] {svc} 失败: {e:?}"));
530 }
531 }
532 }
533 crate::state::UiCmd::Disconnect { gateway } => {
534 let svc = format!("{gateway}.disconnect");
538 match ctx.ask_service::<DisconnectReq, DisconnectReply>(
539 svc.as_str(),
540 DisconnectReq,
541 Duration::from_secs(5),
542 ) {
543 Ok(DisconnectReply { ok: true, msg }) => {
544 self.shared.mark_account_stopped(&gateway);
545 self.shared
546 .log_line(format!("[账户] 断开 {gateway}:{msg}(实例仍在场)"));
547 }
548 Ok(DisconnectReply { ok: false, msg }) => {
549 self.shared
550 .log_line(format!("[账户] 断开 {gateway} 未执行:{msg}"));
551 }
552 Err(e) => {
553 self.shared.log_line(format!(
554 "[账户] 断开 {gateway} 失败:{e:?}(网关不在场或超时)",
555 ));
556 }
557 }
558 }
559 }
560 }
561 }
562}
563
564impl Plugin for UiPlugin {
565 fn name(&self) -> &str {
566 UI_NAME
567 }
568
569 fn deps(&self) -> &[&str] {
570 &[] }
572
573 fn routes(&self) -> Vec<std::borrow::Cow<'static, str>> {
574 vec![
575 std::borrow::Cow::Borrowed(EVENT_LOG),
576 std::borrow::Cow::Borrowed(EVENT_PLUG_UP),
577 std::borrow::Cow::Borrowed(EVENT_PLUG_DOWN),
578 std::borrow::Cow::Borrowed(EVENT_BAR),
579 std::borrow::Cow::Borrowed(EVENT_BAR_UPDATE),
580 std::borrow::Cow::Borrowed(EVENT_TICK),
581 std::borrow::Cow::Borrowed(EVENT_CONTRACT),
582 std::borrow::Cow::Borrowed("eContractFinish"),
583 std::borrow::Cow::Borrowed(EVENT_TIMER),
584 ]
585 }
586
587 fn mailbox_cap(&self) -> usize {
588 1024
589 }
590
591 fn version(&self) -> std::borrow::Cow<'static, str> {
592 std::borrow::Cow::Borrowed(env!("CARGO_PKG_VERSION"))
593 }
594
595 fn services(&self) -> Vec<ServiceDesc> {
596 Vec::new() }
598
599 fn on_init(&self, ctx: &PluginContext) {
600 let _ = self.ctx.set(ctx.clone());
601 }
602
603 fn on_start(&self) {
604 self.shared.log_line("[ui] 界面插件上线");
605 }
606
607 fn on_stop(&self) {
608 self.shared.log_line("[ui] 界面插件下线");
609 }
610
611 fn on_mail(&self, event: &ek_event::Event) {
612 let ty = event.event_type.as_str();
613 if ty == EVENT_LOG {
614 if let Some(d) = event.data.downcast_ref::<LogData>() {
615 let tag = if d.is_warning_or_above() {
616 "WARN"
617 } else {
618 "INFO"
619 };
620 self.shared
621 .log_line(format!("[{tag}][{}] {}", d.gateway_name, d.msg));
622 } else {
623 self.shared.log_line("[eLog] 载荷类型异常(非 LogData)");
624 }
625 return;
626 }
627 if ty == EVENT_PLUG_UP || ty == EVENT_PLUG_DOWN {
628 if let Some(n) = event.data.downcast_ref::<PlugNotice>() {
629 let verb = if ty == EVENT_PLUG_UP {
630 "上线"
631 } else {
632 "下线"
633 };
634 self.shared
635 .log_line(format!("[host] 插件{verb} {} v{}", n.name, n.version));
636 }
637 self.shared.bump();
638 return;
639 }
640 if ty == EVENT_BAR {
641 if let Some(f) = event.data.downcast_ref::<ek_dm::wire::BarsFrame>() {
642 self.shared.market().on_frame(f);
643 }
644 self.shared.bump();
645 return;
646 }
647 if ty == EVENT_BAR_UPDATE {
648 if let Some(b) = event.data.downcast_ref::<ek_data::bar::BarData>() {
649 self.shared.market().on_update(b);
650 }
651 self.shared.bump();
652 return;
653 }
654 if ty == EVENT_TICK {
655 if let Some(t) = event.data.downcast_ref::<ek_data::tick::TickData>() {
656 self.shared.update_tick(t);
657 }
658 return;
659 }
660 if ty == EVENT_CONTRACT {
661 if let Some(c) = event.data.downcast_ref::<ek_data::contract::ContractData>() {
662 self.shared.update_contract(c);
663 }
664 return;
665 }
666 if ty == "eContractFinish" {
667 let gw = event
668 .data
669 .downcast_ref::<String>()
670 .cloned()
671 .unwrap_or_else(|| "?".to_string());
672 self.shared
673 .log_line(format!("[ctp] 合约流完成 {gw}(查询合约窗可勾选订阅)"));
674 return;
675 }
676 if ty == EVENT_TIMER {
677 self.refresh();
678 }
679 }
680}