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