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, 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 }
157
158 fn run_cmds(&self, ctx: &PluginContext) {
160 for cmd in self.shared.drain_cmds() {
161 match cmd {
162 crate::state::UiCmd::Start { factory, name } => {
163 match ctx.ask_service::<StartReq, StartRes>(
164 SVC_START,
165 StartReq {
166 factory: factory.clone(),
167 name: name.clone(),
168 },
169 Duration::from_secs(3),
170 ) {
171 Ok(StartRes::Started { name }) => {
172 self.shared
173 .log_line(format!("[ops] svctl.start {factory} → {name} 上线"));
174 }
175 Ok(StartRes::Rejected { reason }) => {
176 self.shared
177 .log_line(format!("[ops] start {factory} 被拒: {reason}"));
178 }
179 Err(e) => {
180 self.shared
181 .log_line(format!("[ops] start {factory} 失败: {e:?}"));
182 }
183 }
184 }
185 crate::state::UiCmd::Remove { name } => {
186 if name == UI_NAME {
192 self.shared.log_line(
193 "[ops] 拒绝移除 ui:观测面自身不可自摘(摘掉后没有下一拍执行任何指令)"
194 .to_string(),
195 );
196 continue;
197 }
198 match ctx.ask_service::<RemoveReq, RemoveRes>(
199 SVC_REMOVE,
200 RemoveReq { name: name.clone() },
201 Duration::from_secs(3),
202 ) {
203 Ok(RemoveRes::Removed { name }) => {
204 self.shared
205 .log_line(format!("[ops] svctl.remove {name} 完成"));
206 }
207 Ok(RemoveRes::NoSuchName { name }) => {
208 self.shared
209 .log_line(format!("[ops] remove {name} 不存在/不可摘"));
210 }
211 Ok(RemoveRes::Rejected { name, reason }) => {
215 self.shared
216 .log_line(format!("[ops] remove {name} 被拒: {reason}"));
217 }
218 Err(e) => {
219 self.shared
220 .log_line(format!("[ops] remove {name} 失败: {e:?}"));
221 }
222 }
223 }
224 crate::state::UiCmd::Reload {
225 interval,
226 symbol,
227 bars,
228 } => {
229 match ctx.ask_service::<ReloadReq, usize>(
231 "dm.reload",
232 ReloadReq {
233 interval,
234 symbol: symbol.clone(),
235 bars: bars.clone(),
236 size: Some(bars.len()),
237 push: true,
238 },
239 Duration::from_secs(5),
240 ) {
241 Ok(n) => {
242 self.shared.market().note_reload();
243 self.shared.log_line(format!(
244 "[ops] dm.reload {symbol}@{interval}s 回灌 {n} 条"
245 ));
246 }
247 Err(e) => {
248 self.shared
249 .log_line(format!("[ops] dm.reload {symbol} 失败: {e:?}"));
250 }
251 }
252 }
253 crate::state::UiCmd::SendOrder { req, gateway, mode } => {
254 match ctx.ask_service::<SubmitOrderReq, SubmitReply>(
258 "oms.submit_order",
259 SubmitOrderReq {
260 req: req.clone(),
261 gateway_name: gateway.clone(),
262 mode,
263 },
264 Duration::from_secs(10),
265 ) {
266 Ok(reply) => {
267 let tag = match reply.status {
268 SubmitStatus::Sent => "SENT",
269 SubmitStatus::Partial => "PARTIAL",
270 SubmitStatus::Rejected => "REJECTED",
271 };
272 for leg in &reply.legs {
273 match (leg.ok, &leg.en_orderid, &leg.reason) {
274 (true, Some(id), _) => self.shared.log_line(format!(
275 "[ops] submit {} {} {}x{} → {id} (parent {})",
276 leg.req.symbol,
277 leg.req.direction.as_str(),
278 leg.req.offset.as_str(),
279 leg.req.volume,
280 reply.parent_id
281 )),
282 _ => self.shared.log_line(format!(
283 "[ops] submit {} {} {}x{} 被拒: {} (parent {})",
284 leg.req.symbol,
285 leg.req.direction.as_str(),
286 leg.req.offset.as_str(),
287 leg.req.volume,
288 leg.reason.as_deref().unwrap_or("?"),
289 reply.parent_id
290 )),
291 }
292 }
293 if reply.legs.is_empty() {
294 self.shared.log_line(format!(
295 "[ops] submit {} {}x{} {tag}:可平不足零腿 (parent {})",
296 req.symbol,
297 req.offset.as_str(),
298 req.volume,
299 reply.parent_id
300 ));
301 }
302 }
303 Err(e) => {
304 self.shared
305 .log_line(format!("[ops] oms.submit_order 失败: {e:?}"));
306 }
307 }
308 }
309 crate::state::UiCmd::CancelOrder { orderid, gateway } => {
310 match ctx.ask_service::<OmsCancelOrderReq, OmsCancelReply>(
314 "oms.cancel_order",
315 OmsCancelOrderReq {
316 en_orderid: format!("{gateway}.{orderid}"),
317 gateway_name: gateway.clone(),
318 },
319 Duration::from_secs(10),
320 ) {
321 Ok(OmsCancelReply { ok: true, msg }) => {
322 self.shared.log_line(format!("[ops] {msg}"));
323 }
324 Ok(OmsCancelReply { ok: false, msg }) => {
325 self.shared.log_line(format!("[ops] 撤单被拒: {msg}"));
326 }
327 Err(e) => {
328 self.shared
329 .log_line(format!("[ops] oms.cancel_order 失败: {e:?}"));
330 }
331 }
332 }
333 crate::state::UiCmd::CtpConfigure { gateway, form } => {
334 let svc = format!("{gateway}.configure");
337 match ctx.ask_service::<ConfigureReq, ConfigureReply>(
338 svc.as_str(),
339 form.to_configure_req(),
340 Duration::from_secs(5),
341 ) {
342 Ok(ConfigureReply { ok: true, msg, .. }) => {
346 self.shared.log_line(format!("[ops] {svc} 受理:{msg}"));
347 }
348 Ok(ConfigureReply { ok: false, msg, .. }) => {
349 self.shared.log_line(format!("[ops] {svc} 被拒:{msg}"));
350 }
351 Err(e) => {
352 self.shared.log_line(format!("[ops] {svc} 失败: {e:?}"));
353 }
354 }
355 }
356 crate::state::UiCmd::Connect { gateway, form } => {
357 self.shared.note_connect_sent(&gateway);
362 let form = self.shared.prepare_connect_form(&gateway, form);
365 let started = match ctx.ask_service::<StartReq, StartRes>(
366 SVC_START,
367 StartReq {
368 factory: "ctp".to_string(),
369 name: gateway.clone(),
370 },
371 Duration::from_secs(3),
372 ) {
373 Ok(StartRes::Started { name }) => {
374 self.shared
375 .log_line(format!("[ops] svctl.start ctp → {name} 上线"));
376 true
377 }
378 Ok(StartRes::Rejected { reason }) => {
379 if self.shared.account(&gateway).is_some_and(|a| a.connected) {
382 self.shared
383 .log_line(format!("[ops] {gateway} 已在场,重投凭据: {reason}"));
384 true
385 } else {
386 self.shared
387 .log_line(format!("[ops] {gateway} 装配被拒: {reason}"));
388 self.shared.note_connect_result(&gateway, Err(reason));
389 continue;
390 }
391 }
392 Err(e) => {
393 self.shared
394 .log_line(format!("[ops] {gateway} 装配失败: {e:?}"));
395 self.shared
396 .note_connect_result(&gateway, Err(format!("{e:?}")));
397 continue;
398 }
399 };
400 if !started {
401 continue;
402 }
403 let svc = format!("{gateway}.configure");
404 match ctx.ask_service::<ConfigureReq, ConfigureReply>(
405 svc.as_str(),
406 form.to_configure_req(),
407 Duration::from_secs(5),
408 ) {
409 Ok(ConfigureReply {
413 ok: true,
414 msg,
415 product_classes,
416 }) => {
417 self.shared.log_line(format!("[ops] {svc} 受理:{msg}"));
418 self.shared
419 .note_effective_product_classes(&gateway, &product_classes);
420 self.shared.note_connect_result(&gateway, Ok(()));
421 }
422 Ok(ConfigureReply { ok: false, msg, .. }) => {
423 self.shared.log_line(format!("[ops] {svc} 被拒:{msg}"));
424 self.shared.note_connect_result(&gateway, Err(msg));
425 }
426 Err(e) => {
427 self.shared.log_line(format!("[ops] {svc} 失败: {e:?}"));
428 self.shared
429 .note_connect_result(&gateway, Err(format!("{e:?}")));
430 }
431 }
432 }
433 crate::state::UiCmd::CtpSubscribe { gateway, symbols } => {
434 let svc = format!("{gateway}.subscribe");
438 match ctx.ask_service::<CtpSubscribeReq, CtpSubscribeReply>(
439 svc.as_str(),
440 CtpSubscribeReq {
441 symbols: symbols.clone(),
442 },
443 Duration::from_secs(5),
444 ) {
445 Ok(CtpSubscribeReply { ok: true, msg }) => {
446 self.shared.note_subscriptions(&symbols);
447 self.shared.log_line(format!(
448 "[ops] {svc} {} 受理:{}",
449 symbols.join(","),
450 msg
451 ));
452 }
453 Ok(CtpSubscribeReply { ok: false, msg }) => {
454 self.shared.log_line(format!(
455 "[ops] {svc} {} 被拒:{}",
456 symbols.join(","),
457 msg
458 ));
459 }
460 Err(e) => {
461 self.shared.log_line(format!("[ops] {svc} 失败: {e:?}"));
462 }
463 }
464 }
465 crate::state::UiCmd::Disconnect { gateway } => {
466 let svc = format!("{gateway}.disconnect");
470 match ctx.ask_service::<DisconnectReq, DisconnectReply>(
471 svc.as_str(),
472 DisconnectReq,
473 Duration::from_secs(5),
474 ) {
475 Ok(DisconnectReply { ok: true, msg }) => {
476 self.shared.mark_account_stopped(&gateway);
477 self.shared
478 .log_line(format!("[账户] 断开 {gateway}:{msg}(实例仍在场)"));
479 }
480 Ok(DisconnectReply { ok: false, msg }) => {
481 self.shared
482 .log_line(format!("[账户] 断开 {gateway} 未执行:{msg}"));
483 }
484 Err(e) => {
485 self.shared.log_line(format!(
486 "[账户] 断开 {gateway} 失败:{e:?}(网关不在场或超时)",
487 ));
488 }
489 }
490 }
491 }
492 }
493 }
494}
495
496impl Plugin for UiPlugin {
497 fn name(&self) -> &str {
498 UI_NAME
499 }
500
501 fn deps(&self) -> &[&str] {
502 &[] }
504
505 fn routes(&self) -> Vec<std::borrow::Cow<'static, str>> {
506 vec![
507 std::borrow::Cow::Borrowed(EVENT_LOG),
508 std::borrow::Cow::Borrowed(EVENT_PLUG_UP),
509 std::borrow::Cow::Borrowed(EVENT_PLUG_DOWN),
510 std::borrow::Cow::Borrowed(EVENT_BAR),
511 std::borrow::Cow::Borrowed(EVENT_BAR_UPDATE),
512 std::borrow::Cow::Borrowed(EVENT_TICK),
513 std::borrow::Cow::Borrowed(EVENT_CONTRACT),
514 std::borrow::Cow::Borrowed("eContractFinish"),
515 std::borrow::Cow::Borrowed(EVENT_TIMER),
516 ]
517 }
518
519 fn mailbox_cap(&self) -> usize {
520 1024
521 }
522
523 fn version(&self) -> std::borrow::Cow<'static, str> {
524 std::borrow::Cow::Borrowed(env!("CARGO_PKG_VERSION"))
525 }
526
527 fn services(&self) -> Vec<ServiceDesc> {
528 Vec::new() }
530
531 fn on_init(&self, ctx: &PluginContext) {
532 let _ = self.ctx.set(ctx.clone());
533 }
534
535 fn on_start(&self) {
536 self.shared.log_line("[ui] 界面插件上线");
537 }
538
539 fn on_stop(&self) {
540 self.shared.log_line("[ui] 界面插件下线");
541 }
542
543 fn on_mail(&self, event: &ek_event::Event) {
544 let ty = event.event_type.as_str();
545 if ty == EVENT_LOG {
546 if let Some(d) = event.data.downcast_ref::<LogData>() {
547 let tag = if d.is_warning_or_above() {
548 "WARN"
549 } else {
550 "INFO"
551 };
552 self.shared
553 .log_line(format!("[{tag}][{}] {}", d.gateway_name, d.msg));
554 } else {
555 self.shared.log_line("[eLog] 载荷类型异常(非 LogData)");
556 }
557 return;
558 }
559 if ty == EVENT_PLUG_UP || ty == EVENT_PLUG_DOWN {
560 if let Some(n) = event.data.downcast_ref::<PlugNotice>() {
561 let verb = if ty == EVENT_PLUG_UP {
562 "上线"
563 } else {
564 "下线"
565 };
566 self.shared
567 .log_line(format!("[host] 插件{verb} {} v{}", n.name, n.version));
568 }
569 self.shared.bump();
570 return;
571 }
572 if ty == EVENT_BAR {
573 if let Some(f) = event.data.downcast_ref::<ek_dm::wire::BarsFrame>() {
574 self.shared.market().on_frame(f);
575 }
576 self.shared.bump();
577 return;
578 }
579 if ty == EVENT_BAR_UPDATE {
580 if let Some(b) = event.data.downcast_ref::<ek_data::bar::BarData>() {
581 self.shared.market().on_update(b);
582 }
583 self.shared.bump();
584 return;
585 }
586 if ty == EVENT_TICK {
587 if let Some(t) = event.data.downcast_ref::<ek_data::tick::TickData>() {
588 self.shared.update_tick(t);
589 }
590 return;
591 }
592 if ty == EVENT_CONTRACT {
593 if let Some(c) = event.data.downcast_ref::<ek_data::contract::ContractData>() {
594 self.shared.update_contract(c);
595 }
596 return;
597 }
598 if ty == "eContractFinish" {
599 let gw = event
600 .data
601 .downcast_ref::<String>()
602 .cloned()
603 .unwrap_or_else(|| "?".to_string());
604 self.shared
605 .log_line(format!("[ctp] 合约流完成 {gw}(查询合约窗可勾选订阅)"));
606 return;
607 }
608 if ty == EVENT_TIMER {
609 self.refresh();
610 }
611 }
612}