1use std::collections::{BTreeMap, VecDeque};
7use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
8use std::sync::{Arc, Mutex};
9
10use ek_data::bar::BarData;
11use ek_dm::wire::BarsFrame;
12use ek_station::wire::PlugRow;
13
14pub const RING_CAP: usize = 512;
16
17pub(crate) fn beijing_tz() -> chrono::FixedOffset {
22 chrono::FixedOffset::east_opt(8 * 3600).expect("+08 offset is always valid")
23}
24
25#[derive(Debug, Clone)]
29pub struct LogEntry {
30 pub at: String,
32 pub text: String,
34}
35
36#[derive(Debug)]
38pub struct LogRing {
39 lines: VecDeque<LogEntry>,
40 cap: usize,
41 spilled: u64,
42}
43
44impl Default for LogRing {
45 fn default() -> Self {
46 Self::new(RING_CAP)
47 }
48}
49
50impl LogRing {
51 pub fn new(cap: usize) -> Self {
53 Self {
54 lines: VecDeque::with_capacity(cap),
55 cap,
56 spilled: 0,
57 }
58 }
59
60 pub fn push(&mut self, line: impl Into<String>) {
62 if self.cap == 0 {
63 self.spilled += 1;
64 return;
65 }
66 if self.lines.len() == self.cap {
67 self.lines.pop_front();
68 self.spilled += 1;
69 }
70 let at = chrono::Utc::now()
71 .with_timezone(&beijing_tz())
72 .format("%H:%M:%S")
73 .to_string();
74 self.lines.push_back(LogEntry {
75 at,
76 text: line.into(),
77 });
78 }
79
80 pub fn len(&self) -> usize {
82 self.lines.len()
83 }
84
85 pub fn is_empty(&self) -> bool {
87 self.lines.is_empty()
88 }
89
90 pub fn spilled(&self) -> u64 {
92 self.spilled
93 }
94
95 pub fn snapshot(&self) -> Vec<String> {
97 self.lines.iter().map(|e| e.text.clone()).collect()
98 }
99
100 pub fn entries(&self) -> Vec<LogEntry> {
102 self.lines.iter().cloned().collect()
103 }
104
105 pub fn clear(&mut self) {
107 self.lines.clear();
108 }
109}
110
111#[derive(Debug, Clone, Default)]
113pub struct BusSnap {
114 pub rows: Vec<PlugRow>,
116 pub services: Vec<(String, Vec<String>)>,
118}
119
120#[derive(Debug, Clone, PartialEq)]
122pub enum UiCmd {
123 Start { factory: String, name: String },
126 Remove { name: String },
128 Reload {
130 interval: i64,
131 symbol: String,
132 bars: Vec<BarData>,
133 },
134 SendOrder {
140 req: ek_data::order_request::OrderRequest,
141 gateway: String,
142 mode: ek_oms::LockMode,
143 },
144 CancelOrder { orderid: String, gateway: String },
148 CtpConfigure { gateway: String, form: CtpForm },
152 Connect { gateway: String, form: CtpForm },
157 CtpSubscribe {
160 gateway: String,
161 symbols: Vec<String>,
162 },
163 Disconnect { gateway: String },
167}
168
169pub const KLINE_CELL_CAP: usize = 4096;
171
172pub const MAX_ACCOUNTS: usize = 8;
175
176pub const QUERY_REJECT_STREAK_LIMIT: u32 = 3;
181
182pub const ACCOUNT_STALE_SECS: i64 = 30;
187
188pub const STATUS_POLL_TICKS: u32 = 2;
193
194#[derive(Debug, Clone, Copy, PartialEq, Eq)]
196pub enum ConfirmStep {
197 Armed,
198 Fired,
199 Switched,
200}
201
202#[must_use]
205pub fn is_quarantined(state: &str) -> bool {
206 state.contains("Quarantin")
207}
208
209#[derive(Debug, Default)]
213pub struct MarketStore {
214 cells: Mutex<BTreeMap<(String, i64), Vec<BarData>>>,
215 frames: AtomicU64,
217 updates: AtomicU64,
219 reloads: AtomicU64,
221}
222
223#[derive(Debug, Clone, PartialEq)]
225pub struct CellView {
226 pub symbol: String,
227 pub interval: i64,
228 pub bars: Vec<BarData>,
229}
230
231impl MarketStore {
232 pub fn on_frame(&self, frame: &BarsFrame) {
234 let mut cells = self.cells.lock().unwrap();
235 for b in &frame.bars {
236 let key = (b.symbol.clone(), frame.interval);
237 let cell = cells.entry(key).or_default();
238 match cell.last() {
239 Some(last) if b.datetime < last.datetime => continue,
240 Some(last) if b.datetime == last.datetime => {
241 let slot = cell.last_mut().expect("len>1");
242 *slot = b.clone();
243 }
244 _ => {
245 cell.push(b.clone());
246 if cell.len() > KLINE_CELL_CAP {
247 cell.remove(0);
248 }
249 }
250 }
251 }
252 self.frames.fetch_add(1, Ordering::AcqRel);
253 }
254
255 pub fn on_update(&self, b: &BarData) {
257 let mut cells = self.cells.lock().unwrap();
258 let iv = b
259 .interval
260 .unwrap_or(ek_data::constant::Interval::MINUTE)
261 .seconds();
262 let key = (b.symbol.clone(), iv);
263 let cell = cells.entry(key).or_default();
264 match cell.last() {
265 Some(last) if b.datetime == last.datetime => {
266 let slot = cell.last_mut().expect("len>1");
267 *slot = b.clone();
268 }
269 Some(last) if b.datetime < last.datetime => {}
270 _ => {
271 cell.push(b.clone());
272 if cell.len() > KLINE_CELL_CAP {
273 cell.remove(0);
274 }
275 }
276 }
277 self.updates.fetch_add(1, Ordering::AcqRel);
278 }
279
280 pub fn note_reload(&self) {
282 self.reloads.fetch_add(1, Ordering::AcqRel);
283 }
284
285 pub fn cells(&self) -> Vec<CellView> {
287 self.cells
288 .lock()
289 .unwrap()
290 .iter()
291 .map(|((sym, iv), bars)| CellView {
292 symbol: sym.clone(),
293 interval: *iv,
294 bars: bars.clone(),
295 })
296 .collect()
297 }
298
299 pub fn counts(&self) -> (u64, u64, u64) {
301 (
302 self.frames.load(Ordering::Acquire),
303 self.updates.load(Ordering::Acquire),
304 self.reloads.load(Ordering::Acquire),
305 )
306 }
307}
308
309#[derive(Debug, Default, Clone, PartialEq)]
312pub struct ReloadForm {
313 pub interval: String,
314 pub symbol: String,
315 pub csv: String,
316}
317
318pub fn parse_gateway(raw: &str, fallback: Option<&str>) -> Result<String, String> {
325 let g = raw.trim();
326 if !g.is_empty() {
327 if g.chars().any(|c| c.is_whitespace()) {
328 return Err(format!("网关解析: 含空白:{raw:?}"));
329 }
330 return Ok(g.to_string());
331 }
332 match fallback.map(|f| f.trim()).filter(|f| !f.is_empty()) {
333 Some(f) => Ok(f.to_string()),
334 None => Err("网关解析: 未选择账户(先在账户面板添加账户)".to_string()),
335 }
336}
337
338#[derive(Debug, Clone, PartialEq)]
341pub struct CtpForm {
342 pub broker_id: String,
343 pub td_addr: String,
344 pub td_user: String,
345 pub td_password: String,
346 pub td_auth_code: String,
347 pub td_app_id: String,
348 pub md_addr: String,
349 pub md_user: String,
350 pub md_password: String,
351 pub symbols: String,
353 pub accept_futures: bool,
356 pub accept_options: bool,
357}
358
359impl Default for CtpForm {
362 fn default() -> Self {
363 Self {
364 broker_id: String::new(),
365 td_addr: String::new(),
366 td_user: String::new(),
367 td_password: String::new(),
368 td_auth_code: String::new(),
369 td_app_id: String::new(),
370 md_addr: String::new(),
371 md_user: String::new(),
372 md_password: String::new(),
373 symbols: String::new(),
374 accept_futures: true,
375 accept_options: false,
376 }
377 }
378}
379
380impl CtpForm {
381 #[must_use]
385 pub fn product_classes(&self) -> Vec<String> {
386 let mut codes = Vec::new();
387 if self.accept_futures {
388 codes.push("1".to_string());
389 }
390 if self.accept_options {
391 codes.push("2".to_string());
392 }
393 codes
394 }
395
396 pub fn to_configure_req(&self) -> ek_ctp::plugin::ConfigureReq {
398 ek_ctp::plugin::ConfigureReq {
399 broker_id: self.broker_id.trim().to_string(),
400 td_addr: self.td_addr.trim().to_string(),
401 td_user: self.td_user.trim().to_string(),
402 td_password: self.td_password.clone(),
403 td_auth_code: self.td_auth_code.trim().to_string(),
404 td_app_id: self.td_app_id.trim().to_string(),
405 md_addr: self.md_addr.trim().to_string(),
406 md_user: self.md_user.trim().to_string(),
407 md_password: self.md_password.clone(),
408 product_classes: self.product_classes(),
409 }
410 }
411
412 pub fn parse_symbols(&self) -> Result<Vec<String>, String> {
414 let list: Vec<String> = self
415 .symbols
416 .split([',', ',', ' '])
417 .map(|s| s.trim().to_string())
418 .filter(|s| !s.is_empty())
419 .collect();
420 if list.is_empty() {
421 return Err("订阅解析: 标的位空".to_string());
422 }
423 Ok(list)
424 }
425
426 pub fn validate(&self) -> Result<(), String> {
435 fn front(name: &str, v: &str) -> Result<(), String> {
436 let v = v.trim();
437 let Some(rest) = v.strip_prefix("tcp://") else {
438 return Err(format!("{name} 形制应为 tcp://host:port(当前 {:?})", v));
439 };
440 let Some((host, port)) = rest.rsplit_once(':') else {
441 return Err(format!("{name} 缺端口:{name} {:?}", v));
442 };
443 if host.trim().is_empty() {
444 return Err(format!("{name} 主机位空:{}", v));
445 }
446 let port: u32 = port
447 .parse()
448 .map_err(|_| format!("{name} 端口不是数字:{}", v))?;
449 if port == 0 || port > 65535 {
450 return Err(format!("{name} 端口越界(1..=65535):{}", v));
451 }
452 Ok(())
453 }
454 if self.broker_id.trim().is_empty() {
455 return Err("broker 位空".to_string());
456 }
457 if self.td_user.trim().is_empty() {
458 return Err("交易账号位空".to_string());
459 }
460 front("td 前置", &self.td_addr)?;
461 if !self.md_addr.trim().is_empty() {
462 front("md 前置", &self.md_addr)?;
463 }
464 if self.td_app_id.trim().is_empty() {
465 return Err("AppID 位空(CTP 登录必需)".to_string());
466 }
467 if self.td_auth_code.trim().is_empty() {
468 return Err("AuthCode 位空(CTP 登录必需)".to_string());
469 }
470 Ok(())
471 }
472}
473
474#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
481pub enum AccountStage {
482 #[default]
484 NotAssembled,
485 Assembling,
487 Assembled,
489 AssembleFailed,
491}
492
493impl AccountStage {
494 #[must_use]
496 pub fn label(self) -> &'static str {
497 match self {
498 AccountStage::NotAssembled => "未装配",
499 AccountStage::Assembling => "装配中…",
500 AccountStage::Assembled => "已装配·登录态未知",
503 AccountStage::AssembleFailed => "装配失败",
504 }
505 }
506}
507
508#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
513pub enum LoginState {
514 #[default]
519 Unknown,
520 Online,
522 LoginFailed,
524 FrontDown,
526 QueryStarved,
528 Stopped,
530}
531
532impl LoginState {
533 #[must_use]
535 pub fn label(self) -> &'static str {
536 match self {
537 LoginState::Unknown => "登录态未知",
538 LoginState::Online => "在线",
539 LoginState::LoginFailed => "登录失败",
540 LoginState::FrontDown => "掉线",
541 LoginState::QueryStarved => "查询断供",
542 LoginState::Stopped => "已断开",
543 }
544 }
545
546 #[must_use]
548 pub fn is_alarm(self) -> bool {
549 matches!(self, LoginState::LoginFailed | LoginState::FrontDown)
550 }
551}
552
553fn count_note(label: &str, n: u32) -> Option<String> {
555 (n > 0).then(|| format!("{label} {n} 次"))
556}
557
558fn brief_counter_reason(full: &str) -> Option<String> {
565 let tail = full.rfind("信息:").map(|i| &full[i + "信息:".len()..])?;
566 let body = tail.trim().trim_start_matches("CTP:").trim();
567 if body.is_empty() {
568 return None;
569 }
570 let code = full
571 .rfind("代码:")
572 .map(|i| {
573 let rest = &full[i + "代码:".len()..];
574 let end = rest.find(',').unwrap_or(rest.len());
575 rest[..end].trim().to_string()
576 })
577 .filter(|c| !c.is_empty() && c.parse::<i64>().is_ok());
578 Some(match code {
579 Some(c) => format!("{body}({c})"),
580 None => body.to_string(),
581 })
582}
583
584#[derive(Debug, Clone, PartialEq, Default)]
590pub struct AccountRow {
591 pub gateway: String,
593 pub alias: String,
595 pub broker_id: String,
596 pub account: String,
597 pub md_source: bool,
599 pub form: CtpForm,
601 pub stage: AccountStage,
603 pub fail_note: Option<String>,
605 pub effective_classes: Vec<String>,
610 pub connected: bool,
613 pub login: LoginState,
617 pub status_note: Option<String>,
620 pub login_brief: Option<String>,
627}
628
629impl AccountRow {
630 pub fn derive(form: &CtpForm) -> Result<Self, String> {
635 let broker = form.broker_id.trim();
636 let account = form.td_user.trim();
637 let gateway = ek_ctp::derived_gateway_name(broker, account).ok_or_else(|| {
638 "账户: broker/账号 不能为空且只能含字母数字(且派生名 ≤32)".to_string()
639 })?;
640 Ok(Self {
641 gateway,
642 alias: String::new(),
643 broker_id: broker.to_string(),
644 account: account.to_string(),
645 md_source: false,
646 form: form.clone(),
647 stage: AccountStage::NotAssembled,
648 fail_note: None,
649 effective_classes: Vec::new(),
650 connected: false,
651 login: LoginState::Unknown,
652 status_note: None,
653 login_brief: None,
654 })
655 }
656
657 #[must_use]
663 pub fn status_label(&self) -> String {
664 if self.stage == AccountStage::Assembled {
665 match self.login {
666 LoginState::Unknown => "已装配·登录态未知".to_string(),
667 other => format!("已装配·{}", other.label()),
668 }
669 } else {
670 self.stage.label().to_string()
671 }
672 }
673
674 pub fn apply_status(
687 &mut self,
688 st: &ek_ctp::plugin::GatewayStatus,
689 now_ms: i64,
690 reject_limit: u32,
691 stale_secs: i64,
692 ) {
693 let td_side = !self.form.td_addr.trim().is_empty();
694 let (login, note) = if td_side {
695 if st.td_login_failed && !st.td_logged_in {
696 (
697 LoginState::LoginFailed,
698 st.td_last_error
699 .clone()
700 .or_else(|| Some("登录失败(网关未回带原因)".to_string())),
701 )
702 } else if st.td_logged_in {
703 if st.td_query_reject_streak >= reject_limit {
704 (
705 LoginState::QueryStarved,
706 Some(format!("查询连续被拒 {} 次", st.td_query_reject_streak)),
707 )
708 } else if let Some(last) = st.td_last_account_ms {
709 let idle_ms = now_ms.saturating_sub(last);
710 if idle_ms > stale_secs.saturating_mul(1_000) {
711 (
712 LoginState::QueryStarved,
713 Some(format!("资金回报已 {}s 未更新", idle_ms / 1_000)),
714 )
715 } else {
716 (LoginState::Online, count_note("掉线", st.td_disconnects))
717 }
718 } else {
719 let since_login_ms = st.td_last_login_ok_ms.map(|ok| now_ms.saturating_sub(ok));
726 match since_login_ms {
727 Some(ms) if ms > stale_secs.saturating_mul(1_000) => (
728 LoginState::QueryStarved,
729 Some(format!("资金回报从未到手(登录已 {}s)", ms / 1_000)),
730 ),
731 _ => (LoginState::Online, Some("资金回报尚未到手".to_string())),
732 }
733 }
734 } else if self.login == LoginState::Stopped {
735 let extra = if st.td_disconnects > 0 {
736 format!("(此前掉线 {} 次)", st.td_disconnects)
737 } else {
738 String::new()
739 };
740 (
741 LoginState::Stopped,
742 Some(format!("会话已停{extra};『连接』可恢复")),
743 )
744 } else if st.td_disconnects > 0 {
745 (
746 LoginState::FrontDown,
747 Some(format!("掉线 {} 次", st.td_disconnects)),
748 )
749 } else {
750 (LoginState::Unknown, st.td_last_error.clone())
751 }
752 } else if st.md_logged_in {
753 (
754 LoginState::Online,
755 count_note("掉线", st.md_disconnects)
756 .or_else(|| Some("纯行情账户(无 TD 会话)".to_string())),
757 )
758 } else if self.login == LoginState::Stopped {
759 (
760 LoginState::Stopped,
761 Some("会话已停;『连接』可恢复".to_string()),
762 )
763 } else if st.md_disconnects > 0 {
764 (
765 LoginState::FrontDown,
766 Some(format!("掉线 {} 次", st.md_disconnects)),
767 )
768 } else {
769 (LoginState::Unknown, st.md_last_error.clone())
770 };
771 let note = if td_side && st.td_instrument_rejects > 0 {
776 let add = format!("合约查询被拒 {} 次", st.td_instrument_rejects);
777 match note {
778 Some(base) if base.contains("合约查询被拒") => Some(base),
780 Some(base) => Some(format!("{base};{add}")),
781 None => Some(add),
782 }
783 } else {
784 note
785 };
786 self.login = login;
787 self.status_note = note;
788 self.login_brief = if login.is_alarm() {
791 st.td_last_error
792 .as_deref()
793 .or(st.md_last_error.as_deref())
794 .and_then(brief_counter_reason)
795 } else {
796 None
797 };
798 }
799
800 #[must_use]
802 pub fn display_name(&self) -> String {
803 let a = self.alias.trim();
804 if a.is_empty() {
805 self.account.clone()
806 } else {
807 a.to_string()
808 }
809 }
810}
811
812#[derive(Debug, Clone, PartialEq, Default)]
814pub struct TradeSnap {
815 pub orders: Vec<ek_data::order::OrderData>,
816 pub trades: Vec<ek_data::trade::TradeData>,
817 pub positions: Vec<ek_data::position::PositionData>,
818 pub accounts: Vec<ek_data::account::AccountData>,
820}
821
822pub fn parse_reload_csv(form: &ReloadForm) -> Result<(i64, String, Vec<BarData>), String> {
825 let interval: i64 = form
826 .interval
827 .trim()
828 .parse()
829 .map_err(|_| format!("回灌 解析: interval 非整数:{:?}", form.interval))?;
830 let symbol = form.symbol.trim().to_string();
831 if symbol.is_empty() {
832 return Err("回灌解析: symbol 空".to_string());
833 }
834 if form.csv.trim().is_empty() {
835 return Err("回灌解析: csv 空".to_string());
836 }
837 let mut bars = Vec::new();
838 for (i, raw) in form.csv.lines().enumerate() {
839 let line = raw.trim();
840 if line.is_empty() || line.starts_with('#') {
841 continue;
842 }
843 let parts: Vec<&str> = line.split(',').map(str::trim).collect();
844 if parts.len() != 6 {
845 return Err(format!("回灌解析: 第 {} 列数 ≠ 6", i + 1));
846 }
847 let num = |s: &str| -> Result<f64, String> {
848 s.parse::<f64>()
849 .map_err(|_| format!("回灌解析: 第 {} 列数值坏:{:?}", i + 1, s))
850 };
851 let dt_s: i64 = parts[0]
852 .parse()
853 .map_err(|_| format!("回灌解析: 第 {} 列 dt 坏:{:?}", i + 1, parts[0]))?;
854 bars.push(BarData {
855 symbol: symbol.clone(),
856 datetime: dt_s * 1_000_000_000,
857 interval: Some(
858 ek_dm::wire::interval_of_seconds(interval)
859 .unwrap_or(ek_data::constant::Interval::MINUTE),
860 ),
861 open_price: num(parts[1])?,
862 high_price: num(parts[2])?,
863 low_price: num(parts[3])?,
864 close_price: num(parts[4])?,
865 volume: num(parts[5])?,
866 ..BarData::default()
867 });
868 }
869 if bars.is_empty() {
870 return Err("回灌解析: 无有效行".to_string());
871 }
872 Ok((interval, symbol, bars))
873}
874
875#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
880pub enum TopLeftTab {
881 #[default]
882 Orders,
883 Active,
884}
885
886impl TopLeftTab {
887 pub fn label(self) -> &'static str {
888 match self {
889 Self::Orders => "委托",
890 Self::Active => "未成交",
891 }
892 }
893}
894
895#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
897pub enum BottomRightTab {
898 #[default]
899 Account,
900 Logs,
901}
902
903impl BottomRightTab {
904 pub fn label(self) -> &'static str {
905 match self {
906 Self::Account => "资金",
907 Self::Logs => "日志",
908 }
909 }
910}
911
912#[derive(Debug, Clone, Copy, PartialEq)]
915pub struct DataSplit {
916 pub col: f32,
918 pub row: f32,
920}
921
922impl Default for DataSplit {
923 fn default() -> Self {
924 Self {
925 col: crate::splitters::DATA_SPLIT_DEFAULT,
926 row: crate::splitters::DATA_SPLIT_DEFAULT,
927 }
928 }
929}
930
931pub const MARKET_ORDER: [ek_data::constant::Exchange; 6] = [
934 ek_data::constant::Exchange::SHFE,
935 ek_data::constant::Exchange::DCE,
936 ek_data::constant::Exchange::CZCE,
937 ek_data::constant::Exchange::CFFEX,
938 ek_data::constant::Exchange::INE,
939 ek_data::constant::Exchange::GFEX,
940];
941
942#[derive(Debug, Default)]
952pub struct TickStore {
953 ticks: BTreeMap<String, ek_data::tick::TickData>,
954}
955
956impl TickStore {
957 pub fn put(&mut self, t: &ek_data::tick::TickData) {
960 self.ticks
961 .insert(format!("{}.{}", t.exchange.as_str(), t.symbol), t.clone());
962 }
963
964 pub fn rows(&self) -> Vec<ek_data::tick::TickData> {
966 let mut v: Vec<ek_data::tick::TickData> = self.ticks.values().cloned().collect();
967 v.sort_by(|a, b| {
968 (a.exchange.as_str().to_string(), a.symbol.clone())
969 .cmp(&(b.exchange.as_str().to_string(), b.symbol.clone()))
970 });
971 v
972 }
973
974 pub fn groups(&self) -> Vec<(String, Vec<ek_data::tick::TickData>)> {
976 let mut out: Vec<(String, Vec<ek_data::tick::TickData>)> = Vec::new();
977 for t in self.rows() {
978 let ex = t.exchange.as_str().to_string();
979 match out.last_mut() {
980 Some((e, v)) if *e == ex => v.push(t),
981 _ => out.push((ex, vec![t])),
982 }
983 }
984 out
985 }
986
987 pub fn len(&self) -> usize {
988 self.ticks.len()
989 }
990
991 pub fn is_empty(&self) -> bool {
992 self.ticks.is_empty()
993 }
994}
995
996#[derive(Debug, Clone, PartialEq)]
998pub struct OrderForm {
999 pub exchange: String,
1001 pub symbol: String,
1003 pub direction: ek_data::constant::Direction,
1004 pub offset: ek_data::constant::Offset,
1005 pub order_type: ek_data::constant::OrderType,
1006 pub price: String,
1008 pub volume: String,
1009 pub gateway: String,
1011 pub follow_price: bool,
1013 pub auto_split: bool,
1016}
1017
1018impl Default for OrderForm {
1019 fn default() -> Self {
1020 Self {
1021 exchange: "SHFE".to_string(),
1022 symbol: String::new(),
1023 direction: ek_data::constant::Direction::LONG,
1024 offset: ek_data::constant::Offset::OPEN,
1025 order_type: ek_data::constant::OrderType::LIMIT,
1026 price: String::new(),
1027 volume: "1".to_string(),
1028 gateway: String::new(),
1029 follow_price: true,
1030 auto_split: true,
1031 }
1032 }
1033}
1034
1035impl OrderForm {
1036 pub fn to_request(&self) -> Result<ek_data::order_request::OrderRequest, String> {
1038 use std::str::FromStr;
1039 let symbol = self.symbol.trim();
1040 if symbol.is_empty() {
1041 return Err("下单: 代码为空".to_string());
1042 }
1043 if symbol.contains('.') {
1044 return Err("下单: 代码不带交易所前缀(交易所走下拉)".to_string());
1045 }
1046 let exchange = ek_data::constant::Exchange::from_str(self.exchange.trim())
1047 .map_err(|e| format!("下单: {e}"))?;
1048 let price: f64 = self
1049 .price
1050 .trim()
1051 .parse()
1052 .map_err(|_| format!("下单: 价格无效 {:?}", self.price))?;
1053 let volume: f64 = self
1054 .volume
1055 .trim()
1056 .parse()
1057 .map_err(|_| format!("下单: 数量无效 {:?}", self.volume))?;
1058 if self.order_type != ek_data::constant::OrderType::MARKET && price <= 0.0 {
1059 return Err("下单: 限价单价格须为正".to_string());
1060 }
1061 if volume <= 0.0 {
1062 return Err("下单: 数量须为正".to_string());
1063 }
1064 Ok(ek_data::order_request::OrderRequest {
1065 symbol: symbol.to_string(),
1066 exchange,
1067 direction: self.direction,
1068 order_type: self.order_type,
1069 volume,
1070 price,
1071 offset: self.offset,
1072 ..ek_data::order_request::OrderRequest::default()
1073 })
1074 }
1075}
1076
1077#[derive(Debug, Default)]
1083pub struct ContractStore {
1084 contracts: BTreeMap<String, ek_data::contract::ContractData>,
1085}
1086
1087impl ContractStore {
1088 pub fn put(&mut self, c: &ek_data::contract::ContractData) {
1089 self.contracts
1090 .insert(format!("{}.{}", c.exchange.as_str(), c.symbol), c.clone());
1091 }
1092
1093 pub fn query(&self, pattern: &str) -> Vec<ek_data::contract::ContractData> {
1095 let p = pattern.trim();
1096 self.contracts
1097 .values()
1098 .filter(|c| {
1099 p.is_empty()
1100 || c.symbol.contains(p)
1101 || c.exchange.as_str().eq_ignore_ascii_case(p)
1102 || c.name.contains(p)
1103 })
1104 .cloned()
1105 .collect()
1106 }
1107
1108 pub fn len(&self) -> usize {
1109 self.contracts.len()
1110 }
1111
1112 pub fn is_empty(&self) -> bool {
1113 self.contracts.is_empty()
1114 }
1115}
1116
1117#[derive(Debug, Default)]
1119pub struct UiShared {
1120 logs: Mutex<LogRing>,
1121 snap: Mutex<Arc<BusSnap>>,
1122 market: MarketStore,
1123 ctp_form: Mutex<CtpForm>,
1124 trade_snap: Mutex<Arc<TradeSnap>>,
1125 tick_store: Mutex<TickStore>,
1126 order_form: Mutex<OrderForm>,
1127 data_split: Mutex<DataSplit>,
1130 selected_exchange: Mutex<String>,
1132 topleft_tab: Mutex<TopLeftTab>,
1134 bottomright_tab: Mutex<BottomRightTab>,
1136 ops_window: AtomicBool,
1137 accounts_window: AtomicBool,
1140 contracts_window: AtomicBool,
1141 contract_store: Mutex<ContractStore>,
1142 contract_query: Mutex<String>,
1143 contract_selected: Mutex<Vec<String>>,
1144 kline_windows: Mutex<Vec<String>>,
1145 rev: AtomicU64,
1146 stop: AtomicBool,
1148 cmds: Mutex<VecDeque<UiCmd>>,
1150 pending_confirm: Mutex<Option<String>>,
1152 accounts: Mutex<Vec<AccountRow>>,
1154 account_edit_window: AtomicBool,
1156 editing_account: Mutex<Option<String>>,
1158 edit_alias: Mutex<String>,
1160 edit_error: Mutex<Option<String>>,
1162 md_source: Mutex<String>,
1164 subscriptions: Mutex<Vec<String>>,
1166 row_sel: Mutex<BTreeMap<String, String>>,
1169 table_sorts: Mutex<BTreeMap<String, Option<(usize, bool)>>>,
1173 status_tick: AtomicU32,
1177 status_thresholds: Mutex<Option<(u32, i64)>>,
1180 pending_alerts: Mutex<Vec<(crate::toast::AlertLevel, String)>>,
1182 toasts: crate::toast::ToastsCell,
1185 alerted: Mutex<std::collections::BTreeSet<String>>,
1187 exit_confirm_open: AtomicBool,
1189 exit_confirmed: AtomicBool,
1191}
1192
1193impl UiShared {
1194 pub fn new() -> Self {
1196 Self::default()
1197 }
1198
1199 pub fn update_tick(&self, t: &ek_data::tick::TickData) {
1203 self.tick_store.lock().unwrap().put(t);
1204 self.bump();
1205 }
1206
1207 pub fn tick_rows(&self) -> Vec<ek_data::tick::TickData> {
1209 self.tick_store.lock().unwrap().rows()
1210 }
1211
1212 pub fn tick_groups(&self) -> Vec<(String, Vec<ek_data::tick::TickData>)> {
1214 self.tick_store.lock().unwrap().groups()
1215 }
1216
1217 pub fn tick_count(&self) -> usize {
1219 self.tick_store.lock().unwrap().len()
1220 }
1221
1222 pub fn order_form(&self) -> OrderForm {
1224 self.order_form.lock().unwrap().clone()
1225 }
1226
1227 pub fn set_order_form(&self, f: OrderForm) {
1229 *self.order_form.lock().unwrap() = f;
1230 self.bump();
1231 }
1232
1233 pub fn selected_exchange(&self) -> String {
1237 let s = self.selected_exchange.lock().unwrap().clone();
1238 if MARKET_ORDER.iter().any(|e| e.as_str() == s) {
1239 s
1240 } else {
1241 MARKET_ORDER[0].as_str().to_string()
1242 }
1243 }
1244
1245 pub fn set_selected_exchange(&self, s: impl Into<String>) {
1247 *self.selected_exchange.lock().unwrap() = s.into();
1248 self.bump();
1249 }
1250
1251 pub fn topleft_tab(&self) -> TopLeftTab {
1253 *self.topleft_tab.lock().unwrap()
1254 }
1255
1256 pub fn set_topleft_tab(&self, t: TopLeftTab) {
1258 *self.topleft_tab.lock().unwrap() = t;
1259 self.bump();
1260 }
1261
1262 pub fn bottomright_tab(&self) -> BottomRightTab {
1264 *self.bottomright_tab.lock().unwrap()
1265 }
1266
1267 pub fn set_bottomright_tab(&self, t: BottomRightTab) {
1269 *self.bottomright_tab.lock().unwrap() = t;
1270 self.bump();
1271 }
1272
1273 pub fn data_split(&self) -> DataSplit {
1275 *self.data_split.lock().unwrap()
1276 }
1277
1278 pub fn set_data_split(&self, s: DataSplit) {
1281 let s = DataSplit {
1282 col: crate::splitters::clamp_frac(s.col),
1283 row: crate::splitters::clamp_frac(s.row),
1284 };
1285 let mut g = self.data_split.lock().unwrap();
1286 if g.col != s.col || g.row != s.row {
1287 *g = s;
1288 self.bump();
1289 }
1290 }
1291
1292 pub fn reset_layout(&self) {
1296 *self.data_split.lock().unwrap() = DataSplit::default();
1297 *self.selected_exchange.lock().unwrap() = String::new();
1298 *self.topleft_tab.lock().unwrap() = TopLeftTab::default();
1299 *self.bottomright_tab.lock().unwrap() = BottomRightTab::default();
1300 self.bump();
1301 }
1302
1303 pub fn ops_window_open(&self) -> bool {
1307 self.ops_window.load(Ordering::Acquire)
1308 }
1309
1310 pub fn table_sort(&self, table: &str) -> Option<(usize, bool)> {
1313 self.table_sorts
1314 .lock()
1315 .unwrap()
1316 .get(table)
1317 .copied()
1318 .flatten()
1319 }
1320
1321 pub fn toggle_table_sort(&self, table: &str, col: usize) {
1323 let mut m = self.table_sorts.lock().unwrap();
1324 let asc = match m.get(table).copied().flatten() {
1325 Some((c, a)) if c == col => !a,
1326 _ => true,
1327 };
1328 m.insert(table.to_string(), Some((col, asc)));
1329 }
1330
1331 pub fn set_ops_window_open(&self, v: bool) {
1332 self.ops_window.store(v, Ordering::Release);
1333 self.bump();
1334 }
1335
1336 pub fn accounts_window_open(&self) -> bool {
1340 self.accounts_window.load(Ordering::Acquire)
1341 }
1342
1343 pub fn set_accounts_window_open(&self, v: bool) {
1345 self.accounts_window.store(v, Ordering::Release);
1346 self.bump();
1347 }
1348
1349 pub fn account_edit_open(&self) -> bool {
1351 self.account_edit_window.load(Ordering::Acquire)
1352 }
1353
1354 pub fn editing_account(&self) -> Option<String> {
1356 self.editing_account.lock().unwrap().clone()
1357 }
1358
1359 pub fn begin_account_edit(&self, gateway: Option<&str>) -> bool {
1363 match gateway {
1364 Some(gw) => {
1365 let Some(row) = self.account(gw) else {
1366 return false;
1367 };
1368 *self.editing_account.lock().unwrap() = Some(gw.to_string());
1369 *self.edit_alias.lock().unwrap() = row.alias.clone();
1370 self.set_ctp_form(row.form.clone());
1371 }
1372 None => {
1373 *self.editing_account.lock().unwrap() = None;
1374 *self.edit_alias.lock().unwrap() = String::new();
1375 let draft = self.draft_from_existing();
1376 self.set_ctp_form(draft);
1377 }
1378 }
1379 *self.edit_error.lock().unwrap() = None;
1380 self.account_edit_window.store(true, Ordering::Release);
1381 self.bump();
1382 true
1383 }
1384
1385 pub fn end_account_edit(&self) {
1387 *self.editing_account.lock().unwrap() = None;
1388 *self.edit_alias.lock().unwrap() = String::new();
1389 *self.edit_error.lock().unwrap() = None;
1390 self.account_edit_window.store(false, Ordering::Release);
1391 let draft = self.draft_from_existing();
1392 self.set_ctp_form(draft);
1393 self.bump();
1394 }
1395
1396 pub fn end_account_edit_if_editing(&self, gateway: &str) {
1402 if self.editing_account().as_deref() == Some(gateway) {
1403 self.end_account_edit();
1404 }
1405 }
1406
1407 pub fn edit_alias(&self) -> String {
1409 self.edit_alias.lock().unwrap().clone()
1410 }
1411
1412 pub fn set_edit_alias(&self, v: impl Into<String>) {
1414 *self.edit_alias.lock().unwrap() = v.into();
1415 self.bump();
1416 }
1417
1418 pub fn edit_error(&self) -> Option<String> {
1420 self.edit_error.lock().unwrap().clone()
1421 }
1422
1423 pub fn set_edit_error(&self, e: Option<String>) {
1425 *self.edit_error.lock().unwrap() = e;
1426 self.bump();
1427 }
1428
1429 pub fn account_edit_dirty(&self) -> bool {
1431 let editing = self.editing_account();
1432 let draft = self.ctp_form();
1433 match editing {
1434 Some(gw) => self.account(&gw).map(|a| a.form != draft).unwrap_or(false),
1435 None => draft != CtpForm::default() && !draft.td_user.trim().is_empty(),
1436 }
1437 }
1438
1439 pub fn accounts(&self) -> Vec<AccountRow> {
1441 self.accounts.lock().unwrap().clone()
1442 }
1443
1444 pub fn account(&self, gateway: &str) -> Option<AccountRow> {
1446 self.accounts
1447 .lock()
1448 .unwrap()
1449 .iter()
1450 .find(|a| a.gateway == gateway)
1451 .cloned()
1452 }
1453
1454 pub fn add_account(&self, row: AccountRow) -> Result<(), String> {
1460 let mut acc = self.accounts.lock().unwrap();
1461 if acc.iter().any(|a| a.gateway == row.gateway) {
1462 return Err(format!("账户 {} 已存在(同 broker + 账号)", row.gateway));
1463 }
1464 let alias = row.alias.trim();
1465 if !alias.is_empty()
1466 && acc
1467 .iter()
1468 .any(|a| a.alias.trim().eq_ignore_ascii_case(alias))
1469 {
1470 return Err(format!("别名 {alias:?} 已被其他账户占用(别名须唯一)"));
1471 }
1472 if acc.len() >= MAX_ACCOUNTS {
1473 return Err(format!(
1474 "账户已达软上限 {MAX_ACCOUNTS} 个(先移除一个再添加)"
1475 ));
1476 }
1477 acc.push(row);
1478 drop(acc);
1479 self.bump();
1480 Ok(())
1481 }
1482
1483 #[must_use]
1485 pub fn can_add_account(&self) -> bool {
1486 self.accounts.lock().unwrap().len() < MAX_ACCOUNTS
1487 }
1488
1489 pub fn set_account_alias(&self, gateway: &str, alias: &str) -> bool {
1495 let alias = alias.trim().to_string();
1496 let mut acc = self.accounts.lock().unwrap();
1497 if !alias.is_empty()
1498 && acc
1499 .iter()
1500 .any(|a| a.gateway != gateway && a.alias.eq_ignore_ascii_case(&alias))
1501 {
1502 return false;
1503 }
1504 match acc.iter_mut().find(|a| a.gateway == gateway) {
1505 Some(a) => {
1506 a.alias = alias;
1507 drop(acc);
1508 self.bump();
1509 true
1510 }
1511 None => false,
1512 }
1513 }
1514
1515 pub fn note_connect_sent(&self, gateway: &str) {
1520 let mut acc = self.accounts.lock().unwrap();
1521 if let Some(a) = acc.iter_mut().find(|a| a.gateway == gateway) {
1522 a.stage = AccountStage::Assembling;
1523 a.fail_note = None;
1524 a.login = LoginState::Unknown;
1525 a.status_note = None;
1526 a.login_brief = None;
1527 drop(acc);
1528 self.bump();
1529 }
1530 }
1531
1532 pub fn note_connect_result(&self, gateway: &str, result: Result<(), String>) {
1536 let mut acc = self.accounts.lock().unwrap();
1537 if let Some(a) = acc.iter_mut().find(|a| a.gateway == gateway) {
1538 match result {
1539 Ok(()) => {
1540 a.stage = AccountStage::Assembled;
1541 a.fail_note = None;
1542 }
1543 Err(e) => {
1544 a.stage = AccountStage::AssembleFailed;
1545 a.fail_note = Some(e);
1546 }
1547 }
1548 drop(acc);
1549 self.bump();
1550 }
1551 }
1552
1553 pub fn note_effective_product_classes(&self, gateway: &str, classes: &[String]) {
1557 let mut acc = self.accounts.lock().unwrap();
1558 if let Some(a) = acc.iter_mut().find(|a| a.gateway == gateway) {
1559 a.effective_classes = classes.to_vec();
1560 drop(acc);
1561 self.bump();
1562 }
1563 }
1564
1565 #[must_use]
1569 pub fn status_thresholds(&self) -> (u32, i64) {
1570 self.status_thresholds
1571 .lock()
1572 .unwrap()
1573 .unwrap_or((QUERY_REJECT_STREAK_LIMIT, ACCOUNT_STALE_SECS))
1574 }
1575
1576 pub fn set_status_thresholds(&self, reject_limit: u32, stale_secs: i64) {
1578 *self.status_thresholds.lock().unwrap() = Some((reject_limit, stale_secs));
1579 self.bump();
1580 }
1581
1582 pub fn status_poll_due(&self) -> bool {
1584 let n = self.status_tick.fetch_add(1, Ordering::Relaxed) + 1;
1585 n.is_multiple_of(STATUS_POLL_TICKS)
1586 }
1587
1588 pub fn note_gateway_status(&self, gateway: &str, st: &ek_ctp::plugin::GatewayStatus) {
1596 let (limit, stale) = self.status_thresholds();
1597 let now = ek_ctp::map::now_epoch_ms();
1598 let mut alarm: Option<(LoginState, String)> = None;
1601 let mut found = false;
1602 {
1603 let mut acc = self.accounts.lock().unwrap();
1604 if let Some(a) = acc.iter_mut().find(|a| a.gateway == gateway) {
1605 found = true;
1606 a.apply_status(st, now, limit, stale);
1607 if a.login.is_alarm() || a.login == LoginState::QueryStarved {
1608 alarm = Some((
1609 a.login,
1610 a.status_note
1611 .clone()
1612 .unwrap_or_else(|| a.login.label().to_string()),
1613 ));
1614 }
1615 }
1616 }
1617 if found {
1618 self.bump();
1619 }
1620 if let Some((login, note)) = alarm
1621 && self.alert_once(format!("{gateway}|{}", login.label()))
1622 {
1623 self.push_alert(
1624 crate::toast::AlertLevel::Error,
1625 format!("账户 {gateway}:{note}"),
1626 );
1627 }
1628 }
1629
1630 pub fn note_status_unreachable(&self, gateway: &str, why: &str) {
1634 let mut acc = self.accounts.lock().unwrap();
1635 if let Some(a) = acc.iter_mut().find(|a| a.gateway == gateway) {
1636 a.login = LoginState::Unknown;
1637 a.status_note = Some(format!("状态口问不到:{why}"));
1638 a.login_brief = None;
1639 drop(acc);
1640 self.bump();
1641 }
1642 }
1643
1644 pub fn mark_account_stopped(&self, gateway: &str) {
1646 let mut acc = self.accounts.lock().unwrap();
1647 if let Some(a) = acc.iter_mut().find(|a| a.gateway == gateway) {
1648 a.login = LoginState::Stopped;
1649 a.status_note = Some("会话已停;『连接』可恢复".to_string());
1650 a.login_brief = None;
1651 drop(acc);
1652 self.bump();
1653 }
1654 }
1655
1656 pub fn prepare_connect_form(&self, gateway: &str, form: CtpForm) -> CtpForm {
1659 let source = self.md_source();
1660 let mut f = form;
1661 if source.as_str() != gateway {
1662 f.md_addr.clear();
1663 f.md_user.clear();
1664 f.md_password.clear();
1665 }
1666 self.update_account_form(gateway, f.clone());
1667 f
1668 }
1669
1670 #[must_use]
1674 pub fn draft_from_existing(&self) -> CtpForm {
1675 let acc = self.accounts.lock().unwrap();
1676 let mut draft = CtpForm::default();
1677 if let Some(a) = acc.last() {
1678 draft.broker_id = a.form.broker_id.clone();
1679 draft.td_auth_code = a.form.td_auth_code.clone();
1680 draft.td_app_id = a.form.td_app_id.clone();
1681 draft.accept_futures = a.form.accept_futures;
1682 draft.accept_options = a.form.accept_options;
1683 }
1684 draft
1685 }
1686
1687 pub fn update_account_form(&self, gateway: &str, form: CtpForm) -> bool {
1689 let mut acc = self.accounts.lock().unwrap();
1690 match acc.iter_mut().find(|a| a.gateway == gateway) {
1691 Some(a) => {
1692 a.form = form;
1693 drop(acc);
1694 self.bump();
1695 true
1696 }
1697 None => false,
1698 }
1699 }
1700
1701 pub fn remove_account(&self, gateway: &str) -> bool {
1703 let mut acc = self.accounts.lock().unwrap();
1704 let before = acc.len();
1705 acc.retain(|a| a.gateway != gateway);
1706 let removed = acc.len() != before;
1707 if removed {
1708 let mut md = self.md_source.lock().unwrap();
1710 if md.as_str() == gateway {
1711 md.clear();
1712 }
1713 drop(acc);
1714 self.bump();
1715 }
1716 removed
1717 }
1718
1719 pub fn sync_accounts_connected(&self, rows: &[PlugRow]) -> Vec<String> {
1728 let mut newly = Vec::new();
1729 {
1730 let mut acc = self.accounts.lock().unwrap();
1731 for a in acc.iter_mut() {
1732 let up = rows.iter().any(|r| r.name == a.gateway);
1733 if up && !a.connected {
1734 newly.push(a.gateway.clone());
1735 }
1736 a.connected = up;
1737 if up {
1738 a.stage = AccountStage::Assembled;
1739 a.fail_note = None;
1740 } else if a.stage == AccountStage::Assembled {
1741 a.stage = AccountStage::NotAssembled;
1742 a.login = LoginState::Unknown;
1743 a.status_note = None;
1744 a.login_brief = None;
1745 }
1746 }
1747 }
1748 if !newly.is_empty() {
1749 self.bump();
1750 }
1751 newly
1752 }
1753
1754 pub fn md_source(&self) -> String {
1756 self.md_source.lock().unwrap().clone()
1757 }
1758
1759 pub fn accounts_locked(&self) -> bool {
1761 self.accounts.lock().unwrap().iter().any(|a| a.connected)
1762 }
1763
1764 pub fn set_md_source(&self, gateway: &str) -> Result<(), String> {
1766 {
1767 let acc = self.accounts.lock().unwrap();
1768 if acc.iter().any(|a| a.connected) {
1769 return Err("元账号已锁定:有账户在线,切换行情源需先断开该账户".to_string());
1770 }
1771 if !acc.iter().any(|a| a.gateway == gateway) {
1772 return Err(format!("账户 {gateway} 不在清册"));
1773 }
1774 }
1775 let mut acc = self.accounts.lock().unwrap();
1776 for a in acc.iter_mut() {
1777 a.md_source = a.gateway == gateway;
1778 }
1779 drop(acc);
1780 let mut md = self.md_source.lock().unwrap();
1781 *md = gateway.to_string();
1782 self.bump();
1783 Ok(())
1784 }
1785
1786 pub fn subscriptions(&self) -> Vec<String> {
1788 self.subscriptions.lock().unwrap().clone()
1789 }
1790
1791 pub fn note_subscriptions(&self, symbols: &[String]) {
1793 {
1794 let mut subs = self.subscriptions.lock().unwrap();
1795 for s in symbols {
1796 let s = s.trim();
1797 if !s.is_empty() && !subs.iter().any(|x| x == s) {
1798 subs.push(s.to_string());
1799 }
1800 }
1801 }
1802 self.bump();
1803 }
1804
1805 pub fn default_gateway(&self) -> Option<String> {
1812 let chosen = {
1813 let f = self.order_form.lock().unwrap();
1814 f.gateway.trim().to_string()
1815 };
1816 if !chosen.is_empty() && self.account(&chosen).is_some_and(|a| a.connected) {
1817 return Some(chosen);
1818 }
1819 let acc = self.accounts.lock().unwrap();
1820 acc.iter().find(|a| a.connected).map(|a| a.gateway.clone())
1821 }
1822
1823 pub fn kline_symbols(&self) -> Vec<String> {
1825 self.kline_windows.lock().unwrap().clone()
1826 }
1827
1828 pub fn open_kline(&self, symbol: impl Into<String>) {
1830 let s = symbol.into();
1831 let mut v = self.kline_windows.lock().unwrap();
1832 if !v.contains(&s) {
1833 v.push(s.clone());
1834 }
1835 drop(v);
1836 self.log_line(format!("[kline] 打开 K线窗 {s}"));
1837 }
1838
1839 pub fn close_kline(&self, symbol: &str) {
1841 self.kline_windows.lock().unwrap().retain(|s| s != symbol);
1842 self.bump();
1843 }
1844
1845 pub fn contracts_window_open(&self) -> bool {
1849 self.contracts_window.load(Ordering::Acquire)
1850 }
1851
1852 pub fn set_contracts_window_open(&self, v: bool) {
1854 self.contracts_window.store(v, Ordering::Release);
1855 self.bump();
1856 }
1857
1858 pub fn update_contract(&self, c: &ek_data::contract::ContractData) {
1860 self.contract_store.lock().unwrap().put(c);
1861 self.bump();
1862 }
1863
1864 pub fn contracts(&self, pattern: &str) -> Vec<ek_data::contract::ContractData> {
1866 self.contract_store.lock().unwrap().query(pattern)
1867 }
1868
1869 pub fn contract_count(&self) -> usize {
1871 self.contract_store.lock().unwrap().len()
1872 }
1873
1874 pub fn contract_query(&self) -> String {
1876 self.contract_query.lock().unwrap().clone()
1877 }
1878
1879 pub fn set_contract_query(&self, q: impl Into<String>) {
1881 *self.contract_query.lock().unwrap() = q.into();
1882 self.bump();
1883 }
1884
1885 pub fn toggle_contract_selection(&self, en_symbol: &str) {
1887 let mut v = self.contract_selected.lock().unwrap();
1888 match v.iter().position(|s| s == en_symbol) {
1889 Some(i) => {
1890 v.remove(i);
1891 }
1892 None => v.push(en_symbol.to_string()),
1893 }
1894 self.bump();
1895 }
1896
1897 pub fn selected_contracts(&self) -> Vec<String> {
1899 self.contract_selected.lock().unwrap().clone()
1900 }
1901
1902 pub fn log_line(&self, line: impl Into<String>) {
1904 self.logs.lock().unwrap().push(line);
1905 self.bump();
1906 }
1907
1908 pub fn clear_logs(&self) {
1910 self.logs.lock().unwrap().clear();
1911 self.bump();
1912 }
1913
1914 pub fn logs_snapshot(&self) -> (Vec<String>, u64) {
1916 let r = self.logs.lock().unwrap();
1917 (r.snapshot(), r.spilled())
1918 }
1919
1920 pub fn logs_entries(&self) -> (Vec<LogEntry>, u64) {
1922 let r = self.logs.lock().unwrap();
1923 (r.entries(), r.spilled())
1924 }
1925
1926 pub fn logs_len(&self) -> usize {
1928 self.logs.lock().unwrap().len()
1929 }
1930
1931 pub fn set_snapshot(&self, snap: BusSnap) {
1933 *self.snap.lock().unwrap() = Arc::new(snap);
1934 self.bump();
1935 }
1936
1937 pub fn snapshot(&self) -> Arc<BusSnap> {
1939 Arc::clone(&self.snap.lock().unwrap())
1940 }
1941
1942 pub fn rev(&self) -> u64 {
1944 self.rev.load(Ordering::Acquire)
1945 }
1946
1947 pub fn bump(&self) {
1949 self.rev.fetch_add(1, Ordering::AcqRel);
1950 }
1951
1952 pub fn request_stop(&self) {
1954 self.stop.store(true, Ordering::Release);
1955 }
1956
1957 pub fn push_alert(&self, level: crate::toast::AlertLevel, text: impl Into<String>) {
1960 self.pending_alerts
1961 .lock()
1962 .unwrap_or_else(std::sync::PoisonError::into_inner)
1963 .push((level, text.into()));
1964 }
1965
1966 pub(crate) fn take_alerts(&self) -> Vec<(crate::toast::AlertLevel, String)> {
1968 let mut q = self
1969 .pending_alerts
1970 .lock()
1971 .unwrap_or_else(std::sync::PoisonError::into_inner);
1972 std::mem::take(&mut *q)
1973 }
1974
1975 pub(crate) fn toasts(&self) -> std::sync::MutexGuard<'_, Option<egui_notify::Toasts>> {
1977 self.toasts.lock()
1978 }
1979
1980 pub fn alert_once(&self, key: impl Into<String>) -> bool {
1982 let key = key.into();
1983 self.alerted
1984 .lock()
1985 .unwrap_or_else(std::sync::PoisonError::into_inner)
1986 .insert(key)
1987 }
1988
1989 #[must_use]
1991 pub fn pending_alert_count(&self) -> usize {
1992 self.pending_alerts
1993 .lock()
1994 .unwrap_or_else(std::sync::PoisonError::into_inner)
1995 .len()
1996 }
1997
1998 #[must_use]
2000 pub fn alerted_count(&self) -> usize {
2001 self.alerted
2002 .lock()
2003 .unwrap_or_else(std::sync::PoisonError::into_inner)
2004 .len()
2005 }
2006
2007 pub fn exit_confirm_open(&self) -> bool {
2009 self.exit_confirm_open
2010 .load(std::sync::atomic::Ordering::Relaxed)
2011 }
2012
2013 pub fn open_exit_confirm(&self) {
2014 self.exit_confirm_open
2015 .store(true, std::sync::atomic::Ordering::Relaxed);
2016 }
2017
2018 pub fn close_exit_confirm(&self) {
2019 self.exit_confirm_open
2020 .store(false, std::sync::atomic::Ordering::Relaxed);
2021 }
2022
2023 pub fn exit_confirmed(&self) -> bool {
2024 self.exit_confirmed
2025 .load(std::sync::atomic::Ordering::Relaxed)
2026 }
2027
2028 pub fn confirm_exit(&self) {
2029 self.exit_confirmed
2030 .store(true, std::sync::atomic::Ordering::Relaxed);
2031 }
2032
2033 pub fn is_stop(&self) -> bool {
2034 self.stop.load(Ordering::Acquire)
2035 }
2036
2037 pub fn push_cmd(&self, cmd: UiCmd) {
2041 self.cmds.lock().unwrap().push_back(cmd);
2042 self.bump();
2043 }
2044
2045 pub fn drain_cmds(&self) -> Vec<UiCmd> {
2047 self.cmds.lock().unwrap().drain(..).collect()
2048 }
2049
2050 pub fn pending_confirm(&self) -> Option<String> {
2052 self.pending_confirm.lock().unwrap().clone()
2053 }
2054
2055 pub fn confirm_step(&self, name: &str) -> ConfirmStep {
2057 let mut g = self.pending_confirm.lock().unwrap();
2058 match g.take() {
2059 Some(p) if p == name => ConfirmStep::Fired,
2060 Some(_) => {
2061 *g = Some(name.to_string());
2062 ConfirmStep::Switched
2063 }
2064 None => {
2065 *g = Some(name.to_string());
2066 ConfirmStep::Armed
2067 }
2068 }
2069 }
2070
2071 pub fn confirm_cancel(&self) {
2073 *self.pending_confirm.lock().unwrap() = None;
2074 self.bump();
2075 }
2076
2077 pub fn market(&self) -> &MarketStore {
2081 &self.market
2082 }
2083
2084 pub fn set_trade_snap(&self, ts: TradeSnap) {
2088 *self.trade_snap.lock().unwrap() = Arc::new(ts);
2089 self.bump();
2090 }
2091
2092 pub fn trade_snap(&self) -> Arc<TradeSnap> {
2094 self.trade_snap.lock().unwrap().clone()
2095 }
2096
2097 pub fn ctp_form(&self) -> CtpForm {
2101 self.ctp_form.lock().unwrap().clone()
2102 }
2103
2104 pub fn set_ctp_form(&self, f: CtpForm) {
2106 *self.ctp_form.lock().unwrap() = f;
2107 self.bump();
2108 }
2109
2110 pub fn row_selected(&self, table: &str, key: &str) -> bool {
2114 self.row_sel
2115 .lock()
2116 .unwrap()
2117 .get(table)
2118 .is_some_and(|k| k == key)
2119 }
2120
2121 pub fn select_row(&self, table: &str, key: &str) {
2123 self.row_sel
2124 .lock()
2125 .unwrap()
2126 .insert(table.to_string(), key.to_string());
2127 }
2128}