1use std::sync::OnceLock;
15use std::time::Duration;
16
17use ek_core::context::PluginContext;
18use ek_core::plugin::Plugin;
19use ek_core::service::{PlugNotice, ServiceDesc};
20use ek_core::{EVENT_PLUG_DOWN, EVENT_PLUG_UP};
21use ek_ctp::plugin::{
22 ConfigureReply, ConfigureReq, SubscribeReply as CtpSubscribeReply,
23 SubscribeReq as CtpSubscribeReq,
24};
25use ek_data::account::AccountData;
26use ek_data::event::{EVENT_BAR, EVENT_CONTRACT, EVENT_LOG, EVENT_TICK};
27use ek_data::log::LogData;
28use ek_data::order::OrderData;
29use ek_data::position::PositionData;
30use ek_data::trade::TradeData;
31use ek_dm::plugin::{EVENT_BAR_UPDATE, ReloadReq};
32use ek_event::EVENT_TIMER;
33use ek_oms::plugin::{
34 GetAllAccountsReq, GetAllOrdersReq, GetAllPositionsReq, GetAllTradesReq, OmsCancelOrderReq,
35 OmsCancelReply, SubmitOrderReq,
36};
37use ek_oms::{SubmitReply, SubmitStatus};
38use ek_station::wire::{
39 ListReq, PlugRow, RemoveReq, RemoveRes, SVC_LIST, SVC_REMOVE, SVC_START, StartReq, StartRes,
40};
41
42use crate::state::{BusSnap, UiShared};
43
44pub const UI_NAME: &str = "ui";
46
47pub struct UiPlugin {
49 shared: std::sync::Arc<UiShared>,
50 ctx: OnceLock<PluginContext>,
51}
52
53impl UiPlugin {
54 pub fn new(shared: std::sync::Arc<UiShared>) -> Self {
56 Self {
57 shared,
58 ctx: OnceLock::new(),
59 }
60 }
61
62 fn refresh(&self) {
64 let Some(ctx) = self.ctx.get() else {
65 return; };
67 self.run_cmds(ctx);
68 match ctx.ask_service::<ListReq, Vec<PlugRow>>(SVC_LIST, ListReq, Duration::from_secs(2)) {
69 Ok(rows) => {
70 let mut services = Vec::with_capacity(rows.len());
71 for r in &rows {
72 let ids = ctx
73 .services_of(&r.name)
74 .map(|v| v.iter().map(|s| s.id.clone()).collect::<Vec<_>>())
75 .unwrap_or_default();
76 services.push((r.name.clone(), ids));
77 }
78 self.shared.set_snapshot(BusSnap { rows, services });
79 }
80 Err(e) => self.shared.log_line(format!("[ui] svctl.list 失败: {e:?}")),
81 }
82 let orders = ctx
84 .ask_service::<GetAllOrdersReq, Vec<OrderData>>(
85 "oms.get_all_orders",
86 GetAllOrdersReq,
87 Duration::from_secs(2),
88 )
89 .unwrap_or_default();
90 let trades = ctx
91 .ask_service::<GetAllTradesReq, Vec<TradeData>>(
92 "oms.get_all_trades",
93 GetAllTradesReq,
94 Duration::from_secs(2),
95 )
96 .unwrap_or_default();
97 let positions = ctx
98 .ask_service::<GetAllPositionsReq, Vec<PositionData>>(
99 "oms.get_all_positions",
100 GetAllPositionsReq,
101 Duration::from_secs(2),
102 )
103 .unwrap_or_default();
104 let accounts = ctx
105 .ask_service::<GetAllAccountsReq, Vec<AccountData>>(
106 "oms.get_all_accounts",
107 GetAllAccountsReq,
108 Duration::from_secs(2),
109 )
110 .unwrap_or_default();
111 self.shared.set_trade_snap(crate::state::TradeSnap {
112 orders,
113 trades,
114 positions,
115 accounts,
116 });
117 }
118
119 fn run_cmds(&self, ctx: &PluginContext) {
121 for cmd in self.shared.drain_cmds() {
122 match cmd {
123 crate::state::UiCmd::Start { factory } => {
124 match ctx.ask_service::<StartReq, StartRes>(
125 SVC_START,
126 StartReq {
127 factory: factory.clone(),
128 },
129 Duration::from_secs(3),
130 ) {
131 Ok(StartRes::Started { name }) => {
132 self.shared
133 .log_line(format!("[ops] svctl.start {factory} → {name} 上线"));
134 }
135 Ok(StartRes::Rejected { reason }) => {
136 self.shared
137 .log_line(format!("[ops] start {factory} 被拒: {reason}"));
138 }
139 Err(e) => {
140 self.shared
141 .log_line(format!("[ops] start {factory} 失败: {e:?}"));
142 }
143 }
144 }
145 crate::state::UiCmd::Remove { name } => {
146 match ctx.ask_service::<RemoveReq, RemoveRes>(
147 SVC_REMOVE,
148 RemoveReq { name: name.clone() },
149 Duration::from_secs(3),
150 ) {
151 Ok(RemoveRes::Removed { name }) => {
152 self.shared
153 .log_line(format!("[ops] svctl.remove {name} 完成"));
154 }
155 Ok(RemoveRes::NoSuchName { name }) => {
156 self.shared
157 .log_line(format!("[ops] remove {name} 不存在/不可摘"));
158 }
159 Err(e) => {
160 self.shared
161 .log_line(format!("[ops] remove {name} 失败: {e:?}"));
162 }
163 }
164 }
165 crate::state::UiCmd::Reload {
166 interval,
167 symbol,
168 bars,
169 } => {
170 match ctx.ask_service::<ReloadReq, usize>(
172 "dm.reload",
173 ReloadReq {
174 interval,
175 symbol: symbol.clone(),
176 bars: bars.clone(),
177 size: Some(bars.len()),
178 push: true,
179 },
180 Duration::from_secs(5),
181 ) {
182 Ok(n) => {
183 self.shared.market().note_reload();
184 self.shared.log_line(format!(
185 "[ops] dm.reload {symbol}@{interval}s 回灌 {n} 条"
186 ));
187 }
188 Err(e) => {
189 self.shared
190 .log_line(format!("[ops] dm.reload {symbol} 失败: {e:?}"));
191 }
192 }
193 }
194 crate::state::UiCmd::SendOrder { req, gateway, mode } => {
195 match ctx.ask_service::<SubmitOrderReq, SubmitReply>(
199 "oms.submit_order",
200 SubmitOrderReq {
201 req: req.clone(),
202 gateway_name: gateway.clone(),
203 mode,
204 },
205 Duration::from_secs(10),
206 ) {
207 Ok(reply) => {
208 let tag = match reply.status {
209 SubmitStatus::Sent => "SENT",
210 SubmitStatus::Partial => "PARTIAL",
211 SubmitStatus::Rejected => "REJECTED",
212 };
213 for leg in &reply.legs {
214 match (leg.ok, &leg.en_orderid, &leg.reason) {
215 (true, Some(id), _) => self.shared.log_line(format!(
216 "[ops] submit {} {} {}x{} → {id} (parent {})",
217 leg.req.symbol,
218 leg.req.direction.as_str(),
219 leg.req.offset.as_str(),
220 leg.req.volume,
221 reply.parent_id
222 )),
223 _ => self.shared.log_line(format!(
224 "[ops] submit {} {} {}x{} 被拒: {} (parent {})",
225 leg.req.symbol,
226 leg.req.direction.as_str(),
227 leg.req.offset.as_str(),
228 leg.req.volume,
229 leg.reason.as_deref().unwrap_or("?"),
230 reply.parent_id
231 )),
232 }
233 }
234 if reply.legs.is_empty() {
235 self.shared.log_line(format!(
236 "[ops] submit {} {}x{} {tag}:可平不足零腿 (parent {})",
237 req.symbol,
238 req.offset.as_str(),
239 req.volume,
240 reply.parent_id
241 ));
242 }
243 }
244 Err(e) => {
245 self.shared
246 .log_line(format!("[ops] oms.submit_order 失败: {e:?}"));
247 }
248 }
249 }
250 crate::state::UiCmd::CancelOrder { orderid, gateway } => {
251 match ctx.ask_service::<OmsCancelOrderReq, OmsCancelReply>(
255 "oms.cancel_order",
256 OmsCancelOrderReq {
257 en_orderid: format!("{gateway}.{orderid}"),
258 gateway_name: gateway.clone(),
259 },
260 Duration::from_secs(10),
261 ) {
262 Ok(OmsCancelReply { ok: true, msg }) => {
263 self.shared.log_line(format!("[ops] {msg}"));
264 }
265 Ok(OmsCancelReply { ok: false, msg }) => {
266 self.shared.log_line(format!("[ops] 撤单被拒: {msg}"));
267 }
268 Err(e) => {
269 self.shared
270 .log_line(format!("[ops] oms.cancel_order 失败: {e:?}"));
271 }
272 }
273 }
274 crate::state::UiCmd::QueryRate => {
275 match ctx.ask_service::<crate::sim::QueryRateReq, crate::sim::QueryRateReply>(
276 "sim.query_rate",
277 crate::sim::QueryRateReq,
278 Duration::from_secs(2),
279 ) {
280 Ok(r) => {
281 self.shared.log_line(format!(
282 "[ops] sim.query_rate used={}/{} 余{}",
283 r.used, r.limit, r.remaining
284 ));
285 }
286 Err(e) => {
287 self.shared
288 .log_line(format!("[ops] sim.query_rate 失败: {e:?}"));
289 }
290 }
291 }
292 crate::state::UiCmd::CtpConfigure(form) => {
293 match ctx.ask_service::<ConfigureReq, ConfigureReply>(
295 "ctp.configure",
296 form.to_configure_req(),
297 Duration::from_secs(5),
298 ) {
299 Ok(ConfigureReply { ok: true, msg }) => {
300 self.shared
301 .log_line(format!("[ops] ctp.configure 受理:{msg}"));
302 }
303 Ok(ConfigureReply { ok: false, msg }) => {
304 self.shared
305 .log_line(format!("[ops] ctp.configure 被拒:{msg}"));
306 }
307 Err(e) => {
308 self.shared
309 .log_line(format!("[ops] ctp.configure 失败: {e:?}"));
310 }
311 }
312 }
313 crate::state::UiCmd::CtpSubscribe { symbols } => {
314 match ctx.ask_service::<CtpSubscribeReq, CtpSubscribeReply>(
315 "ctp.subscribe",
316 CtpSubscribeReq {
317 symbols: symbols.clone(),
318 },
319 Duration::from_secs(5),
320 ) {
321 Ok(CtpSubscribeReply { ok: true, msg }) => {
322 self.shared.log_line(format!(
323 "[ops] ctp.subscribe {} 受理:{}",
324 symbols.join(","),
325 msg
326 ));
327 }
328 Ok(CtpSubscribeReply { ok: false, msg }) => {
329 self.shared.log_line(format!(
330 "[ops] ctp.subscribe {} 被拒:{}",
331 symbols.join(","),
332 msg
333 ));
334 }
335 Err(e) => {
336 self.shared
337 .log_line(format!("[ops] ctp.subscribe 失败: {e:?}"));
338 }
339 }
340 }
341 }
342 }
343 }
344}
345
346impl Plugin for UiPlugin {
347 fn name(&self) -> &str {
348 UI_NAME
349 }
350
351 fn deps(&self) -> &[&str] {
352 &[] }
354
355 fn routes(&self) -> Vec<std::borrow::Cow<'static, str>> {
356 vec![
357 std::borrow::Cow::Borrowed(EVENT_LOG),
358 std::borrow::Cow::Borrowed(EVENT_PLUG_UP),
359 std::borrow::Cow::Borrowed(EVENT_PLUG_DOWN),
360 std::borrow::Cow::Borrowed(EVENT_BAR),
361 std::borrow::Cow::Borrowed(EVENT_BAR_UPDATE),
362 std::borrow::Cow::Borrowed(EVENT_TICK),
363 std::borrow::Cow::Borrowed(EVENT_CONTRACT),
364 std::borrow::Cow::Borrowed("eContractFinish"),
365 std::borrow::Cow::Borrowed(EVENT_TIMER),
366 ]
367 }
368
369 fn mailbox_cap(&self) -> usize {
370 1024
371 }
372
373 fn version(&self) -> std::borrow::Cow<'static, str> {
374 std::borrow::Cow::Borrowed(env!("CARGO_PKG_VERSION"))
375 }
376
377 fn services(&self) -> Vec<ServiceDesc> {
378 Vec::new() }
380
381 fn on_init(&self, ctx: &PluginContext) {
382 let _ = self.ctx.set(ctx.clone());
383 }
384
385 fn on_start(&self) {
386 self.shared.log_line("[ui] 界面插件上线");
387 }
388
389 fn on_stop(&self) {
390 self.shared.log_line("[ui] 界面插件下线");
391 }
392
393 fn on_mail(&self, event: &ek_event::Event) {
394 let ty = event.event_type.as_str();
395 if ty == EVENT_LOG {
396 if let Some(d) = event.data.downcast_ref::<LogData>() {
397 let tag = if d.is_warning_or_above() {
398 "WARN"
399 } else {
400 "INFO"
401 };
402 self.shared
403 .log_line(format!("[{tag}][{}] {}", d.gateway_name, d.msg));
404 } else {
405 self.shared.log_line("[eLog] 载荷类型异常(非 LogData)");
406 }
407 return;
408 }
409 if ty == EVENT_PLUG_UP || ty == EVENT_PLUG_DOWN {
410 if let Some(n) = event.data.downcast_ref::<PlugNotice>() {
411 let verb = if ty == EVENT_PLUG_UP {
412 "上线"
413 } else {
414 "下线"
415 };
416 self.shared
417 .log_line(format!("[host] 插件{verb} {} v{}", n.name, n.version));
418 }
419 self.shared.bump();
420 return;
421 }
422 if ty == EVENT_BAR {
423 if let Some(f) = event.data.downcast_ref::<ek_dm::wire::BarsFrame>() {
424 self.shared.market().on_frame(f);
425 }
426 self.shared.bump();
427 return;
428 }
429 if ty == EVENT_BAR_UPDATE {
430 if let Some(b) = event.data.downcast_ref::<ek_data::bar::BarData>() {
431 self.shared.market().on_update(b);
432 }
433 self.shared.bump();
434 return;
435 }
436 if ty == EVENT_TICK {
437 if let Some(t) = event.data.downcast_ref::<ek_data::tick::TickData>() {
438 self.shared.update_tick(t);
439 }
440 return;
441 }
442 if ty == EVENT_CONTRACT {
443 if let Some(c) = event.data.downcast_ref::<ek_data::contract::ContractData>() {
444 self.shared.update_contract(c);
445 }
446 return;
447 }
448 if ty == "eContractFinish" {
449 let gw = event
450 .data
451 .downcast_ref::<String>()
452 .cloned()
453 .unwrap_or_else(|| "?".to_string());
454 self.shared
455 .log_line(format!("[ctp] 合约流完成 {gw}(查询合约窗可勾选订阅)"));
456 return;
457 }
458 if ty == EVENT_TIMER {
459 self.refresh();
460 }
461 }
462}