1use std::collections::{BTreeMap, VecDeque};
7use std::sync::atomic::{AtomicBool, AtomicU8, 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
558#[derive(Debug, Clone, PartialEq, Default)]
564pub struct AccountRow {
565 pub gateway: String,
567 pub alias: String,
569 pub broker_id: String,
570 pub account: String,
571 pub md_source: bool,
573 pub form: CtpForm,
575 pub stage: AccountStage,
577 pub fail_note: Option<String>,
579 pub effective_classes: Vec<String>,
584 pub connected: bool,
587 pub login: LoginState,
591 pub status_note: Option<String>,
594}
595
596impl AccountRow {
597 pub fn derive(form: &CtpForm) -> Result<Self, String> {
602 let broker = form.broker_id.trim();
603 let account = form.td_user.trim();
604 let gateway = ek_ctp::derived_gateway_name(broker, account).ok_or_else(|| {
605 "账户: broker/账号 不能为空且只能含字母数字(且派生名 ≤32)".to_string()
606 })?;
607 Ok(Self {
608 gateway,
609 alias: String::new(),
610 broker_id: broker.to_string(),
611 account: account.to_string(),
612 md_source: false,
613 form: form.clone(),
614 stage: AccountStage::NotAssembled,
615 fail_note: None,
616 effective_classes: Vec::new(),
617 connected: false,
618 login: LoginState::Unknown,
619 status_note: None,
620 })
621 }
622
623 #[must_use]
629 pub fn status_label(&self) -> String {
630 if self.stage == AccountStage::Assembled {
631 match self.login {
632 LoginState::Unknown => "已装配·登录态未知".to_string(),
633 other => format!("已装配·{}", other.label()),
634 }
635 } else {
636 self.stage.label().to_string()
637 }
638 }
639
640 pub fn apply_status(
653 &mut self,
654 st: &ek_ctp::plugin::GatewayStatus,
655 now_ms: i64,
656 reject_limit: u32,
657 stale_secs: i64,
658 ) {
659 let td_side = !self.form.td_addr.trim().is_empty();
660 let (login, note) = if td_side {
661 if st.td_login_failed && !st.td_logged_in {
662 (
663 LoginState::LoginFailed,
664 st.td_last_error
665 .clone()
666 .or_else(|| Some("登录失败(网关未回带原因)".to_string())),
667 )
668 } else if st.td_logged_in {
669 if st.td_query_reject_streak >= reject_limit {
670 (
671 LoginState::QueryStarved,
672 Some(format!("查询连续被拒 {} 次", st.td_query_reject_streak)),
673 )
674 } else if let Some(last) = st.td_last_account_ms {
675 let idle_ms = now_ms.saturating_sub(last);
676 if idle_ms > stale_secs.saturating_mul(1_000) {
677 (
678 LoginState::QueryStarved,
679 Some(format!("资金回报已 {}s 未更新", idle_ms / 1_000)),
680 )
681 } else {
682 (LoginState::Online, count_note("掉线", st.td_disconnects))
683 }
684 } else {
685 let since_login_ms = st.td_last_login_ok_ms.map(|ok| now_ms.saturating_sub(ok));
692 match since_login_ms {
693 Some(ms) if ms > stale_secs.saturating_mul(1_000) => (
694 LoginState::QueryStarved,
695 Some(format!("资金回报从未到手(登录已 {}s)", ms / 1_000)),
696 ),
697 _ => (LoginState::Online, Some("资金回报尚未到手".to_string())),
698 }
699 }
700 } else if self.login == LoginState::Stopped {
701 let extra = if st.td_disconnects > 0 {
702 format!("(此前掉线 {} 次)", st.td_disconnects)
703 } else {
704 String::new()
705 };
706 (
707 LoginState::Stopped,
708 Some(format!("会话已停{extra};『连接』可恢复")),
709 )
710 } else if st.td_disconnects > 0 {
711 (
712 LoginState::FrontDown,
713 Some(format!("掉线 {} 次", st.td_disconnects)),
714 )
715 } else {
716 (LoginState::Unknown, st.td_last_error.clone())
717 }
718 } else if st.md_logged_in {
719 (
720 LoginState::Online,
721 count_note("掉线", st.md_disconnects)
722 .or_else(|| Some("纯行情账户(无 TD 会话)".to_string())),
723 )
724 } else if self.login == LoginState::Stopped {
725 (
726 LoginState::Stopped,
727 Some("会话已停;『连接』可恢复".to_string()),
728 )
729 } else if st.md_disconnects > 0 {
730 (
731 LoginState::FrontDown,
732 Some(format!("掉线 {} 次", st.md_disconnects)),
733 )
734 } else {
735 (LoginState::Unknown, st.md_last_error.clone())
736 };
737 let note = if td_side && st.td_instrument_rejects > 0 {
742 let add = format!("合约查询被拒 {} 次", st.td_instrument_rejects);
743 match note {
744 Some(base) if base.contains("合约查询被拒") => Some(base),
746 Some(base) => Some(format!("{base};{add}")),
747 None => Some(add),
748 }
749 } else {
750 note
751 };
752 self.login = login;
753 self.status_note = note;
754 }
755
756 #[must_use]
758 pub fn display_name(&self) -> String {
759 let a = self.alias.trim();
760 if a.is_empty() {
761 self.account.clone()
762 } else {
763 a.to_string()
764 }
765 }
766}
767
768#[derive(Debug, Clone, PartialEq, Default)]
770pub struct TradeSnap {
771 pub orders: Vec<ek_data::order::OrderData>,
772 pub trades: Vec<ek_data::trade::TradeData>,
773 pub positions: Vec<ek_data::position::PositionData>,
774 pub accounts: Vec<ek_data::account::AccountData>,
776}
777
778pub fn parse_reload_csv(form: &ReloadForm) -> Result<(i64, String, Vec<BarData>), String> {
781 let interval: i64 = form
782 .interval
783 .trim()
784 .parse()
785 .map_err(|_| format!("回灌 解析: interval 非整数:{:?}", form.interval))?;
786 let symbol = form.symbol.trim().to_string();
787 if symbol.is_empty() {
788 return Err("回灌解析: symbol 空".to_string());
789 }
790 if form.csv.trim().is_empty() {
791 return Err("回灌解析: csv 空".to_string());
792 }
793 let mut bars = Vec::new();
794 for (i, raw) in form.csv.lines().enumerate() {
795 let line = raw.trim();
796 if line.is_empty() || line.starts_with('#') {
797 continue;
798 }
799 let parts: Vec<&str> = line.split(',').map(str::trim).collect();
800 if parts.len() != 6 {
801 return Err(format!("回灌解析: 第 {} 列数 ≠ 6", i + 1));
802 }
803 let num = |s: &str| -> Result<f64, String> {
804 s.parse::<f64>()
805 .map_err(|_| format!("回灌解析: 第 {} 列数值坏:{:?}", i + 1, s))
806 };
807 let dt_s: i64 = parts[0]
808 .parse()
809 .map_err(|_| format!("回灌解析: 第 {} 列 dt 坏:{:?}", i + 1, parts[0]))?;
810 bars.push(BarData {
811 symbol: symbol.clone(),
812 datetime: dt_s * 1_000_000_000,
813 interval: Some(
814 ek_dm::wire::interval_of_seconds(interval)
815 .unwrap_or(ek_data::constant::Interval::MINUTE),
816 ),
817 open_price: num(parts[1])?,
818 high_price: num(parts[2])?,
819 low_price: num(parts[3])?,
820 close_price: num(parts[4])?,
821 volume: num(parts[5])?,
822 ..BarData::default()
823 });
824 }
825 if bars.is_empty() {
826 return Err("回灌解析: 无有效行".to_string());
827 }
828 Ok((interval, symbol, bars))
829}
830
831#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
836pub enum TopLeftTab {
837 #[default]
838 Orders,
839 Active,
840}
841
842impl TopLeftTab {
843 pub fn label(self) -> &'static str {
844 match self {
845 Self::Orders => "委托",
846 Self::Active => "未成交",
847 }
848 }
849}
850
851#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
853pub enum BottomRightTab {
854 #[default]
855 Account,
856 Logs,
857}
858
859impl BottomRightTab {
860 pub fn label(self) -> &'static str {
861 match self {
862 Self::Account => "资金",
863 Self::Logs => "日志",
864 }
865 }
866}
867
868#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
872pub enum Theme {
873 #[default]
875 FinanceDark,
876 EguiDark,
878 EguiLight,
880 QtClassic,
882 QDarkStyle,
888}
889
890impl Theme {
891 pub fn label(self) -> &'static str {
892 match self {
893 Self::FinanceDark => "金融深色",
894 Self::EguiDark => "egui深色",
895 Self::EguiLight => "egui浅色",
896 Self::QtClassic => "Qt灰白",
897 Self::QDarkStyle => "Qt深色",
898 }
899 }
900
901 pub fn next(self) -> Self {
903 match self {
904 Self::FinanceDark => Self::EguiDark,
905 Self::EguiDark => Self::EguiLight,
906 Self::EguiLight => Self::QtClassic,
907 Self::QtClassic => Self::QDarkStyle,
908 Self::QDarkStyle => Self::FinanceDark,
909 }
910 }
911}
912
913#[derive(Debug, Default)]
916pub struct TickStore {
917 ticks: BTreeMap<String, ek_data::tick::TickData>,
918}
919
920impl TickStore {
921 pub fn put(&mut self, t: &ek_data::tick::TickData) {
924 self.ticks
925 .insert(format!("{}.{}", t.exchange.as_str(), t.symbol), t.clone());
926 }
927
928 pub fn rows(&self) -> Vec<ek_data::tick::TickData> {
930 let mut v: Vec<ek_data::tick::TickData> = self.ticks.values().cloned().collect();
931 v.sort_by(|a, b| {
932 (a.exchange.as_str().to_string(), a.symbol.clone())
933 .cmp(&(b.exchange.as_str().to_string(), b.symbol.clone()))
934 });
935 v
936 }
937
938 pub fn groups(&self) -> Vec<(String, Vec<ek_data::tick::TickData>)> {
940 let mut out: Vec<(String, Vec<ek_data::tick::TickData>)> = Vec::new();
941 for t in self.rows() {
942 let ex = t.exchange.as_str().to_string();
943 match out.last_mut() {
944 Some((e, v)) if *e == ex => v.push(t),
945 _ => out.push((ex, vec![t])),
946 }
947 }
948 out
949 }
950
951 pub fn len(&self) -> usize {
952 self.ticks.len()
953 }
954
955 pub fn is_empty(&self) -> bool {
956 self.ticks.is_empty()
957 }
958}
959
960#[derive(Debug, Clone, PartialEq)]
962pub struct OrderForm {
963 pub exchange: String,
965 pub symbol: String,
967 pub direction: ek_data::constant::Direction,
968 pub offset: ek_data::constant::Offset,
969 pub order_type: ek_data::constant::OrderType,
970 pub price: String,
972 pub volume: String,
973 pub gateway: String,
975 pub follow_price: bool,
977 pub auto_split: bool,
980}
981
982impl Default for OrderForm {
983 fn default() -> Self {
984 Self {
985 exchange: "SHFE".to_string(),
986 symbol: String::new(),
987 direction: ek_data::constant::Direction::LONG,
988 offset: ek_data::constant::Offset::OPEN,
989 order_type: ek_data::constant::OrderType::LIMIT,
990 price: String::new(),
991 volume: "1".to_string(),
992 gateway: String::new(),
993 follow_price: true,
994 auto_split: true,
995 }
996 }
997}
998
999impl OrderForm {
1000 pub fn to_request(&self) -> Result<ek_data::order_request::OrderRequest, String> {
1002 use std::str::FromStr;
1003 let symbol = self.symbol.trim();
1004 if symbol.is_empty() {
1005 return Err("下单: 代码为空".to_string());
1006 }
1007 if symbol.contains('.') {
1008 return Err("下单: 代码不带交易所前缀(交易所走下拉)".to_string());
1009 }
1010 let exchange = ek_data::constant::Exchange::from_str(self.exchange.trim())
1011 .map_err(|e| format!("下单: {e}"))?;
1012 let price: f64 = self
1013 .price
1014 .trim()
1015 .parse()
1016 .map_err(|_| format!("下单: 价格无效 {:?}", self.price))?;
1017 let volume: f64 = self
1018 .volume
1019 .trim()
1020 .parse()
1021 .map_err(|_| format!("下单: 数量无效 {:?}", self.volume))?;
1022 if self.order_type != ek_data::constant::OrderType::MARKET && price <= 0.0 {
1023 return Err("下单: 限价单价格须为正".to_string());
1024 }
1025 if volume <= 0.0 {
1026 return Err("下单: 数量须为正".to_string());
1027 }
1028 Ok(ek_data::order_request::OrderRequest {
1029 symbol: symbol.to_string(),
1030 exchange,
1031 direction: self.direction,
1032 order_type: self.order_type,
1033 volume,
1034 price,
1035 offset: self.offset,
1036 ..ek_data::order_request::OrderRequest::default()
1037 })
1038 }
1039}
1040
1041#[derive(Debug, Default)]
1047pub struct ContractStore {
1048 contracts: BTreeMap<String, ek_data::contract::ContractData>,
1049}
1050
1051impl ContractStore {
1052 pub fn put(&mut self, c: &ek_data::contract::ContractData) {
1053 self.contracts
1054 .insert(format!("{}.{}", c.exchange.as_str(), c.symbol), c.clone());
1055 }
1056
1057 pub fn query(&self, pattern: &str) -> Vec<ek_data::contract::ContractData> {
1059 let p = pattern.trim();
1060 self.contracts
1061 .values()
1062 .filter(|c| {
1063 p.is_empty()
1064 || c.symbol.contains(p)
1065 || c.exchange.as_str().eq_ignore_ascii_case(p)
1066 || c.name.contains(p)
1067 })
1068 .cloned()
1069 .collect()
1070 }
1071
1072 pub fn len(&self) -> usize {
1073 self.contracts.len()
1074 }
1075
1076 pub fn is_empty(&self) -> bool {
1077 self.contracts.is_empty()
1078 }
1079}
1080
1081#[derive(Debug, Default)]
1083pub struct UiShared {
1084 logs: Mutex<LogRing>,
1085 snap: Mutex<Arc<BusSnap>>,
1086 market: MarketStore,
1087 ctp_form: Mutex<CtpForm>,
1088 trade_snap: Mutex<Arc<TradeSnap>>,
1089 tick_store: Mutex<TickStore>,
1090 order_form: Mutex<OrderForm>,
1091 selected_exchange: Mutex<String>,
1092 topleft_tab: Mutex<TopLeftTab>,
1093 bottomright_tab: Mutex<BottomRightTab>,
1094 ops_window: AtomicBool,
1095 accounts_window: AtomicBool,
1098 contracts_window: AtomicBool,
1099 theme: AtomicU8,
1101 contract_store: Mutex<ContractStore>,
1102 contract_query: Mutex<String>,
1103 contract_selected: Mutex<Vec<String>>,
1104 kline_windows: Mutex<Vec<String>>,
1105 rev: AtomicU64,
1106 stop: AtomicBool,
1108 cmds: Mutex<VecDeque<UiCmd>>,
1110 pending_confirm: Mutex<Option<String>>,
1112 accounts: Mutex<Vec<AccountRow>>,
1114 account_edit_window: AtomicBool,
1116 editing_account: Mutex<Option<String>>,
1118 edit_alias: Mutex<String>,
1120 edit_error: Mutex<Option<String>>,
1122 md_source: Mutex<String>,
1124 subscriptions: Mutex<Vec<String>>,
1126 row_sel: Mutex<BTreeMap<String, String>>,
1129 status_tick: AtomicU32,
1131 status_thresholds: Mutex<Option<(u32, i64)>>,
1134}
1135
1136impl UiShared {
1137 pub fn new() -> Self {
1139 Self::default()
1140 }
1141
1142 pub fn update_tick(&self, t: &ek_data::tick::TickData) {
1146 self.tick_store.lock().unwrap().put(t);
1147 self.bump();
1148 }
1149
1150 pub fn tick_rows(&self) -> Vec<ek_data::tick::TickData> {
1152 self.tick_store.lock().unwrap().rows()
1153 }
1154
1155 pub fn tick_groups(&self) -> Vec<(String, Vec<ek_data::tick::TickData>)> {
1157 self.tick_store.lock().unwrap().groups()
1158 }
1159
1160 pub fn tick_count(&self) -> usize {
1162 self.tick_store.lock().unwrap().len()
1163 }
1164
1165 pub fn order_form(&self) -> OrderForm {
1167 self.order_form.lock().unwrap().clone()
1168 }
1169
1170 pub fn set_order_form(&self, f: OrderForm) {
1172 *self.order_form.lock().unwrap() = f;
1173 self.bump();
1174 }
1175
1176 pub fn selected_exchange(&self) -> String {
1178 self.selected_exchange.lock().unwrap().clone()
1179 }
1180
1181 pub fn set_selected_exchange(&self, s: impl Into<String>) {
1183 *self.selected_exchange.lock().unwrap() = s.into();
1184 self.bump();
1185 }
1186
1187 pub fn topleft_tab(&self) -> TopLeftTab {
1189 *self.topleft_tab.lock().unwrap()
1190 }
1191
1192 pub fn set_topleft_tab(&self, t: TopLeftTab) {
1194 *self.topleft_tab.lock().unwrap() = t;
1195 self.bump();
1196 }
1197
1198 pub fn bottomright_tab(&self) -> BottomRightTab {
1200 *self.bottomright_tab.lock().unwrap()
1201 }
1202
1203 pub fn set_bottomright_tab(&self, t: BottomRightTab) {
1205 *self.bottomright_tab.lock().unwrap() = t;
1206 self.bump();
1207 }
1208
1209 pub fn theme(&self) -> Theme {
1216 match self.theme.load(Ordering::Acquire) {
1217 1 => Theme::EguiDark,
1218 2 => Theme::EguiLight,
1219 3 => Theme::QtClassic,
1220 4 => Theme::QDarkStyle,
1221 _ => Theme::FinanceDark,
1222 }
1223 }
1224
1225 pub fn set_theme(&self, t: Theme) {
1227 self.theme.store(t as u8, Ordering::Release);
1228 self.bump();
1229 }
1230
1231 pub fn cycle_theme(&self) -> Theme {
1233 let t = self.theme().next();
1234 self.set_theme(t);
1235 t
1236 }
1237
1238 pub fn ops_window_open(&self) -> bool {
1240 self.ops_window.load(Ordering::Acquire)
1241 }
1242
1243 pub fn set_ops_window_open(&self, v: bool) {
1245 self.ops_window.store(v, Ordering::Release);
1246 self.bump();
1247 }
1248
1249 pub fn accounts_window_open(&self) -> bool {
1253 self.accounts_window.load(Ordering::Acquire)
1254 }
1255
1256 pub fn set_accounts_window_open(&self, v: bool) {
1258 self.accounts_window.store(v, Ordering::Release);
1259 self.bump();
1260 }
1261
1262 pub fn account_edit_open(&self) -> bool {
1264 self.account_edit_window.load(Ordering::Acquire)
1265 }
1266
1267 pub fn editing_account(&self) -> Option<String> {
1269 self.editing_account.lock().unwrap().clone()
1270 }
1271
1272 pub fn begin_account_edit(&self, gateway: Option<&str>) -> bool {
1276 match gateway {
1277 Some(gw) => {
1278 let Some(row) = self.account(gw) else {
1279 return false;
1280 };
1281 *self.editing_account.lock().unwrap() = Some(gw.to_string());
1282 *self.edit_alias.lock().unwrap() = row.alias.clone();
1283 self.set_ctp_form(row.form.clone());
1284 }
1285 None => {
1286 *self.editing_account.lock().unwrap() = None;
1287 *self.edit_alias.lock().unwrap() = String::new();
1288 let draft = self.draft_from_existing();
1289 self.set_ctp_form(draft);
1290 }
1291 }
1292 *self.edit_error.lock().unwrap() = None;
1293 self.account_edit_window.store(true, Ordering::Release);
1294 self.bump();
1295 true
1296 }
1297
1298 pub fn end_account_edit(&self) {
1300 *self.editing_account.lock().unwrap() = None;
1301 *self.edit_alias.lock().unwrap() = String::new();
1302 *self.edit_error.lock().unwrap() = None;
1303 self.account_edit_window.store(false, Ordering::Release);
1304 let draft = self.draft_from_existing();
1305 self.set_ctp_form(draft);
1306 self.bump();
1307 }
1308
1309 pub fn end_account_edit_if_editing(&self, gateway: &str) {
1315 if self.editing_account().as_deref() == Some(gateway) {
1316 self.end_account_edit();
1317 }
1318 }
1319
1320 pub fn edit_alias(&self) -> String {
1322 self.edit_alias.lock().unwrap().clone()
1323 }
1324
1325 pub fn set_edit_alias(&self, v: impl Into<String>) {
1327 *self.edit_alias.lock().unwrap() = v.into();
1328 self.bump();
1329 }
1330
1331 pub fn edit_error(&self) -> Option<String> {
1333 self.edit_error.lock().unwrap().clone()
1334 }
1335
1336 pub fn set_edit_error(&self, e: Option<String>) {
1338 *self.edit_error.lock().unwrap() = e;
1339 self.bump();
1340 }
1341
1342 pub fn account_edit_dirty(&self) -> bool {
1344 let editing = self.editing_account();
1345 let draft = self.ctp_form();
1346 match editing {
1347 Some(gw) => self.account(&gw).map(|a| a.form != draft).unwrap_or(false),
1348 None => draft != CtpForm::default() && !draft.td_user.trim().is_empty(),
1349 }
1350 }
1351
1352 pub fn accounts(&self) -> Vec<AccountRow> {
1354 self.accounts.lock().unwrap().clone()
1355 }
1356
1357 pub fn account(&self, gateway: &str) -> Option<AccountRow> {
1359 self.accounts
1360 .lock()
1361 .unwrap()
1362 .iter()
1363 .find(|a| a.gateway == gateway)
1364 .cloned()
1365 }
1366
1367 pub fn add_account(&self, row: AccountRow) -> Result<(), String> {
1373 let mut acc = self.accounts.lock().unwrap();
1374 if acc.iter().any(|a| a.gateway == row.gateway) {
1375 return Err(format!("账户 {} 已存在(同 broker + 账号)", row.gateway));
1376 }
1377 let alias = row.alias.trim();
1378 if !alias.is_empty()
1379 && acc
1380 .iter()
1381 .any(|a| a.alias.trim().eq_ignore_ascii_case(alias))
1382 {
1383 return Err(format!("别名 {alias:?} 已被其他账户占用(别名须唯一)"));
1384 }
1385 if acc.len() >= MAX_ACCOUNTS {
1386 return Err(format!(
1387 "账户已达软上限 {MAX_ACCOUNTS} 个(先移除一个再添加)"
1388 ));
1389 }
1390 acc.push(row);
1391 drop(acc);
1392 self.bump();
1393 Ok(())
1394 }
1395
1396 #[must_use]
1398 pub fn can_add_account(&self) -> bool {
1399 self.accounts.lock().unwrap().len() < MAX_ACCOUNTS
1400 }
1401
1402 pub fn set_account_alias(&self, gateway: &str, alias: &str) -> bool {
1408 let alias = alias.trim().to_string();
1409 let mut acc = self.accounts.lock().unwrap();
1410 if !alias.is_empty()
1411 && acc
1412 .iter()
1413 .any(|a| a.gateway != gateway && a.alias.eq_ignore_ascii_case(&alias))
1414 {
1415 return false;
1416 }
1417 match acc.iter_mut().find(|a| a.gateway == gateway) {
1418 Some(a) => {
1419 a.alias = alias;
1420 drop(acc);
1421 self.bump();
1422 true
1423 }
1424 None => false,
1425 }
1426 }
1427
1428 pub fn note_connect_sent(&self, gateway: &str) {
1433 let mut acc = self.accounts.lock().unwrap();
1434 if let Some(a) = acc.iter_mut().find(|a| a.gateway == gateway) {
1435 a.stage = AccountStage::Assembling;
1436 a.fail_note = None;
1437 a.login = LoginState::Unknown;
1438 a.status_note = None;
1439 drop(acc);
1440 self.bump();
1441 }
1442 }
1443
1444 pub fn note_connect_result(&self, gateway: &str, result: Result<(), String>) {
1448 let mut acc = self.accounts.lock().unwrap();
1449 if let Some(a) = acc.iter_mut().find(|a| a.gateway == gateway) {
1450 match result {
1451 Ok(()) => {
1452 a.stage = AccountStage::Assembled;
1453 a.fail_note = None;
1454 }
1455 Err(e) => {
1456 a.stage = AccountStage::AssembleFailed;
1457 a.fail_note = Some(e);
1458 }
1459 }
1460 drop(acc);
1461 self.bump();
1462 }
1463 }
1464
1465 pub fn note_effective_product_classes(&self, gateway: &str, classes: &[String]) {
1469 let mut acc = self.accounts.lock().unwrap();
1470 if let Some(a) = acc.iter_mut().find(|a| a.gateway == gateway) {
1471 a.effective_classes = classes.to_vec();
1472 drop(acc);
1473 self.bump();
1474 }
1475 }
1476
1477 #[must_use]
1481 pub fn status_thresholds(&self) -> (u32, i64) {
1482 self.status_thresholds
1483 .lock()
1484 .unwrap()
1485 .unwrap_or((QUERY_REJECT_STREAK_LIMIT, ACCOUNT_STALE_SECS))
1486 }
1487
1488 pub fn set_status_thresholds(&self, reject_limit: u32, stale_secs: i64) {
1490 *self.status_thresholds.lock().unwrap() = Some((reject_limit, stale_secs));
1491 self.bump();
1492 }
1493
1494 pub fn status_poll_due(&self) -> bool {
1496 let n = self.status_tick.fetch_add(1, Ordering::Relaxed) + 1;
1497 n % STATUS_POLL_TICKS == 0
1498 }
1499
1500 pub fn note_gateway_status(&self, gateway: &str, st: &ek_ctp::plugin::GatewayStatus) {
1502 let (limit, stale) = self.status_thresholds();
1503 let now = ek_ctp::map::now_epoch_ms();
1504 let mut acc = self.accounts.lock().unwrap();
1505 if let Some(a) = acc.iter_mut().find(|a| a.gateway == gateway) {
1506 a.apply_status(st, now, limit, stale);
1507 drop(acc);
1508 self.bump();
1509 }
1510 }
1511
1512 pub fn note_status_unreachable(&self, gateway: &str, why: &str) {
1516 let mut acc = self.accounts.lock().unwrap();
1517 if let Some(a) = acc.iter_mut().find(|a| a.gateway == gateway) {
1518 a.login = LoginState::Unknown;
1519 a.status_note = Some(format!("状态口问不到:{why}"));
1520 drop(acc);
1521 self.bump();
1522 }
1523 }
1524
1525 pub fn mark_account_stopped(&self, gateway: &str) {
1527 let mut acc = self.accounts.lock().unwrap();
1528 if let Some(a) = acc.iter_mut().find(|a| a.gateway == gateway) {
1529 a.login = LoginState::Stopped;
1530 a.status_note = Some("会话已停;『连接』可恢复".to_string());
1531 drop(acc);
1532 self.bump();
1533 }
1534 }
1535
1536 pub fn prepare_connect_form(&self, gateway: &str, form: CtpForm) -> CtpForm {
1539 let source = self.md_source();
1540 let mut f = form;
1541 if source.as_str() != gateway {
1542 f.md_addr.clear();
1543 f.md_user.clear();
1544 f.md_password.clear();
1545 }
1546 self.update_account_form(gateway, f.clone());
1547 f
1548 }
1549
1550 #[must_use]
1554 pub fn draft_from_existing(&self) -> CtpForm {
1555 let acc = self.accounts.lock().unwrap();
1556 let mut draft = CtpForm::default();
1557 if let Some(a) = acc.last() {
1558 draft.broker_id = a.form.broker_id.clone();
1559 draft.td_auth_code = a.form.td_auth_code.clone();
1560 draft.td_app_id = a.form.td_app_id.clone();
1561 draft.accept_futures = a.form.accept_futures;
1562 draft.accept_options = a.form.accept_options;
1563 }
1564 draft
1565 }
1566
1567 pub fn update_account_form(&self, gateway: &str, form: CtpForm) -> bool {
1569 let mut acc = self.accounts.lock().unwrap();
1570 match acc.iter_mut().find(|a| a.gateway == gateway) {
1571 Some(a) => {
1572 a.form = form;
1573 drop(acc);
1574 self.bump();
1575 true
1576 }
1577 None => false,
1578 }
1579 }
1580
1581 pub fn remove_account(&self, gateway: &str) -> bool {
1583 let mut acc = self.accounts.lock().unwrap();
1584 let before = acc.len();
1585 acc.retain(|a| a.gateway != gateway);
1586 let removed = acc.len() != before;
1587 if removed {
1588 let mut md = self.md_source.lock().unwrap();
1590 if md.as_str() == gateway {
1591 md.clear();
1592 }
1593 drop(acc);
1594 self.bump();
1595 }
1596 removed
1597 }
1598
1599 pub fn sync_accounts_connected(&self, rows: &[PlugRow]) -> Vec<String> {
1608 let mut newly = Vec::new();
1609 {
1610 let mut acc = self.accounts.lock().unwrap();
1611 for a in acc.iter_mut() {
1612 let up = rows.iter().any(|r| r.name == a.gateway);
1613 if up && !a.connected {
1614 newly.push(a.gateway.clone());
1615 }
1616 a.connected = up;
1617 if up {
1618 a.stage = AccountStage::Assembled;
1619 a.fail_note = None;
1620 } else if a.stage == AccountStage::Assembled {
1621 a.stage = AccountStage::NotAssembled;
1622 a.login = LoginState::Unknown;
1623 a.status_note = None;
1624 }
1625 }
1626 }
1627 if !newly.is_empty() {
1628 self.bump();
1629 }
1630 newly
1631 }
1632
1633 pub fn md_source(&self) -> String {
1635 self.md_source.lock().unwrap().clone()
1636 }
1637
1638 pub fn accounts_locked(&self) -> bool {
1640 self.accounts.lock().unwrap().iter().any(|a| a.connected)
1641 }
1642
1643 pub fn set_md_source(&self, gateway: &str) -> Result<(), String> {
1645 {
1646 let acc = self.accounts.lock().unwrap();
1647 if acc.iter().any(|a| a.connected) {
1648 return Err("元账号已锁定:有账户在线,切换行情源需先断开该账户".to_string());
1649 }
1650 if !acc.iter().any(|a| a.gateway == gateway) {
1651 return Err(format!("账户 {gateway} 不在清册"));
1652 }
1653 }
1654 let mut acc = self.accounts.lock().unwrap();
1655 for a in acc.iter_mut() {
1656 a.md_source = a.gateway == gateway;
1657 }
1658 drop(acc);
1659 let mut md = self.md_source.lock().unwrap();
1660 *md = gateway.to_string();
1661 self.bump();
1662 Ok(())
1663 }
1664
1665 pub fn subscriptions(&self) -> Vec<String> {
1667 self.subscriptions.lock().unwrap().clone()
1668 }
1669
1670 pub fn note_subscriptions(&self, symbols: &[String]) {
1672 {
1673 let mut subs = self.subscriptions.lock().unwrap();
1674 for s in symbols {
1675 let s = s.trim();
1676 if !s.is_empty() && !subs.iter().any(|x| x == s) {
1677 subs.push(s.to_string());
1678 }
1679 }
1680 }
1681 self.bump();
1682 }
1683
1684 pub fn default_gateway(&self) -> Option<String> {
1691 let chosen = {
1692 let f = self.order_form.lock().unwrap();
1693 f.gateway.trim().to_string()
1694 };
1695 if !chosen.is_empty() && self.account(&chosen).is_some_and(|a| a.connected) {
1696 return Some(chosen);
1697 }
1698 let acc = self.accounts.lock().unwrap();
1699 acc.iter().find(|a| a.connected).map(|a| a.gateway.clone())
1700 }
1701
1702 pub fn kline_symbols(&self) -> Vec<String> {
1704 self.kline_windows.lock().unwrap().clone()
1705 }
1706
1707 pub fn open_kline(&self, symbol: impl Into<String>) {
1709 let s = symbol.into();
1710 let mut v = self.kline_windows.lock().unwrap();
1711 if !v.contains(&s) {
1712 v.push(s.clone());
1713 }
1714 drop(v);
1715 self.log_line(format!("[kline] 打开 K线窗 {s}"));
1716 }
1717
1718 pub fn close_kline(&self, symbol: &str) {
1720 self.kline_windows.lock().unwrap().retain(|s| s != symbol);
1721 self.bump();
1722 }
1723
1724 pub fn contracts_window_open(&self) -> bool {
1728 self.contracts_window.load(Ordering::Acquire)
1729 }
1730
1731 pub fn set_contracts_window_open(&self, v: bool) {
1733 self.contracts_window.store(v, Ordering::Release);
1734 self.bump();
1735 }
1736
1737 pub fn update_contract(&self, c: &ek_data::contract::ContractData) {
1739 self.contract_store.lock().unwrap().put(c);
1740 self.bump();
1741 }
1742
1743 pub fn contracts(&self, pattern: &str) -> Vec<ek_data::contract::ContractData> {
1745 self.contract_store.lock().unwrap().query(pattern)
1746 }
1747
1748 pub fn contract_count(&self) -> usize {
1750 self.contract_store.lock().unwrap().len()
1751 }
1752
1753 pub fn contract_query(&self) -> String {
1755 self.contract_query.lock().unwrap().clone()
1756 }
1757
1758 pub fn set_contract_query(&self, q: impl Into<String>) {
1760 *self.contract_query.lock().unwrap() = q.into();
1761 self.bump();
1762 }
1763
1764 pub fn toggle_contract_selection(&self, en_symbol: &str) {
1766 let mut v = self.contract_selected.lock().unwrap();
1767 match v.iter().position(|s| s == en_symbol) {
1768 Some(i) => {
1769 v.remove(i);
1770 }
1771 None => v.push(en_symbol.to_string()),
1772 }
1773 self.bump();
1774 }
1775
1776 pub fn selected_contracts(&self) -> Vec<String> {
1778 self.contract_selected.lock().unwrap().clone()
1779 }
1780
1781 pub fn log_line(&self, line: impl Into<String>) {
1783 self.logs.lock().unwrap().push(line);
1784 self.bump();
1785 }
1786
1787 pub fn clear_logs(&self) {
1789 self.logs.lock().unwrap().clear();
1790 self.bump();
1791 }
1792
1793 pub fn logs_snapshot(&self) -> (Vec<String>, u64) {
1795 let r = self.logs.lock().unwrap();
1796 (r.snapshot(), r.spilled())
1797 }
1798
1799 pub fn logs_entries(&self) -> (Vec<LogEntry>, u64) {
1801 let r = self.logs.lock().unwrap();
1802 (r.entries(), r.spilled())
1803 }
1804
1805 pub fn logs_len(&self) -> usize {
1807 self.logs.lock().unwrap().len()
1808 }
1809
1810 pub fn set_snapshot(&self, snap: BusSnap) {
1812 *self.snap.lock().unwrap() = Arc::new(snap);
1813 self.bump();
1814 }
1815
1816 pub fn snapshot(&self) -> Arc<BusSnap> {
1818 Arc::clone(&self.snap.lock().unwrap())
1819 }
1820
1821 pub fn rev(&self) -> u64 {
1823 self.rev.load(Ordering::Acquire)
1824 }
1825
1826 pub fn bump(&self) {
1828 self.rev.fetch_add(1, Ordering::AcqRel);
1829 }
1830
1831 pub fn request_stop(&self) {
1833 self.stop.store(true, Ordering::Release);
1834 }
1835
1836 pub fn is_stop(&self) -> bool {
1838 self.stop.load(Ordering::Acquire)
1839 }
1840
1841 pub fn push_cmd(&self, cmd: UiCmd) {
1845 self.cmds.lock().unwrap().push_back(cmd);
1846 self.bump();
1847 }
1848
1849 pub fn drain_cmds(&self) -> Vec<UiCmd> {
1851 self.cmds.lock().unwrap().drain(..).collect()
1852 }
1853
1854 pub fn pending_confirm(&self) -> Option<String> {
1856 self.pending_confirm.lock().unwrap().clone()
1857 }
1858
1859 pub fn confirm_step(&self, name: &str) -> ConfirmStep {
1861 let mut g = self.pending_confirm.lock().unwrap();
1862 match g.take() {
1863 Some(p) if p == name => ConfirmStep::Fired,
1864 Some(_) => {
1865 *g = Some(name.to_string());
1866 ConfirmStep::Switched
1867 }
1868 None => {
1869 *g = Some(name.to_string());
1870 ConfirmStep::Armed
1871 }
1872 }
1873 }
1874
1875 pub fn confirm_cancel(&self) {
1877 *self.pending_confirm.lock().unwrap() = None;
1878 self.bump();
1879 }
1880
1881 pub fn market(&self) -> &MarketStore {
1885 &self.market
1886 }
1887
1888 pub fn set_trade_snap(&self, ts: TradeSnap) {
1892 *self.trade_snap.lock().unwrap() = Arc::new(ts);
1893 self.bump();
1894 }
1895
1896 pub fn trade_snap(&self) -> Arc<TradeSnap> {
1898 self.trade_snap.lock().unwrap().clone()
1899 }
1900
1901 pub fn ctp_form(&self) -> CtpForm {
1905 self.ctp_form.lock().unwrap().clone()
1906 }
1907
1908 pub fn set_ctp_form(&self, f: CtpForm) {
1910 *self.ctp_form.lock().unwrap() = f;
1911 self.bump();
1912 }
1913
1914 pub fn row_selected(&self, table: &str, key: &str) -> bool {
1918 self.row_sel
1919 .lock()
1920 .unwrap()
1921 .get(table)
1922 .is_some_and(|k| k == key)
1923 }
1924
1925 pub fn select_row(&self, table: &str, key: &str) {
1927 self.row_sel
1928 .lock()
1929 .unwrap()
1930 .insert(table.to_string(), key.to_string());
1931 }
1932}