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 struct UiPlugin {
50 shared: std::sync::Arc<UiShared>,
51 ctx: OnceLock<PluginContext>,
52}
53
54impl UiPlugin {
55 pub fn new(shared: std::sync::Arc<UiShared>) -> Self {
57 Self {
58 shared,
59 ctx: OnceLock::new(),
60 }
61 }
62
63 fn refresh(&self) {
65 let Some(ctx) = self.ctx.get() else {
66 return; };
68 self.run_cmds(ctx);
69 match ctx.ask_service::<ListReq, Vec<PlugRow>>(SVC_LIST, ListReq, Duration::from_secs(2)) {
70 Ok(rows) => {
71 let mut services = Vec::with_capacity(rows.len());
72 for r in &rows {
73 let ids = ctx
74 .services_of(&r.name)
75 .map(|v| v.iter().map(|s| s.id.clone()).collect::<Vec<_>>())
76 .unwrap_or_default();
77 services.push((r.name.clone(), ids));
78 }
79 let newly = self.shared.sync_accounts_connected(&rows);
81 self.shared.set_snapshot(BusSnap { rows, services });
82 let md = self.shared.md_source();
83 let subs = self.shared.subscriptions();
84 for gw in newly {
85 if !md.is_empty() && gw == md && !subs.is_empty() {
86 self.shared.push_cmd(crate::state::UiCmd::CtpSubscribe {
87 gateway: gw.clone(),
88 symbols: subs.clone(),
89 });
90 self.shared
91 .log_line(format!("[ops] 行情源 {gw} 上线,重放订阅 {} 项", subs.len()));
92 }
93 }
94 }
95 Err(e) => self.shared.log_line(format!("[ui] svctl.list 失败: {e:?}")),
96 }
97 let orders = ctx
99 .ask_service::<GetAllOrdersReq, Vec<OrderData>>(
100 "oms.get_all_orders",
101 GetAllOrdersReq,
102 Duration::from_secs(2),
103 )
104 .unwrap_or_default();
105 let trades = ctx
106 .ask_service::<GetAllTradesReq, Vec<TradeData>>(
107 "oms.get_all_trades",
108 GetAllTradesReq,
109 Duration::from_secs(2),
110 )
111 .unwrap_or_default();
112 let positions = ctx
113 .ask_service::<GetAllPositionsReq, Vec<PositionData>>(
114 "oms.get_all_positions",
115 GetAllPositionsReq,
116 Duration::from_secs(2),
117 )
118 .unwrap_or_default();
119 let accounts = ctx
120 .ask_service::<GetAllAccountsReq, Vec<AccountData>>(
121 "oms.get_all_accounts",
122 GetAllAccountsReq,
123 Duration::from_secs(2),
124 )
125 .unwrap_or_default();
126 self.shared.set_trade_snap(crate::state::TradeSnap {
127 orders,
128 trades,
129 positions,
130 accounts,
131 });
132 if self.shared.status_poll_due() {
139 for a in self.shared.accounts() {
140 if a.stage != crate::state::AccountStage::Assembled || !a.connected {
141 continue;
142 }
143 let svc = format!("{}.status", a.gateway);
144 match ctx.ask_service::<StatusReq, GatewayStatus>(
145 &svc,
146 StatusReq,
147 Duration::from_secs(2),
148 ) {
149 Ok(st) => self.shared.note_gateway_status(&a.gateway, &st),
150 Err(e) => self
151 .shared
152 .note_status_unreachable(&a.gateway, &format!("{e:?}")),
153 }
154 }
155 }
156 if self.shared.pipeline_poll_due() {
164 match ctx.ask_service::<PipelineStatusReq, Vec<String>>(
165 "oms.pipeline_status",
166 PipelineStatusReq,
167 Duration::from_secs(2),
168 ) {
169 Ok(lines) => self.shared.set_pipeline_lines(lines),
170 Err(e) => {
171 if !self.shared.pipeline_lines().is_empty() {
175 self.shared
176 .log_line(format!("[ui] oms.pipeline_status 失手: {e:?}"));
177 }
178 self.shared.set_pipeline_lines(Vec::new());
179 }
180 }
181 }
182 }
183
184 fn run_cmds(&self, ctx: &PluginContext) {
186 for cmd in self.shared.drain_cmds() {
187 match cmd {
188 crate::state::UiCmd::Start { factory, name } => {
189 match ctx.ask_service::<StartReq, StartRes>(
190 SVC_START,
191 StartReq {
192 factory: factory.clone(),
193 name: name.clone(),
194 },
195 Duration::from_secs(3),
196 ) {
197 Ok(StartRes::Started { name }) => {
198 self.shared
199 .log_line(format!("[ops] svctl.start {factory} → {name} 上线"));
200 }
201 Ok(StartRes::Rejected { reason }) => {
202 self.shared
203 .log_line(format!("[ops] start {factory} 被拒: {reason}"));
204 }
205 Err(e) => {
206 self.shared
207 .log_line(format!("[ops] start {factory} 失败: {e:?}"));
208 }
209 }
210 }
211 crate::state::UiCmd::Remove { name } => {
212 if name == UI_NAME {
218 self.shared.log_line(
219 "[ops] 拒绝移除 ui:观测面自身不可自摘(摘掉后没有下一拍执行任何指令)"
220 .to_string(),
221 );
222 continue;
223 }
224 match ctx.ask_service::<RemoveReq, RemoveRes>(
225 SVC_REMOVE,
226 RemoveReq { name: name.clone() },
227 Duration::from_secs(3),
228 ) {
229 Ok(RemoveRes::Removed { name }) => {
230 self.shared
231 .log_line(format!("[ops] svctl.remove {name} 完成"));
232 }
233 Ok(RemoveRes::NoSuchName { name }) => {
234 self.shared
235 .log_line(format!("[ops] remove {name} 不存在/不可摘"));
236 }
237 Ok(RemoveRes::Rejected { name, reason }) => {
241 self.shared
242 .log_line(format!("[ops] remove {name} 被拒: {reason}"));
243 }
244 Err(e) => {
245 self.shared
246 .log_line(format!("[ops] remove {name} 失败: {e:?}"));
247 }
248 }
249 }
250 crate::state::UiCmd::Reload {
251 interval,
252 symbol,
253 bars,
254 } => {
255 match ctx.ask_service::<ReloadReq, usize>(
257 "dm.reload",
258 ReloadReq {
259 interval,
260 symbol: symbol.clone(),
261 bars: bars.clone(),
262 size: Some(bars.len()),
263 push: true,
264 },
265 Duration::from_secs(5),
266 ) {
267 Ok(n) => {
268 self.shared.market().note_reload();
269 self.shared.log_line(format!(
270 "[ops] dm.reload {symbol}@{interval}s 回灌 {n} 条"
271 ));
272 }
273 Err(e) => {
274 self.shared
275 .log_line(format!("[ops] dm.reload {symbol} 失败: {e:?}"));
276 }
277 }
278 }
279 crate::state::UiCmd::SendOrder { req, gateway, mode } => {
280 match ctx.ask_service::<SubmitOrderReq, SubmitReply>(
284 "oms.submit_order",
285 SubmitOrderReq {
286 req: req.clone(),
287 gateway_name: gateway.clone(),
288 mode,
289 },
290 Duration::from_secs(10),
291 ) {
292 Ok(reply) => {
293 let tag = match reply.status {
294 SubmitStatus::Sent => "SENT",
295 SubmitStatus::Partial => "PARTIAL",
296 SubmitStatus::Rejected => "REJECTED",
297 };
298 for leg in &reply.legs {
299 match (leg.ok, &leg.en_orderid, &leg.reason) {
300 (true, Some(id), _) => self.shared.log_line(format!(
301 "[ops] submit {} {} {}x{} → {id} (parent {})",
302 leg.req.symbol,
303 leg.req.direction.as_str(),
304 leg.req.offset.as_str(),
305 leg.req.volume,
306 reply.parent_id
307 )),
308 _ => self.shared.log_line(format!(
309 "[ops] submit {} {} {}x{} 被拒: {} (parent {})",
310 leg.req.symbol,
311 leg.req.direction.as_str(),
312 leg.req.offset.as_str(),
313 leg.req.volume,
314 leg.reason.as_deref().unwrap_or("?"),
315 reply.parent_id
316 )),
317 }
318 }
319 if reply.legs.is_empty() {
320 self.shared.log_line(format!(
321 "[ops] submit {} {}x{} {tag}:可平不足零腿 (parent {})",
322 req.symbol,
323 req.offset.as_str(),
324 req.volume,
325 reply.parent_id
326 ));
327 }
328 }
329 Err(e) => {
330 self.shared
331 .log_line(format!("[ops] oms.submit_order 失败: {e:?}"));
332 }
333 }
334 }
335 crate::state::UiCmd::CancelOrder { orderid, gateway } => {
336 match ctx.ask_service::<OmsCancelOrderReq, OmsCancelReply>(
340 "oms.cancel_order",
341 OmsCancelOrderReq {
342 en_orderid: format!("{gateway}.{orderid}"),
343 gateway_name: gateway.clone(),
344 },
345 Duration::from_secs(10),
346 ) {
347 Ok(OmsCancelReply { ok: true, msg }) => {
348 self.shared.log_line(format!("[ops] {msg}"));
349 }
350 Ok(OmsCancelReply { ok: false, msg }) => {
351 self.shared.log_line(format!("[ops] 撤单被拒: {msg}"));
352 }
353 Err(e) => {
354 self.shared
355 .log_line(format!("[ops] oms.cancel_order 失败: {e:?}"));
356 }
357 }
358 }
359 crate::state::UiCmd::CtpConfigure { gateway, form } => {
360 let svc = format!("{gateway}.configure");
363 match ctx.ask_service::<ConfigureReq, ConfigureReply>(
364 svc.as_str(),
365 form.to_configure_req(),
366 Duration::from_secs(5),
367 ) {
368 Ok(ConfigureReply { ok: true, msg, .. }) => {
372 self.shared.log_line(format!("[ops] {svc} 受理:{msg}"));
373 }
374 Ok(ConfigureReply { ok: false, msg, .. }) => {
375 self.shared.log_line(format!("[ops] {svc} 被拒:{msg}"));
376 }
377 Err(e) => {
378 self.shared.log_line(format!("[ops] {svc} 失败: {e:?}"));
379 }
380 }
381 }
382 crate::state::UiCmd::Connect { gateway, form } => {
383 self.shared.note_connect_sent(&gateway);
388 let form = self.shared.prepare_connect_form(&gateway, form);
391 let started = match ctx.ask_service::<StartReq, StartRes>(
392 SVC_START,
393 StartReq {
394 factory: "ctp".to_string(),
395 name: gateway.clone(),
396 },
397 Duration::from_secs(3),
398 ) {
399 Ok(StartRes::Started { name }) => {
400 self.shared
401 .log_line(format!("[ops] svctl.start ctp → {name} 上线"));
402 true
403 }
404 Ok(StartRes::Rejected { reason }) => {
405 if self.shared.account(&gateway).is_some_and(|a| a.connected) {
408 self.shared
409 .log_line(format!("[ops] {gateway} 已在场,重投凭据: {reason}"));
410 true
411 } else {
412 self.shared
413 .log_line(format!("[ops] {gateway} 装配被拒: {reason}"));
414 self.shared.note_connect_result(&gateway, Err(reason));
415 continue;
416 }
417 }
418 Err(e) => {
419 self.shared
420 .log_line(format!("[ops] {gateway} 装配失败: {e:?}"));
421 self.shared
422 .note_connect_result(&gateway, Err(format!("{e:?}")));
423 continue;
424 }
425 };
426 if !started {
427 continue;
428 }
429 let svc = format!("{gateway}.configure");
430 match ctx.ask_service::<ConfigureReq, ConfigureReply>(
431 svc.as_str(),
432 form.to_configure_req(),
433 Duration::from_secs(5),
434 ) {
435 Ok(ConfigureReply {
439 ok: true,
440 msg,
441 product_classes,
442 }) => {
443 self.shared.log_line(format!("[ops] {svc} 受理:{msg}"));
444 self.shared
445 .note_effective_product_classes(&gateway, &product_classes);
446 self.shared.note_connect_result(&gateway, Ok(()));
447 }
448 Ok(ConfigureReply { ok: false, msg, .. }) => {
449 self.shared.log_line(format!("[ops] {svc} 被拒:{msg}"));
450 self.shared.note_connect_result(&gateway, Err(msg));
451 }
452 Err(e) => {
453 self.shared.log_line(format!("[ops] {svc} 失败: {e:?}"));
454 self.shared
455 .note_connect_result(&gateway, Err(format!("{e:?}")));
456 }
457 }
458 }
459 crate::state::UiCmd::CtpSubscribe { gateway, symbols } => {
460 let svc = format!("{gateway}.subscribe");
464 match ctx.ask_service::<CtpSubscribeReq, CtpSubscribeReply>(
465 svc.as_str(),
466 CtpSubscribeReq {
467 symbols: symbols.clone(),
468 },
469 Duration::from_secs(5),
470 ) {
471 Ok(CtpSubscribeReply { ok: true, msg }) => {
472 self.shared.note_subscriptions(&symbols);
473 self.shared.log_line(format!(
474 "[ops] {svc} {} 受理:{}",
475 symbols.join(","),
476 msg
477 ));
478 }
479 Ok(CtpSubscribeReply { ok: false, msg }) => {
480 self.shared.log_line(format!(
481 "[ops] {svc} {} 被拒:{}",
482 symbols.join(","),
483 msg
484 ));
485 }
486 Err(e) => {
487 self.shared.log_line(format!("[ops] {svc} 失败: {e:?}"));
488 }
489 }
490 }
491 crate::state::UiCmd::Disconnect { gateway } => {
492 let svc = format!("{gateway}.disconnect");
496 match ctx.ask_service::<DisconnectReq, DisconnectReply>(
497 svc.as_str(),
498 DisconnectReq,
499 Duration::from_secs(5),
500 ) {
501 Ok(DisconnectReply { ok: true, msg }) => {
502 self.shared.mark_account_stopped(&gateway);
503 self.shared
504 .log_line(format!("[账户] 断开 {gateway}:{msg}(实例仍在场)"));
505 }
506 Ok(DisconnectReply { ok: false, msg }) => {
507 self.shared
508 .log_line(format!("[账户] 断开 {gateway} 未执行:{msg}"));
509 }
510 Err(e) => {
511 self.shared.log_line(format!(
512 "[账户] 断开 {gateway} 失败:{e:?}(网关不在场或超时)",
513 ));
514 }
515 }
516 }
517 }
518 }
519 }
520}
521
522impl Plugin for UiPlugin {
523 fn name(&self) -> &str {
524 UI_NAME
525 }
526
527 fn deps(&self) -> &[&str] {
528 &[] }
530
531 fn routes(&self) -> Vec<std::borrow::Cow<'static, str>> {
532 vec![
533 std::borrow::Cow::Borrowed(EVENT_LOG),
534 std::borrow::Cow::Borrowed(EVENT_PLUG_UP),
535 std::borrow::Cow::Borrowed(EVENT_PLUG_DOWN),
536 std::borrow::Cow::Borrowed(EVENT_BAR),
537 std::borrow::Cow::Borrowed(EVENT_BAR_UPDATE),
538 std::borrow::Cow::Borrowed(EVENT_TICK),
539 std::borrow::Cow::Borrowed(EVENT_CONTRACT),
540 std::borrow::Cow::Borrowed("eContractFinish"),
541 std::borrow::Cow::Borrowed(EVENT_TIMER),
542 ]
543 }
544
545 fn mailbox_cap(&self) -> usize {
546 1024
547 }
548
549 fn version(&self) -> std::borrow::Cow<'static, str> {
550 std::borrow::Cow::Borrowed(env!("CARGO_PKG_VERSION"))
551 }
552
553 fn services(&self) -> Vec<ServiceDesc> {
554 Vec::new() }
556
557 fn on_init(&self, ctx: &PluginContext) {
558 let _ = self.ctx.set(ctx.clone());
559 }
560
561 fn on_start(&self) {
562 self.shared.log_line("[ui] 界面插件上线");
563 }
564
565 fn on_stop(&self) {
566 self.shared.log_line("[ui] 界面插件下线");
567 }
568
569 fn on_mail(&self, event: &ek_event::Event) {
570 let ty = event.event_type.as_str();
571 if ty == EVENT_LOG {
572 if let Some(d) = event.data.downcast_ref::<LogData>() {
573 let tag = if d.is_warning_or_above() {
574 "WARN"
575 } else {
576 "INFO"
577 };
578 self.shared
579 .log_line(format!("[{tag}][{}] {}", d.gateway_name, d.msg));
580 } else {
581 self.shared.log_line("[eLog] 载荷类型异常(非 LogData)");
582 }
583 return;
584 }
585 if ty == EVENT_PLUG_UP || ty == EVENT_PLUG_DOWN {
586 if let Some(n) = event.data.downcast_ref::<PlugNotice>() {
587 let verb = if ty == EVENT_PLUG_UP {
588 "上线"
589 } else {
590 "下线"
591 };
592 self.shared
593 .log_line(format!("[host] 插件{verb} {} v{}", n.name, n.version));
594 }
595 self.shared.bump();
596 return;
597 }
598 if ty == EVENT_BAR {
599 if let Some(f) = event.data.downcast_ref::<ek_dm::wire::BarsFrame>() {
600 self.shared.market().on_frame(f);
601 }
602 self.shared.bump();
603 return;
604 }
605 if ty == EVENT_BAR_UPDATE {
606 if let Some(b) = event.data.downcast_ref::<ek_data::bar::BarData>() {
607 self.shared.market().on_update(b);
608 }
609 self.shared.bump();
610 return;
611 }
612 if ty == EVENT_TICK {
613 if let Some(t) = event.data.downcast_ref::<ek_data::tick::TickData>() {
614 self.shared.update_tick(t);
615 }
616 return;
617 }
618 if ty == EVENT_CONTRACT {
619 if let Some(c) = event.data.downcast_ref::<ek_data::contract::ContractData>() {
620 self.shared.update_contract(c);
621 }
622 return;
623 }
624 if ty == "eContractFinish" {
625 let gw = event
626 .data
627 .downcast_ref::<String>()
628 .cloned()
629 .unwrap_or_else(|| "?".to_string());
630 self.shared
631 .log_line(format!("[ctp] 合约流完成 {gw}(查询合约窗可勾选订阅)"));
632 return;
633 }
634 if ty == EVENT_TIMER {
635 self.refresh();
636 }
637 }
638}