1use std::collections::{BTreeMap, VecDeque};
7use std::sync::atomic::{AtomicBool, AtomicU8, 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
17#[derive(Debug)]
19pub struct LogRing {
20 lines: VecDeque<String>,
21 cap: usize,
22 spilled: u64,
23}
24
25impl Default for LogRing {
26 fn default() -> Self {
27 Self::new(RING_CAP)
28 }
29}
30
31impl LogRing {
32 pub fn new(cap: usize) -> Self {
34 Self {
35 lines: VecDeque::with_capacity(cap),
36 cap,
37 spilled: 0,
38 }
39 }
40
41 pub fn push(&mut self, line: impl Into<String>) {
43 if self.cap == 0 {
44 self.spilled += 1;
45 return;
46 }
47 if self.lines.len() == self.cap {
48 self.lines.pop_front();
49 self.spilled += 1;
50 }
51 self.lines.push_back(line.into());
52 }
53
54 pub fn len(&self) -> usize {
56 self.lines.len()
57 }
58
59 pub fn is_empty(&self) -> bool {
61 self.lines.is_empty()
62 }
63
64 pub fn spilled(&self) -> u64 {
66 self.spilled
67 }
68
69 pub fn snapshot(&self) -> Vec<String> {
71 self.lines.iter().cloned().collect()
72 }
73
74 pub fn clear(&mut self) {
76 self.lines.clear();
77 }
78}
79
80#[derive(Debug, Clone, Default)]
82pub struct BusSnap {
83 pub rows: Vec<PlugRow>,
85 pub services: Vec<(String, Vec<String>)>,
87}
88
89#[derive(Debug, Clone, PartialEq)]
91pub enum UiCmd {
92 Start { factory: String },
94 Remove { name: String },
96 Reload {
98 interval: i64,
99 symbol: String,
100 bars: Vec<BarData>,
101 },
102 SendOrder {
108 req: ek_data::order_request::OrderRequest,
109 gateway: String,
110 mode: ek_oms::LockMode,
111 },
112 CancelOrder { orderid: String, gateway: String },
116 QueryRate,
118 CtpConfigure(CtpForm),
121 CtpSubscribe { symbols: Vec<String> },
123}
124
125pub const KLINE_CELL_CAP: usize = 4096;
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130pub enum ConfirmStep {
131 Armed,
132 Fired,
133 Switched,
134}
135
136#[must_use]
139pub fn is_quarantined(state: &str) -> bool {
140 state.contains("Quarantin")
141}
142
143#[derive(Debug, Default)]
147pub struct MarketStore {
148 cells: Mutex<BTreeMap<(String, i64), Vec<BarData>>>,
149 frames: AtomicU64,
151 updates: AtomicU64,
153 reloads: AtomicU64,
155}
156
157#[derive(Debug, Clone, PartialEq)]
159pub struct CellView {
160 pub symbol: String,
161 pub interval: i64,
162 pub bars: Vec<BarData>,
163}
164
165impl MarketStore {
166 pub fn on_frame(&self, frame: &BarsFrame) {
168 let mut cells = self.cells.lock().unwrap();
169 for b in &frame.bars {
170 let key = (b.symbol.clone(), frame.interval);
171 let cell = cells.entry(key).or_default();
172 match cell.last() {
173 Some(last) if b.datetime < last.datetime => continue,
174 Some(last) if b.datetime == last.datetime => {
175 let slot = cell.last_mut().expect("len>1");
176 *slot = b.clone();
177 }
178 _ => {
179 cell.push(b.clone());
180 if cell.len() > KLINE_CELL_CAP {
181 cell.remove(0);
182 }
183 }
184 }
185 }
186 self.frames.fetch_add(1, Ordering::AcqRel);
187 }
188
189 pub fn on_update(&self, b: &BarData) {
191 let mut cells = self.cells.lock().unwrap();
192 let iv = b
193 .interval
194 .unwrap_or(ek_data::constant::Interval::MINUTE)
195 .seconds();
196 let key = (b.symbol.clone(), iv);
197 let cell = cells.entry(key).or_default();
198 match cell.last() {
199 Some(last) if b.datetime == last.datetime => {
200 let slot = cell.last_mut().expect("len>1");
201 *slot = b.clone();
202 }
203 Some(last) if b.datetime < last.datetime => {}
204 _ => {
205 cell.push(b.clone());
206 if cell.len() > KLINE_CELL_CAP {
207 cell.remove(0);
208 }
209 }
210 }
211 self.updates.fetch_add(1, Ordering::AcqRel);
212 }
213
214 pub fn note_reload(&self) {
216 self.reloads.fetch_add(1, Ordering::AcqRel);
217 }
218
219 pub fn cells(&self) -> Vec<CellView> {
221 self.cells
222 .lock()
223 .unwrap()
224 .iter()
225 .map(|((sym, iv), bars)| CellView {
226 symbol: sym.clone(),
227 interval: *iv,
228 bars: bars.clone(),
229 })
230 .collect()
231 }
232
233 pub fn counts(&self) -> (u64, u64, u64) {
235 (
236 self.frames.load(Ordering::Acquire),
237 self.updates.load(Ordering::Acquire),
238 self.reloads.load(Ordering::Acquire),
239 )
240 }
241}
242
243#[derive(Debug, Default, Clone, PartialEq)]
246pub struct ReloadForm {
247 pub interval: String,
248 pub symbol: String,
249 pub csv: String,
250}
251
252pub fn parse_gateway(raw: &str) -> Result<String, String> {
254 let g = raw.trim();
255 if g.is_empty() {
256 return Ok("sim".to_string());
257 }
258 if g.chars().any(|c| c.is_whitespace()) {
259 return Err(format!("网关解析: 含空白:{raw:?}"));
260 }
261 Ok(g.to_string())
262}
263
264#[derive(Debug, Default, Clone, PartialEq)]
267pub struct CtpForm {
268 pub broker_id: String,
269 pub td_addr: String,
270 pub td_user: String,
271 pub td_password: String,
272 pub td_auth_code: String,
273 pub td_app_id: String,
274 pub md_addr: String,
275 pub md_user: String,
276 pub md_password: String,
277 pub symbols: String,
279}
280
281impl CtpForm {
282 pub fn to_configure_req(&self) -> ek_ctp::plugin::ConfigureReq {
284 ek_ctp::plugin::ConfigureReq {
285 broker_id: self.broker_id.trim().to_string(),
286 td_addr: self.td_addr.trim().to_string(),
287 td_user: self.td_user.trim().to_string(),
288 td_password: self.td_password.clone(),
289 td_auth_code: self.td_auth_code.trim().to_string(),
290 td_app_id: self.td_app_id.trim().to_string(),
291 md_addr: self.md_addr.trim().to_string(),
292 md_user: self.md_user.trim().to_string(),
293 md_password: self.md_password.clone(),
294 }
295 }
296
297 pub fn parse_symbols(&self) -> Result<Vec<String>, String> {
299 let list: Vec<String> = self
300 .symbols
301 .split([',', ',', ' '])
302 .map(|s| s.trim().to_string())
303 .filter(|s| !s.is_empty())
304 .collect();
305 if list.is_empty() {
306 return Err("订阅解析: 标的位空".to_string());
307 }
308 Ok(list)
309 }
310}
311
312#[derive(Debug, Clone, PartialEq, Default)]
314pub struct TradeSnap {
315 pub orders: Vec<ek_data::order::OrderData>,
316 pub trades: Vec<ek_data::trade::TradeData>,
317 pub positions: Vec<ek_data::position::PositionData>,
318 pub accounts: Vec<ek_data::account::AccountData>,
320}
321
322pub fn parse_reload_csv(form: &ReloadForm) -> Result<(i64, String, Vec<BarData>), String> {
325 let interval: i64 = form
326 .interval
327 .trim()
328 .parse()
329 .map_err(|_| format!("回灌 解析: interval 非整数:{:?}", form.interval))?;
330 let symbol = form.symbol.trim().to_string();
331 if symbol.is_empty() {
332 return Err("回灌解析: symbol 空".to_string());
333 }
334 if form.csv.trim().is_empty() {
335 return Err("回灌解析: csv 空".to_string());
336 }
337 let mut bars = Vec::new();
338 for (i, raw) in form.csv.lines().enumerate() {
339 let line = raw.trim();
340 if line.is_empty() || line.starts_with('#') {
341 continue;
342 }
343 let parts: Vec<&str> = line.split(',').map(str::trim).collect();
344 if parts.len() != 6 {
345 return Err(format!("回灌解析: 第 {} 列数 ≠ 6", i + 1));
346 }
347 let num = |s: &str| -> Result<f64, String> {
348 s.parse::<f64>()
349 .map_err(|_| format!("回灌解析: 第 {} 列数值坏:{:?}", i + 1, s))
350 };
351 let dt_s: i64 = parts[0]
352 .parse()
353 .map_err(|_| format!("回灌解析: 第 {} 列 dt 坏:{:?}", i + 1, parts[0]))?;
354 bars.push(BarData {
355 symbol: symbol.clone(),
356 datetime: dt_s * 1_000_000_000,
357 interval: Some(
358 ek_dm::wire::interval_of_seconds(interval)
359 .unwrap_or(ek_data::constant::Interval::MINUTE),
360 ),
361 open_price: num(parts[1])?,
362 high_price: num(parts[2])?,
363 low_price: num(parts[3])?,
364 close_price: num(parts[4])?,
365 volume: num(parts[5])?,
366 ..BarData::default()
367 });
368 }
369 if bars.is_empty() {
370 return Err("回灌解析: 无有效行".to_string());
371 }
372 Ok((interval, symbol, bars))
373}
374
375#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
380pub enum TopLeftTab {
381 #[default]
382 Orders,
383 Active,
384}
385
386impl TopLeftTab {
387 pub fn label(self) -> &'static str {
388 match self {
389 Self::Orders => "委托",
390 Self::Active => "未成交",
391 }
392 }
393}
394
395#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
397pub enum BottomRightTab {
398 #[default]
399 Account,
400 Logs,
401}
402
403impl BottomRightTab {
404 pub fn label(self) -> &'static str {
405 match self {
406 Self::Account => "资金",
407 Self::Logs => "日志",
408 }
409 }
410}
411
412#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
416pub enum Theme {
417 #[default]
419 FinanceDark,
420 EguiDark,
422 EguiLight,
424 QtClassic,
426}
427
428impl Theme {
429 pub fn label(self) -> &'static str {
430 match self {
431 Self::FinanceDark => "金融深色",
432 Self::EguiDark => "egui深色",
433 Self::EguiLight => "egui浅色",
434 Self::QtClassic => "Qt灰白",
435 }
436 }
437
438 pub fn next(self) -> Self {
440 match self {
441 Self::FinanceDark => Self::EguiDark,
442 Self::EguiDark => Self::EguiLight,
443 Self::EguiLight => Self::QtClassic,
444 Self::QtClassic => Self::FinanceDark,
445 }
446 }
447}
448
449#[derive(Debug, Default)]
452pub struct TickStore {
453 ticks: BTreeMap<String, ek_data::tick::TickData>,
454}
455
456impl TickStore {
457 pub fn put(&mut self, t: &ek_data::tick::TickData) {
460 self.ticks
461 .insert(format!("{}.{}", t.exchange.as_str(), t.symbol), t.clone());
462 }
463
464 pub fn rows(&self) -> Vec<ek_data::tick::TickData> {
466 let mut v: Vec<ek_data::tick::TickData> = self.ticks.values().cloned().collect();
467 v.sort_by(|a, b| {
468 (a.exchange.as_str().to_string(), a.symbol.clone())
469 .cmp(&(b.exchange.as_str().to_string(), b.symbol.clone()))
470 });
471 v
472 }
473
474 pub fn groups(&self) -> Vec<(String, Vec<ek_data::tick::TickData>)> {
476 let mut out: Vec<(String, Vec<ek_data::tick::TickData>)> = Vec::new();
477 for t in self.rows() {
478 let ex = t.exchange.as_str().to_string();
479 match out.last_mut() {
480 Some((e, v)) if *e == ex => v.push(t),
481 _ => out.push((ex, vec![t])),
482 }
483 }
484 out
485 }
486
487 pub fn len(&self) -> usize {
488 self.ticks.len()
489 }
490
491 pub fn is_empty(&self) -> bool {
492 self.ticks.is_empty()
493 }
494}
495
496#[derive(Debug, Clone, PartialEq)]
498pub struct OrderForm {
499 pub exchange: String,
501 pub symbol: String,
503 pub direction: ek_data::constant::Direction,
504 pub offset: ek_data::constant::Offset,
505 pub order_type: ek_data::constant::OrderType,
506 pub price: String,
508 pub volume: String,
509 pub gateway: String,
511 pub follow_price: bool,
513 pub auto_split: bool,
516}
517
518impl Default for OrderForm {
519 fn default() -> Self {
520 Self {
521 exchange: "SHFE".to_string(),
522 symbol: String::new(),
523 direction: ek_data::constant::Direction::LONG,
524 offset: ek_data::constant::Offset::OPEN,
525 order_type: ek_data::constant::OrderType::LIMIT,
526 price: String::new(),
527 volume: "1".to_string(),
528 gateway: String::new(),
529 follow_price: true,
530 auto_split: true,
531 }
532 }
533}
534
535impl OrderForm {
536 pub fn to_request(&self) -> Result<ek_data::order_request::OrderRequest, String> {
538 use std::str::FromStr;
539 let symbol = self.symbol.trim();
540 if symbol.is_empty() {
541 return Err("下单: 代码为空".to_string());
542 }
543 if symbol.contains('.') {
544 return Err("下单: 代码不带交易所前缀(交易所走下拉)".to_string());
545 }
546 let exchange = ek_data::constant::Exchange::from_str(self.exchange.trim())
547 .map_err(|e| format!("下单: {e}"))?;
548 let price: f64 = self
549 .price
550 .trim()
551 .parse()
552 .map_err(|_| format!("下单: 价格无效 {:?}", self.price))?;
553 let volume: f64 = self
554 .volume
555 .trim()
556 .parse()
557 .map_err(|_| format!("下单: 数量无效 {:?}", self.volume))?;
558 if self.order_type != ek_data::constant::OrderType::MARKET && price <= 0.0 {
559 return Err("下单: 限价单价格须为正".to_string());
560 }
561 if volume <= 0.0 {
562 return Err("下单: 数量须为正".to_string());
563 }
564 Ok(ek_data::order_request::OrderRequest {
565 symbol: symbol.to_string(),
566 exchange,
567 direction: self.direction,
568 order_type: self.order_type,
569 volume,
570 price,
571 offset: self.offset,
572 ..ek_data::order_request::OrderRequest::default()
573 })
574 }
575}
576
577#[derive(Debug, Default)]
583pub struct ContractStore {
584 contracts: BTreeMap<String, ek_data::contract::ContractData>,
585}
586
587impl ContractStore {
588 pub fn put(&mut self, c: &ek_data::contract::ContractData) {
589 self.contracts
590 .insert(format!("{}.{}", c.exchange.as_str(), c.symbol), c.clone());
591 }
592
593 pub fn query(&self, pattern: &str) -> Vec<ek_data::contract::ContractData> {
595 let p = pattern.trim();
596 self.contracts
597 .values()
598 .filter(|c| {
599 p.is_empty()
600 || c.symbol.contains(p)
601 || c.exchange.as_str().eq_ignore_ascii_case(p)
602 || c.name.contains(p)
603 })
604 .cloned()
605 .collect()
606 }
607
608 pub fn len(&self) -> usize {
609 self.contracts.len()
610 }
611
612 pub fn is_empty(&self) -> bool {
613 self.contracts.is_empty()
614 }
615}
616
617#[derive(Debug, Default)]
619pub struct UiShared {
620 logs: Mutex<LogRing>,
621 snap: Mutex<Arc<BusSnap>>,
622 market: MarketStore,
623 ctp_form: Mutex<CtpForm>,
624 trade_snap: Mutex<Arc<TradeSnap>>,
625 tick_store: Mutex<TickStore>,
626 order_form: Mutex<OrderForm>,
627 selected_exchange: Mutex<String>,
628 topleft_tab: Mutex<TopLeftTab>,
629 bottomright_tab: Mutex<BottomRightTab>,
630 ops_window: AtomicBool,
631 ctp_window: AtomicBool,
632 contracts_window: AtomicBool,
633 theme: AtomicU8,
635 contract_store: Mutex<ContractStore>,
636 contract_query: Mutex<String>,
637 contract_selected: Mutex<Vec<String>>,
638 kline_windows: Mutex<Vec<String>>,
639 rev: AtomicU64,
640 stop: AtomicBool,
642 cmds: Mutex<VecDeque<UiCmd>>,
644 pending_confirm: Mutex<Option<String>>,
646 row_sel: Mutex<BTreeMap<String, String>>,
649}
650
651impl UiShared {
652 pub fn new() -> Self {
654 Self::default()
655 }
656
657 pub fn update_tick(&self, t: &ek_data::tick::TickData) {
661 self.tick_store.lock().unwrap().put(t);
662 self.bump();
663 }
664
665 pub fn tick_rows(&self) -> Vec<ek_data::tick::TickData> {
667 self.tick_store.lock().unwrap().rows()
668 }
669
670 pub fn tick_groups(&self) -> Vec<(String, Vec<ek_data::tick::TickData>)> {
672 self.tick_store.lock().unwrap().groups()
673 }
674
675 pub fn tick_count(&self) -> usize {
677 self.tick_store.lock().unwrap().len()
678 }
679
680 pub fn order_form(&self) -> OrderForm {
682 self.order_form.lock().unwrap().clone()
683 }
684
685 pub fn set_order_form(&self, f: OrderForm) {
687 *self.order_form.lock().unwrap() = f;
688 self.bump();
689 }
690
691 pub fn selected_exchange(&self) -> String {
693 self.selected_exchange.lock().unwrap().clone()
694 }
695
696 pub fn set_selected_exchange(&self, s: impl Into<String>) {
698 *self.selected_exchange.lock().unwrap() = s.into();
699 self.bump();
700 }
701
702 pub fn topleft_tab(&self) -> TopLeftTab {
704 *self.topleft_tab.lock().unwrap()
705 }
706
707 pub fn set_topleft_tab(&self, t: TopLeftTab) {
709 *self.topleft_tab.lock().unwrap() = t;
710 self.bump();
711 }
712
713 pub fn bottomright_tab(&self) -> BottomRightTab {
715 *self.bottomright_tab.lock().unwrap()
716 }
717
718 pub fn set_bottomright_tab(&self, t: BottomRightTab) {
720 *self.bottomright_tab.lock().unwrap() = t;
721 self.bump();
722 }
723
724 pub fn theme(&self) -> Theme {
728 match self.theme.load(Ordering::Acquire) {
729 1 => Theme::EguiDark,
730 2 => Theme::EguiLight,
731 3 => Theme::QtClassic,
732 _ => Theme::FinanceDark,
733 }
734 }
735
736 pub fn set_theme(&self, t: Theme) {
738 self.theme.store(t as u8, Ordering::Release);
739 self.bump();
740 }
741
742 pub fn cycle_theme(&self) -> Theme {
744 let t = self.theme().next();
745 self.set_theme(t);
746 t
747 }
748
749 pub fn ops_window_open(&self) -> bool {
751 self.ops_window.load(Ordering::Acquire)
752 }
753
754 pub fn set_ops_window_open(&self, v: bool) {
756 self.ops_window.store(v, Ordering::Release);
757 self.bump();
758 }
759
760 pub fn ctp_window_open(&self) -> bool {
762 self.ctp_window.load(Ordering::Acquire)
763 }
764
765 pub fn set_ctp_window_open(&self, v: bool) {
767 self.ctp_window.store(v, Ordering::Release);
768 self.bump();
769 }
770
771 pub fn kline_symbols(&self) -> Vec<String> {
773 self.kline_windows.lock().unwrap().clone()
774 }
775
776 pub fn open_kline(&self, symbol: impl Into<String>) {
778 let s = symbol.into();
779 let mut v = self.kline_windows.lock().unwrap();
780 if !v.contains(&s) {
781 v.push(s.clone());
782 }
783 drop(v);
784 self.log_line(format!("[kline] 打开 K线窗 {s}"));
785 }
786
787 pub fn close_kline(&self, symbol: &str) {
789 self.kline_windows.lock().unwrap().retain(|s| s != symbol);
790 self.bump();
791 }
792
793 pub fn contracts_window_open(&self) -> bool {
797 self.contracts_window.load(Ordering::Acquire)
798 }
799
800 pub fn set_contracts_window_open(&self, v: bool) {
802 self.contracts_window.store(v, Ordering::Release);
803 self.bump();
804 }
805
806 pub fn update_contract(&self, c: &ek_data::contract::ContractData) {
808 self.contract_store.lock().unwrap().put(c);
809 self.bump();
810 }
811
812 pub fn contracts(&self, pattern: &str) -> Vec<ek_data::contract::ContractData> {
814 self.contract_store.lock().unwrap().query(pattern)
815 }
816
817 pub fn contract_count(&self) -> usize {
819 self.contract_store.lock().unwrap().len()
820 }
821
822 pub fn contract_query(&self) -> String {
824 self.contract_query.lock().unwrap().clone()
825 }
826
827 pub fn set_contract_query(&self, q: impl Into<String>) {
829 *self.contract_query.lock().unwrap() = q.into();
830 self.bump();
831 }
832
833 pub fn toggle_contract_selection(&self, en_symbol: &str) {
835 let mut v = self.contract_selected.lock().unwrap();
836 match v.iter().position(|s| s == en_symbol) {
837 Some(i) => {
838 v.remove(i);
839 }
840 None => v.push(en_symbol.to_string()),
841 }
842 self.bump();
843 }
844
845 pub fn selected_contracts(&self) -> Vec<String> {
847 self.contract_selected.lock().unwrap().clone()
848 }
849
850 pub fn log_line(&self, line: impl Into<String>) {
852 self.logs.lock().unwrap().push(line);
853 self.bump();
854 }
855
856 pub fn clear_logs(&self) {
858 self.logs.lock().unwrap().clear();
859 self.bump();
860 }
861
862 pub fn logs_snapshot(&self) -> (Vec<String>, u64) {
864 let r = self.logs.lock().unwrap();
865 (r.snapshot(), r.spilled())
866 }
867
868 pub fn logs_len(&self) -> usize {
870 self.logs.lock().unwrap().len()
871 }
872
873 pub fn set_snapshot(&self, snap: BusSnap) {
875 *self.snap.lock().unwrap() = Arc::new(snap);
876 self.bump();
877 }
878
879 pub fn snapshot(&self) -> Arc<BusSnap> {
881 Arc::clone(&self.snap.lock().unwrap())
882 }
883
884 pub fn rev(&self) -> u64 {
886 self.rev.load(Ordering::Acquire)
887 }
888
889 pub fn bump(&self) {
891 self.rev.fetch_add(1, Ordering::AcqRel);
892 }
893
894 pub fn request_stop(&self) {
896 self.stop.store(true, Ordering::Release);
897 }
898
899 pub fn is_stop(&self) -> bool {
901 self.stop.load(Ordering::Acquire)
902 }
903
904 pub fn push_cmd(&self, cmd: UiCmd) {
908 self.cmds.lock().unwrap().push_back(cmd);
909 self.bump();
910 }
911
912 pub fn drain_cmds(&self) -> Vec<UiCmd> {
914 self.cmds.lock().unwrap().drain(..).collect()
915 }
916
917 pub fn pending_confirm(&self) -> Option<String> {
919 self.pending_confirm.lock().unwrap().clone()
920 }
921
922 pub fn confirm_step(&self, name: &str) -> ConfirmStep {
924 let mut g = self.pending_confirm.lock().unwrap();
925 match g.take() {
926 Some(p) if p == name => ConfirmStep::Fired,
927 Some(_) => {
928 *g = Some(name.to_string());
929 ConfirmStep::Switched
930 }
931 None => {
932 *g = Some(name.to_string());
933 ConfirmStep::Armed
934 }
935 }
936 }
937
938 pub fn confirm_cancel(&self) {
940 *self.pending_confirm.lock().unwrap() = None;
941 self.bump();
942 }
943
944 pub fn market(&self) -> &MarketStore {
948 &self.market
949 }
950
951 pub fn set_trade_snap(&self, ts: TradeSnap) {
955 *self.trade_snap.lock().unwrap() = Arc::new(ts);
956 self.bump();
957 }
958
959 pub fn trade_snap(&self) -> Arc<TradeSnap> {
961 self.trade_snap.lock().unwrap().clone()
962 }
963
964 pub fn ctp_form(&self) -> CtpForm {
968 self.ctp_form.lock().unwrap().clone()
969 }
970
971 pub fn set_ctp_form(&self, f: CtpForm) {
973 *self.ctp_form.lock().unwrap() = f;
974 self.bump();
975 }
976
977 pub fn row_selected(&self, table: &str, key: &str) -> bool {
981 self.row_sel
982 .lock()
983 .unwrap()
984 .get(table)
985 .is_some_and(|k| k == key)
986 }
987
988 pub fn select_row(&self, table: &str, key: &str) {
990 self.row_sel
991 .lock()
992 .unwrap()
993 .insert(table.to_string(), key.to_string());
994 }
995}