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 CtpConfigure(CtpForm),
119 CtpSubscribe { symbols: Vec<String> },
121}
122
123pub const KLINE_CELL_CAP: usize = 4096;
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128pub enum ConfirmStep {
129 Armed,
130 Fired,
131 Switched,
132}
133
134#[must_use]
137pub fn is_quarantined(state: &str) -> bool {
138 state.contains("Quarantin")
139}
140
141#[derive(Debug, Default)]
145pub struct MarketStore {
146 cells: Mutex<BTreeMap<(String, i64), Vec<BarData>>>,
147 frames: AtomicU64,
149 updates: AtomicU64,
151 reloads: AtomicU64,
153}
154
155#[derive(Debug, Clone, PartialEq)]
157pub struct CellView {
158 pub symbol: String,
159 pub interval: i64,
160 pub bars: Vec<BarData>,
161}
162
163impl MarketStore {
164 pub fn on_frame(&self, frame: &BarsFrame) {
166 let mut cells = self.cells.lock().unwrap();
167 for b in &frame.bars {
168 let key = (b.symbol.clone(), frame.interval);
169 let cell = cells.entry(key).or_default();
170 match cell.last() {
171 Some(last) if b.datetime < last.datetime => continue,
172 Some(last) if b.datetime == last.datetime => {
173 let slot = cell.last_mut().expect("len>1");
174 *slot = b.clone();
175 }
176 _ => {
177 cell.push(b.clone());
178 if cell.len() > KLINE_CELL_CAP {
179 cell.remove(0);
180 }
181 }
182 }
183 }
184 self.frames.fetch_add(1, Ordering::AcqRel);
185 }
186
187 pub fn on_update(&self, b: &BarData) {
189 let mut cells = self.cells.lock().unwrap();
190 let iv = b
191 .interval
192 .unwrap_or(ek_data::constant::Interval::MINUTE)
193 .seconds();
194 let key = (b.symbol.clone(), iv);
195 let cell = cells.entry(key).or_default();
196 match cell.last() {
197 Some(last) if b.datetime == last.datetime => {
198 let slot = cell.last_mut().expect("len>1");
199 *slot = b.clone();
200 }
201 Some(last) if b.datetime < last.datetime => {}
202 _ => {
203 cell.push(b.clone());
204 if cell.len() > KLINE_CELL_CAP {
205 cell.remove(0);
206 }
207 }
208 }
209 self.updates.fetch_add(1, Ordering::AcqRel);
210 }
211
212 pub fn note_reload(&self) {
214 self.reloads.fetch_add(1, Ordering::AcqRel);
215 }
216
217 pub fn cells(&self) -> Vec<CellView> {
219 self.cells
220 .lock()
221 .unwrap()
222 .iter()
223 .map(|((sym, iv), bars)| CellView {
224 symbol: sym.clone(),
225 interval: *iv,
226 bars: bars.clone(),
227 })
228 .collect()
229 }
230
231 pub fn counts(&self) -> (u64, u64, u64) {
233 (
234 self.frames.load(Ordering::Acquire),
235 self.updates.load(Ordering::Acquire),
236 self.reloads.load(Ordering::Acquire),
237 )
238 }
239}
240
241#[derive(Debug, Default, Clone, PartialEq)]
244pub struct ReloadForm {
245 pub interval: String,
246 pub symbol: String,
247 pub csv: String,
248}
249
250pub fn parse_gateway(raw: &str) -> Result<String, String> {
253 let g = raw.trim();
254 if g.is_empty() {
255 return Ok("ctp".to_string());
256 }
257 if g.chars().any(|c| c.is_whitespace()) {
258 return Err(format!("网关解析: 含空白:{raw:?}"));
259 }
260 Ok(g.to_string())
261}
262
263#[derive(Debug, Default, Clone, PartialEq)]
266pub struct CtpForm {
267 pub broker_id: String,
268 pub td_addr: String,
269 pub td_user: String,
270 pub td_password: String,
271 pub td_auth_code: String,
272 pub td_app_id: String,
273 pub md_addr: String,
274 pub md_user: String,
275 pub md_password: String,
276 pub symbols: String,
278}
279
280impl CtpForm {
281 pub fn to_configure_req(&self) -> ek_ctp::plugin::ConfigureReq {
283 ek_ctp::plugin::ConfigureReq {
284 broker_id: self.broker_id.trim().to_string(),
285 td_addr: self.td_addr.trim().to_string(),
286 td_user: self.td_user.trim().to_string(),
287 td_password: self.td_password.clone(),
288 td_auth_code: self.td_auth_code.trim().to_string(),
289 td_app_id: self.td_app_id.trim().to_string(),
290 md_addr: self.md_addr.trim().to_string(),
291 md_user: self.md_user.trim().to_string(),
292 md_password: self.md_password.clone(),
293 }
294 }
295
296 pub fn parse_symbols(&self) -> Result<Vec<String>, String> {
298 let list: Vec<String> = self
299 .symbols
300 .split([',', ',', ' '])
301 .map(|s| s.trim().to_string())
302 .filter(|s| !s.is_empty())
303 .collect();
304 if list.is_empty() {
305 return Err("订阅解析: 标的位空".to_string());
306 }
307 Ok(list)
308 }
309}
310
311#[derive(Debug, Clone, PartialEq, Default)]
313pub struct TradeSnap {
314 pub orders: Vec<ek_data::order::OrderData>,
315 pub trades: Vec<ek_data::trade::TradeData>,
316 pub positions: Vec<ek_data::position::PositionData>,
317 pub accounts: Vec<ek_data::account::AccountData>,
319}
320
321pub fn parse_reload_csv(form: &ReloadForm) -> Result<(i64, String, Vec<BarData>), String> {
324 let interval: i64 = form
325 .interval
326 .trim()
327 .parse()
328 .map_err(|_| format!("回灌 解析: interval 非整数:{:?}", form.interval))?;
329 let symbol = form.symbol.trim().to_string();
330 if symbol.is_empty() {
331 return Err("回灌解析: symbol 空".to_string());
332 }
333 if form.csv.trim().is_empty() {
334 return Err("回灌解析: csv 空".to_string());
335 }
336 let mut bars = Vec::new();
337 for (i, raw) in form.csv.lines().enumerate() {
338 let line = raw.trim();
339 if line.is_empty() || line.starts_with('#') {
340 continue;
341 }
342 let parts: Vec<&str> = line.split(',').map(str::trim).collect();
343 if parts.len() != 6 {
344 return Err(format!("回灌解析: 第 {} 列数 ≠ 6", i + 1));
345 }
346 let num = |s: &str| -> Result<f64, String> {
347 s.parse::<f64>()
348 .map_err(|_| format!("回灌解析: 第 {} 列数值坏:{:?}", i + 1, s))
349 };
350 let dt_s: i64 = parts[0]
351 .parse()
352 .map_err(|_| format!("回灌解析: 第 {} 列 dt 坏:{:?}", i + 1, parts[0]))?;
353 bars.push(BarData {
354 symbol: symbol.clone(),
355 datetime: dt_s * 1_000_000_000,
356 interval: Some(
357 ek_dm::wire::interval_of_seconds(interval)
358 .unwrap_or(ek_data::constant::Interval::MINUTE),
359 ),
360 open_price: num(parts[1])?,
361 high_price: num(parts[2])?,
362 low_price: num(parts[3])?,
363 close_price: num(parts[4])?,
364 volume: num(parts[5])?,
365 ..BarData::default()
366 });
367 }
368 if bars.is_empty() {
369 return Err("回灌解析: 无有效行".to_string());
370 }
371 Ok((interval, symbol, bars))
372}
373
374#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
379pub enum TopLeftTab {
380 #[default]
381 Orders,
382 Active,
383}
384
385impl TopLeftTab {
386 pub fn label(self) -> &'static str {
387 match self {
388 Self::Orders => "委托",
389 Self::Active => "未成交",
390 }
391 }
392}
393
394#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
396pub enum BottomRightTab {
397 #[default]
398 Account,
399 Logs,
400}
401
402impl BottomRightTab {
403 pub fn label(self) -> &'static str {
404 match self {
405 Self::Account => "资金",
406 Self::Logs => "日志",
407 }
408 }
409}
410
411#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
415pub enum Theme {
416 #[default]
418 FinanceDark,
419 EguiDark,
421 EguiLight,
423 QtClassic,
425}
426
427impl Theme {
428 pub fn label(self) -> &'static str {
429 match self {
430 Self::FinanceDark => "金融深色",
431 Self::EguiDark => "egui深色",
432 Self::EguiLight => "egui浅色",
433 Self::QtClassic => "Qt灰白",
434 }
435 }
436
437 pub fn next(self) -> Self {
439 match self {
440 Self::FinanceDark => Self::EguiDark,
441 Self::EguiDark => Self::EguiLight,
442 Self::EguiLight => Self::QtClassic,
443 Self::QtClassic => Self::FinanceDark,
444 }
445 }
446}
447
448#[derive(Debug, Default)]
451pub struct TickStore {
452 ticks: BTreeMap<String, ek_data::tick::TickData>,
453}
454
455impl TickStore {
456 pub fn put(&mut self, t: &ek_data::tick::TickData) {
459 self.ticks
460 .insert(format!("{}.{}", t.exchange.as_str(), t.symbol), t.clone());
461 }
462
463 pub fn rows(&self) -> Vec<ek_data::tick::TickData> {
465 let mut v: Vec<ek_data::tick::TickData> = self.ticks.values().cloned().collect();
466 v.sort_by(|a, b| {
467 (a.exchange.as_str().to_string(), a.symbol.clone())
468 .cmp(&(b.exchange.as_str().to_string(), b.symbol.clone()))
469 });
470 v
471 }
472
473 pub fn groups(&self) -> Vec<(String, Vec<ek_data::tick::TickData>)> {
475 let mut out: Vec<(String, Vec<ek_data::tick::TickData>)> = Vec::new();
476 for t in self.rows() {
477 let ex = t.exchange.as_str().to_string();
478 match out.last_mut() {
479 Some((e, v)) if *e == ex => v.push(t),
480 _ => out.push((ex, vec![t])),
481 }
482 }
483 out
484 }
485
486 pub fn len(&self) -> usize {
487 self.ticks.len()
488 }
489
490 pub fn is_empty(&self) -> bool {
491 self.ticks.is_empty()
492 }
493}
494
495#[derive(Debug, Clone, PartialEq)]
497pub struct OrderForm {
498 pub exchange: String,
500 pub symbol: String,
502 pub direction: ek_data::constant::Direction,
503 pub offset: ek_data::constant::Offset,
504 pub order_type: ek_data::constant::OrderType,
505 pub price: String,
507 pub volume: String,
508 pub gateway: String,
510 pub follow_price: bool,
512 pub auto_split: bool,
515}
516
517impl Default for OrderForm {
518 fn default() -> Self {
519 Self {
520 exchange: "SHFE".to_string(),
521 symbol: String::new(),
522 direction: ek_data::constant::Direction::LONG,
523 offset: ek_data::constant::Offset::OPEN,
524 order_type: ek_data::constant::OrderType::LIMIT,
525 price: String::new(),
526 volume: "1".to_string(),
527 gateway: String::new(),
528 follow_price: true,
529 auto_split: true,
530 }
531 }
532}
533
534impl OrderForm {
535 pub fn to_request(&self) -> Result<ek_data::order_request::OrderRequest, String> {
537 use std::str::FromStr;
538 let symbol = self.symbol.trim();
539 if symbol.is_empty() {
540 return Err("下单: 代码为空".to_string());
541 }
542 if symbol.contains('.') {
543 return Err("下单: 代码不带交易所前缀(交易所走下拉)".to_string());
544 }
545 let exchange = ek_data::constant::Exchange::from_str(self.exchange.trim())
546 .map_err(|e| format!("下单: {e}"))?;
547 let price: f64 = self
548 .price
549 .trim()
550 .parse()
551 .map_err(|_| format!("下单: 价格无效 {:?}", self.price))?;
552 let volume: f64 = self
553 .volume
554 .trim()
555 .parse()
556 .map_err(|_| format!("下单: 数量无效 {:?}", self.volume))?;
557 if self.order_type != ek_data::constant::OrderType::MARKET && price <= 0.0 {
558 return Err("下单: 限价单价格须为正".to_string());
559 }
560 if volume <= 0.0 {
561 return Err("下单: 数量须为正".to_string());
562 }
563 Ok(ek_data::order_request::OrderRequest {
564 symbol: symbol.to_string(),
565 exchange,
566 direction: self.direction,
567 order_type: self.order_type,
568 volume,
569 price,
570 offset: self.offset,
571 ..ek_data::order_request::OrderRequest::default()
572 })
573 }
574}
575
576#[derive(Debug, Default)]
582pub struct ContractStore {
583 contracts: BTreeMap<String, ek_data::contract::ContractData>,
584}
585
586impl ContractStore {
587 pub fn put(&mut self, c: &ek_data::contract::ContractData) {
588 self.contracts
589 .insert(format!("{}.{}", c.exchange.as_str(), c.symbol), c.clone());
590 }
591
592 pub fn query(&self, pattern: &str) -> Vec<ek_data::contract::ContractData> {
594 let p = pattern.trim();
595 self.contracts
596 .values()
597 .filter(|c| {
598 p.is_empty()
599 || c.symbol.contains(p)
600 || c.exchange.as_str().eq_ignore_ascii_case(p)
601 || c.name.contains(p)
602 })
603 .cloned()
604 .collect()
605 }
606
607 pub fn len(&self) -> usize {
608 self.contracts.len()
609 }
610
611 pub fn is_empty(&self) -> bool {
612 self.contracts.is_empty()
613 }
614}
615
616#[derive(Debug, Default)]
618pub struct UiShared {
619 logs: Mutex<LogRing>,
620 snap: Mutex<Arc<BusSnap>>,
621 market: MarketStore,
622 ctp_form: Mutex<CtpForm>,
623 trade_snap: Mutex<Arc<TradeSnap>>,
624 tick_store: Mutex<TickStore>,
625 order_form: Mutex<OrderForm>,
626 selected_exchange: Mutex<String>,
627 topleft_tab: Mutex<TopLeftTab>,
628 bottomright_tab: Mutex<BottomRightTab>,
629 ops_window: AtomicBool,
630 ctp_window: AtomicBool,
631 contracts_window: AtomicBool,
632 theme: AtomicU8,
634 contract_store: Mutex<ContractStore>,
635 contract_query: Mutex<String>,
636 contract_selected: Mutex<Vec<String>>,
637 kline_windows: Mutex<Vec<String>>,
638 rev: AtomicU64,
639 stop: AtomicBool,
641 cmds: Mutex<VecDeque<UiCmd>>,
643 pending_confirm: Mutex<Option<String>>,
645 row_sel: Mutex<BTreeMap<String, String>>,
648}
649
650impl UiShared {
651 pub fn new() -> Self {
653 Self::default()
654 }
655
656 pub fn update_tick(&self, t: &ek_data::tick::TickData) {
660 self.tick_store.lock().unwrap().put(t);
661 self.bump();
662 }
663
664 pub fn tick_rows(&self) -> Vec<ek_data::tick::TickData> {
666 self.tick_store.lock().unwrap().rows()
667 }
668
669 pub fn tick_groups(&self) -> Vec<(String, Vec<ek_data::tick::TickData>)> {
671 self.tick_store.lock().unwrap().groups()
672 }
673
674 pub fn tick_count(&self) -> usize {
676 self.tick_store.lock().unwrap().len()
677 }
678
679 pub fn order_form(&self) -> OrderForm {
681 self.order_form.lock().unwrap().clone()
682 }
683
684 pub fn set_order_form(&self, f: OrderForm) {
686 *self.order_form.lock().unwrap() = f;
687 self.bump();
688 }
689
690 pub fn selected_exchange(&self) -> String {
692 self.selected_exchange.lock().unwrap().clone()
693 }
694
695 pub fn set_selected_exchange(&self, s: impl Into<String>) {
697 *self.selected_exchange.lock().unwrap() = s.into();
698 self.bump();
699 }
700
701 pub fn topleft_tab(&self) -> TopLeftTab {
703 *self.topleft_tab.lock().unwrap()
704 }
705
706 pub fn set_topleft_tab(&self, t: TopLeftTab) {
708 *self.topleft_tab.lock().unwrap() = t;
709 self.bump();
710 }
711
712 pub fn bottomright_tab(&self) -> BottomRightTab {
714 *self.bottomright_tab.lock().unwrap()
715 }
716
717 pub fn set_bottomright_tab(&self, t: BottomRightTab) {
719 *self.bottomright_tab.lock().unwrap() = t;
720 self.bump();
721 }
722
723 pub fn theme(&self) -> Theme {
727 match self.theme.load(Ordering::Acquire) {
728 1 => Theme::EguiDark,
729 2 => Theme::EguiLight,
730 3 => Theme::QtClassic,
731 _ => Theme::FinanceDark,
732 }
733 }
734
735 pub fn set_theme(&self, t: Theme) {
737 self.theme.store(t as u8, Ordering::Release);
738 self.bump();
739 }
740
741 pub fn cycle_theme(&self) -> Theme {
743 let t = self.theme().next();
744 self.set_theme(t);
745 t
746 }
747
748 pub fn ops_window_open(&self) -> bool {
750 self.ops_window.load(Ordering::Acquire)
751 }
752
753 pub fn set_ops_window_open(&self, v: bool) {
755 self.ops_window.store(v, Ordering::Release);
756 self.bump();
757 }
758
759 pub fn ctp_window_open(&self) -> bool {
761 self.ctp_window.load(Ordering::Acquire)
762 }
763
764 pub fn set_ctp_window_open(&self, v: bool) {
766 self.ctp_window.store(v, Ordering::Release);
767 self.bump();
768 }
769
770 pub fn kline_symbols(&self) -> Vec<String> {
772 self.kline_windows.lock().unwrap().clone()
773 }
774
775 pub fn open_kline(&self, symbol: impl Into<String>) {
777 let s = symbol.into();
778 let mut v = self.kline_windows.lock().unwrap();
779 if !v.contains(&s) {
780 v.push(s.clone());
781 }
782 drop(v);
783 self.log_line(format!("[kline] 打开 K线窗 {s}"));
784 }
785
786 pub fn close_kline(&self, symbol: &str) {
788 self.kline_windows.lock().unwrap().retain(|s| s != symbol);
789 self.bump();
790 }
791
792 pub fn contracts_window_open(&self) -> bool {
796 self.contracts_window.load(Ordering::Acquire)
797 }
798
799 pub fn set_contracts_window_open(&self, v: bool) {
801 self.contracts_window.store(v, Ordering::Release);
802 self.bump();
803 }
804
805 pub fn update_contract(&self, c: &ek_data::contract::ContractData) {
807 self.contract_store.lock().unwrap().put(c);
808 self.bump();
809 }
810
811 pub fn contracts(&self, pattern: &str) -> Vec<ek_data::contract::ContractData> {
813 self.contract_store.lock().unwrap().query(pattern)
814 }
815
816 pub fn contract_count(&self) -> usize {
818 self.contract_store.lock().unwrap().len()
819 }
820
821 pub fn contract_query(&self) -> String {
823 self.contract_query.lock().unwrap().clone()
824 }
825
826 pub fn set_contract_query(&self, q: impl Into<String>) {
828 *self.contract_query.lock().unwrap() = q.into();
829 self.bump();
830 }
831
832 pub fn toggle_contract_selection(&self, en_symbol: &str) {
834 let mut v = self.contract_selected.lock().unwrap();
835 match v.iter().position(|s| s == en_symbol) {
836 Some(i) => {
837 v.remove(i);
838 }
839 None => v.push(en_symbol.to_string()),
840 }
841 self.bump();
842 }
843
844 pub fn selected_contracts(&self) -> Vec<String> {
846 self.contract_selected.lock().unwrap().clone()
847 }
848
849 pub fn log_line(&self, line: impl Into<String>) {
851 self.logs.lock().unwrap().push(line);
852 self.bump();
853 }
854
855 pub fn clear_logs(&self) {
857 self.logs.lock().unwrap().clear();
858 self.bump();
859 }
860
861 pub fn logs_snapshot(&self) -> (Vec<String>, u64) {
863 let r = self.logs.lock().unwrap();
864 (r.snapshot(), r.spilled())
865 }
866
867 pub fn logs_len(&self) -> usize {
869 self.logs.lock().unwrap().len()
870 }
871
872 pub fn set_snapshot(&self, snap: BusSnap) {
874 *self.snap.lock().unwrap() = Arc::new(snap);
875 self.bump();
876 }
877
878 pub fn snapshot(&self) -> Arc<BusSnap> {
880 Arc::clone(&self.snap.lock().unwrap())
881 }
882
883 pub fn rev(&self) -> u64 {
885 self.rev.load(Ordering::Acquire)
886 }
887
888 pub fn bump(&self) {
890 self.rev.fetch_add(1, Ordering::AcqRel);
891 }
892
893 pub fn request_stop(&self) {
895 self.stop.store(true, Ordering::Release);
896 }
897
898 pub fn is_stop(&self) -> bool {
900 self.stop.load(Ordering::Acquire)
901 }
902
903 pub fn push_cmd(&self, cmd: UiCmd) {
907 self.cmds.lock().unwrap().push_back(cmd);
908 self.bump();
909 }
910
911 pub fn drain_cmds(&self) -> Vec<UiCmd> {
913 self.cmds.lock().unwrap().drain(..).collect()
914 }
915
916 pub fn pending_confirm(&self) -> Option<String> {
918 self.pending_confirm.lock().unwrap().clone()
919 }
920
921 pub fn confirm_step(&self, name: &str) -> ConfirmStep {
923 let mut g = self.pending_confirm.lock().unwrap();
924 match g.take() {
925 Some(p) if p == name => ConfirmStep::Fired,
926 Some(_) => {
927 *g = Some(name.to_string());
928 ConfirmStep::Switched
929 }
930 None => {
931 *g = Some(name.to_string());
932 ConfirmStep::Armed
933 }
934 }
935 }
936
937 pub fn confirm_cancel(&self) {
939 *self.pending_confirm.lock().unwrap() = None;
940 self.bump();
941 }
942
943 pub fn market(&self) -> &MarketStore {
947 &self.market
948 }
949
950 pub fn set_trade_snap(&self, ts: TradeSnap) {
954 *self.trade_snap.lock().unwrap() = Arc::new(ts);
955 self.bump();
956 }
957
958 pub fn trade_snap(&self) -> Arc<TradeSnap> {
960 self.trade_snap.lock().unwrap().clone()
961 }
962
963 pub fn ctp_form(&self) -> CtpForm {
967 self.ctp_form.lock().unwrap().clone()
968 }
969
970 pub fn set_ctp_form(&self, f: CtpForm) {
972 *self.ctp_form.lock().unwrap() = f;
973 self.bump();
974 }
975
976 pub fn row_selected(&self, table: &str, key: &str) -> bool {
980 self.row_sel
981 .lock()
982 .unwrap()
983 .get(table)
984 .is_some_and(|k| k == key)
985 }
986
987 pub fn select_row(&self, table: &str, key: &str) {
989 self.row_sel
990 .lock()
991 .unwrap()
992 .insert(table.to_string(), key.to_string());
993 }
994}