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