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