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, SubscribeReply as CtpSubscribeReply,
24 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 }
133
134 fn run_cmds(&self, ctx: &PluginContext) {
136 for cmd in self.shared.drain_cmds() {
137 match cmd {
138 crate::state::UiCmd::Start { factory, name } => {
139 match ctx.ask_service::<StartReq, StartRes>(
140 SVC_START,
141 StartReq {
142 factory: factory.clone(),
143 name: name.clone(),
144 },
145 Duration::from_secs(3),
146 ) {
147 Ok(StartRes::Started { name }) => {
148 self.shared
149 .log_line(format!("[ops] svctl.start {factory} → {name} 上线"));
150 }
151 Ok(StartRes::Rejected { reason }) => {
152 self.shared
153 .log_line(format!("[ops] start {factory} 被拒: {reason}"));
154 }
155 Err(e) => {
156 self.shared
157 .log_line(format!("[ops] start {factory} 失败: {e:?}"));
158 }
159 }
160 }
161 crate::state::UiCmd::Remove { name } => {
162 match ctx.ask_service::<RemoveReq, RemoveRes>(
163 SVC_REMOVE,
164 RemoveReq { name: name.clone() },
165 Duration::from_secs(3),
166 ) {
167 Ok(RemoveRes::Removed { name }) => {
168 self.shared
169 .log_line(format!("[ops] svctl.remove {name} 完成"));
170 }
171 Ok(RemoveRes::NoSuchName { name }) => {
172 self.shared
173 .log_line(format!("[ops] remove {name} 不存在/不可摘"));
174 }
175 Err(e) => {
176 self.shared
177 .log_line(format!("[ops] remove {name} 失败: {e:?}"));
178 }
179 }
180 }
181 crate::state::UiCmd::Reload {
182 interval,
183 symbol,
184 bars,
185 } => {
186 match ctx.ask_service::<ReloadReq, usize>(
188 "dm.reload",
189 ReloadReq {
190 interval,
191 symbol: symbol.clone(),
192 bars: bars.clone(),
193 size: Some(bars.len()),
194 push: true,
195 },
196 Duration::from_secs(5),
197 ) {
198 Ok(n) => {
199 self.shared.market().note_reload();
200 self.shared.log_line(format!(
201 "[ops] dm.reload {symbol}@{interval}s 回灌 {n} 条"
202 ));
203 }
204 Err(e) => {
205 self.shared
206 .log_line(format!("[ops] dm.reload {symbol} 失败: {e:?}"));
207 }
208 }
209 }
210 crate::state::UiCmd::SendOrder { req, gateway, mode } => {
211 match ctx.ask_service::<SubmitOrderReq, SubmitReply>(
215 "oms.submit_order",
216 SubmitOrderReq {
217 req: req.clone(),
218 gateway_name: gateway.clone(),
219 mode,
220 },
221 Duration::from_secs(10),
222 ) {
223 Ok(reply) => {
224 let tag = match reply.status {
225 SubmitStatus::Sent => "SENT",
226 SubmitStatus::Partial => "PARTIAL",
227 SubmitStatus::Rejected => "REJECTED",
228 };
229 for leg in &reply.legs {
230 match (leg.ok, &leg.en_orderid, &leg.reason) {
231 (true, Some(id), _) => self.shared.log_line(format!(
232 "[ops] submit {} {} {}x{} → {id} (parent {})",
233 leg.req.symbol,
234 leg.req.direction.as_str(),
235 leg.req.offset.as_str(),
236 leg.req.volume,
237 reply.parent_id
238 )),
239 _ => self.shared.log_line(format!(
240 "[ops] submit {} {} {}x{} 被拒: {} (parent {})",
241 leg.req.symbol,
242 leg.req.direction.as_str(),
243 leg.req.offset.as_str(),
244 leg.req.volume,
245 leg.reason.as_deref().unwrap_or("?"),
246 reply.parent_id
247 )),
248 }
249 }
250 if reply.legs.is_empty() {
251 self.shared.log_line(format!(
252 "[ops] submit {} {}x{} {tag}:可平不足零腿 (parent {})",
253 req.symbol,
254 req.offset.as_str(),
255 req.volume,
256 reply.parent_id
257 ));
258 }
259 }
260 Err(e) => {
261 self.shared
262 .log_line(format!("[ops] oms.submit_order 失败: {e:?}"));
263 }
264 }
265 }
266 crate::state::UiCmd::CancelOrder { orderid, gateway } => {
267 match ctx.ask_service::<OmsCancelOrderReq, OmsCancelReply>(
271 "oms.cancel_order",
272 OmsCancelOrderReq {
273 en_orderid: format!("{gateway}.{orderid}"),
274 gateway_name: gateway.clone(),
275 },
276 Duration::from_secs(10),
277 ) {
278 Ok(OmsCancelReply { ok: true, msg }) => {
279 self.shared.log_line(format!("[ops] {msg}"));
280 }
281 Ok(OmsCancelReply { ok: false, msg }) => {
282 self.shared.log_line(format!("[ops] 撤单被拒: {msg}"));
283 }
284 Err(e) => {
285 self.shared
286 .log_line(format!("[ops] oms.cancel_order 失败: {e:?}"));
287 }
288 }
289 }
290 crate::state::UiCmd::CtpConfigure { gateway, form } => {
291 let svc = format!("{gateway}.configure");
294 match ctx.ask_service::<ConfigureReq, ConfigureReply>(
295 svc.as_str(),
296 form.to_configure_req(),
297 Duration::from_secs(5),
298 ) {
299 Ok(ConfigureReply { ok: true, msg, .. }) => {
303 self.shared.log_line(format!("[ops] {svc} 受理:{msg}"));
304 }
305 Ok(ConfigureReply { ok: false, msg, .. }) => {
306 self.shared.log_line(format!("[ops] {svc} 被拒:{msg}"));
307 }
308 Err(e) => {
309 self.shared.log_line(format!("[ops] {svc} 失败: {e:?}"));
310 }
311 }
312 }
313 crate::state::UiCmd::Connect { gateway, form } => {
314 self.shared.note_connect_sent(&gateway);
319 let form = self.shared.prepare_connect_form(&gateway, form);
322 let started = match ctx.ask_service::<StartReq, StartRes>(
323 SVC_START,
324 StartReq {
325 factory: "ctp".to_string(),
326 name: gateway.clone(),
327 },
328 Duration::from_secs(3),
329 ) {
330 Ok(StartRes::Started { name }) => {
331 self.shared
332 .log_line(format!("[ops] svctl.start ctp → {name} 上线"));
333 true
334 }
335 Ok(StartRes::Rejected { reason }) => {
336 if self.shared.account(&gateway).is_some_and(|a| a.connected) {
339 self.shared
340 .log_line(format!("[ops] {gateway} 已在场,重投凭据: {reason}"));
341 true
342 } else {
343 self.shared
344 .log_line(format!("[ops] {gateway} 装配被拒: {reason}"));
345 self.shared.note_connect_result(&gateway, Err(reason));
346 continue;
347 }
348 }
349 Err(e) => {
350 self.shared
351 .log_line(format!("[ops] {gateway} 装配失败: {e:?}"));
352 self.shared
353 .note_connect_result(&gateway, Err(format!("{e:?}")));
354 continue;
355 }
356 };
357 if !started {
358 continue;
359 }
360 let svc = format!("{gateway}.configure");
361 match ctx.ask_service::<ConfigureReq, ConfigureReply>(
362 svc.as_str(),
363 form.to_configure_req(),
364 Duration::from_secs(5),
365 ) {
366 Ok(ConfigureReply {
370 ok: true,
371 msg,
372 product_classes,
373 }) => {
374 self.shared.log_line(format!("[ops] {svc} 受理:{msg}"));
375 self.shared
376 .note_effective_product_classes(&gateway, &product_classes);
377 self.shared.note_connect_result(&gateway, Ok(()));
378 }
379 Ok(ConfigureReply { ok: false, msg, .. }) => {
380 self.shared.log_line(format!("[ops] {svc} 被拒:{msg}"));
381 self.shared.note_connect_result(&gateway, Err(msg));
382 }
383 Err(e) => {
384 self.shared.log_line(format!("[ops] {svc} 失败: {e:?}"));
385 self.shared
386 .note_connect_result(&gateway, Err(format!("{e:?}")));
387 }
388 }
389 }
390 crate::state::UiCmd::CtpSubscribe { gateway, symbols } => {
391 let svc = format!("{gateway}.subscribe");
395 match ctx.ask_service::<CtpSubscribeReq, CtpSubscribeReply>(
396 svc.as_str(),
397 CtpSubscribeReq {
398 symbols: symbols.clone(),
399 },
400 Duration::from_secs(5),
401 ) {
402 Ok(CtpSubscribeReply { ok: true, msg }) => {
403 self.shared.note_subscriptions(&symbols);
404 self.shared.log_line(format!(
405 "[ops] {svc} {} 受理:{}",
406 symbols.join(","),
407 msg
408 ));
409 }
410 Ok(CtpSubscribeReply { ok: false, msg }) => {
411 self.shared.log_line(format!(
412 "[ops] {svc} {} 被拒:{}",
413 symbols.join(","),
414 msg
415 ));
416 }
417 Err(e) => {
418 self.shared.log_line(format!("[ops] {svc} 失败: {e:?}"));
419 }
420 }
421 }
422 }
423 }
424 }
425}
426
427impl Plugin for UiPlugin {
428 fn name(&self) -> &str {
429 UI_NAME
430 }
431
432 fn deps(&self) -> &[&str] {
433 &[] }
435
436 fn routes(&self) -> Vec<std::borrow::Cow<'static, str>> {
437 vec![
438 std::borrow::Cow::Borrowed(EVENT_LOG),
439 std::borrow::Cow::Borrowed(EVENT_PLUG_UP),
440 std::borrow::Cow::Borrowed(EVENT_PLUG_DOWN),
441 std::borrow::Cow::Borrowed(EVENT_BAR),
442 std::borrow::Cow::Borrowed(EVENT_BAR_UPDATE),
443 std::borrow::Cow::Borrowed(EVENT_TICK),
444 std::borrow::Cow::Borrowed(EVENT_CONTRACT),
445 std::borrow::Cow::Borrowed("eContractFinish"),
446 std::borrow::Cow::Borrowed(EVENT_TIMER),
447 ]
448 }
449
450 fn mailbox_cap(&self) -> usize {
451 1024
452 }
453
454 fn version(&self) -> std::borrow::Cow<'static, str> {
455 std::borrow::Cow::Borrowed(env!("CARGO_PKG_VERSION"))
456 }
457
458 fn services(&self) -> Vec<ServiceDesc> {
459 Vec::new() }
461
462 fn on_init(&self, ctx: &PluginContext) {
463 let _ = self.ctx.set(ctx.clone());
464 }
465
466 fn on_start(&self) {
467 self.shared.log_line("[ui] 界面插件上线");
468 }
469
470 fn on_stop(&self) {
471 self.shared.log_line("[ui] 界面插件下线");
472 }
473
474 fn on_mail(&self, event: &ek_event::Event) {
475 let ty = event.event_type.as_str();
476 if ty == EVENT_LOG {
477 if let Some(d) = event.data.downcast_ref::<LogData>() {
478 let tag = if d.is_warning_or_above() {
479 "WARN"
480 } else {
481 "INFO"
482 };
483 self.shared
484 .log_line(format!("[{tag}][{}] {}", d.gateway_name, d.msg));
485 } else {
486 self.shared.log_line("[eLog] 载荷类型异常(非 LogData)");
487 }
488 return;
489 }
490 if ty == EVENT_PLUG_UP || ty == EVENT_PLUG_DOWN {
491 if let Some(n) = event.data.downcast_ref::<PlugNotice>() {
492 let verb = if ty == EVENT_PLUG_UP {
493 "上线"
494 } else {
495 "下线"
496 };
497 self.shared
498 .log_line(format!("[host] 插件{verb} {} v{}", n.name, n.version));
499 }
500 self.shared.bump();
501 return;
502 }
503 if ty == EVENT_BAR {
504 if let Some(f) = event.data.downcast_ref::<ek_dm::wire::BarsFrame>() {
505 self.shared.market().on_frame(f);
506 }
507 self.shared.bump();
508 return;
509 }
510 if ty == EVENT_BAR_UPDATE {
511 if let Some(b) = event.data.downcast_ref::<ek_data::bar::BarData>() {
512 self.shared.market().on_update(b);
513 }
514 self.shared.bump();
515 return;
516 }
517 if ty == EVENT_TICK {
518 if let Some(t) = event.data.downcast_ref::<ek_data::tick::TickData>() {
519 self.shared.update_tick(t);
520 }
521 return;
522 }
523 if ty == EVENT_CONTRACT {
524 if let Some(c) = event.data.downcast_ref::<ek_data::contract::ContractData>() {
525 self.shared.update_contract(c);
526 }
527 return;
528 }
529 if ty == "eContractFinish" {
530 let gw = event
531 .data
532 .downcast_ref::<String>()
533 .cloned()
534 .unwrap_or_else(|| "?".to_string());
535 self.shared
536 .log_line(format!("[ctp] 合约流完成 {gw}(查询合约窗可勾选订阅)"));
537 return;
538 }
539 if ty == EVENT_TIMER {
540 self.refresh();
541 }
542 }
543}