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, name: String },
95 Remove { name: String },
97 Reload {
99 interval: i64,
100 symbol: String,
101 bars: Vec<BarData>,
102 },
103 SendOrder {
109 req: ek_data::order_request::OrderRequest,
110 gateway: String,
111 mode: ek_oms::LockMode,
112 },
113 CancelOrder { orderid: String, gateway: String },
117 CtpConfigure { gateway: String, form: CtpForm },
120 CtpSubscribe {
123 gateway: String,
124 symbols: Vec<String>,
125 },
126}
127
128pub const KLINE_CELL_CAP: usize = 4096;
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub enum ConfirmStep {
134 Armed,
135 Fired,
136 Switched,
137}
138
139#[must_use]
142pub fn is_quarantined(state: &str) -> bool {
143 state.contains("Quarantin")
144}
145
146#[derive(Debug, Default)]
150pub struct MarketStore {
151 cells: Mutex<BTreeMap<(String, i64), Vec<BarData>>>,
152 frames: AtomicU64,
154 updates: AtomicU64,
156 reloads: AtomicU64,
158}
159
160#[derive(Debug, Clone, PartialEq)]
162pub struct CellView {
163 pub symbol: String,
164 pub interval: i64,
165 pub bars: Vec<BarData>,
166}
167
168impl MarketStore {
169 pub fn on_frame(&self, frame: &BarsFrame) {
171 let mut cells = self.cells.lock().unwrap();
172 for b in &frame.bars {
173 let key = (b.symbol.clone(), frame.interval);
174 let cell = cells.entry(key).or_default();
175 match cell.last() {
176 Some(last) if b.datetime < last.datetime => continue,
177 Some(last) if b.datetime == last.datetime => {
178 let slot = cell.last_mut().expect("len>1");
179 *slot = b.clone();
180 }
181 _ => {
182 cell.push(b.clone());
183 if cell.len() > KLINE_CELL_CAP {
184 cell.remove(0);
185 }
186 }
187 }
188 }
189 self.frames.fetch_add(1, Ordering::AcqRel);
190 }
191
192 pub fn on_update(&self, b: &BarData) {
194 let mut cells = self.cells.lock().unwrap();
195 let iv = b
196 .interval
197 .unwrap_or(ek_data::constant::Interval::MINUTE)
198 .seconds();
199 let key = (b.symbol.clone(), iv);
200 let cell = cells.entry(key).or_default();
201 match cell.last() {
202 Some(last) if b.datetime == last.datetime => {
203 let slot = cell.last_mut().expect("len>1");
204 *slot = b.clone();
205 }
206 Some(last) if b.datetime < last.datetime => {}
207 _ => {
208 cell.push(b.clone());
209 if cell.len() > KLINE_CELL_CAP {
210 cell.remove(0);
211 }
212 }
213 }
214 self.updates.fetch_add(1, Ordering::AcqRel);
215 }
216
217 pub fn note_reload(&self) {
219 self.reloads.fetch_add(1, Ordering::AcqRel);
220 }
221
222 pub fn cells(&self) -> Vec<CellView> {
224 self.cells
225 .lock()
226 .unwrap()
227 .iter()
228 .map(|((sym, iv), bars)| CellView {
229 symbol: sym.clone(),
230 interval: *iv,
231 bars: bars.clone(),
232 })
233 .collect()
234 }
235
236 pub fn counts(&self) -> (u64, u64, u64) {
238 (
239 self.frames.load(Ordering::Acquire),
240 self.updates.load(Ordering::Acquire),
241 self.reloads.load(Ordering::Acquire),
242 )
243 }
244}
245
246#[derive(Debug, Default, Clone, PartialEq)]
249pub struct ReloadForm {
250 pub interval: String,
251 pub symbol: String,
252 pub csv: String,
253}
254
255pub fn parse_gateway(raw: &str, fallback: Option<&str>) -> Result<String, String> {
262 let g = raw.trim();
263 if !g.is_empty() {
264 if g.chars().any(|c| c.is_whitespace()) {
265 return Err(format!("网关解析: 含空白:{raw:?}"));
266 }
267 return Ok(g.to_string());
268 }
269 match fallback.map(|f| f.trim()).filter(|f| !f.is_empty()) {
270 Some(f) => Ok(f.to_string()),
271 None => Err("网关解析: 未选择账户(先在账户面板添加账户)".to_string()),
272 }
273}
274
275#[derive(Debug, Clone, PartialEq)]
278pub struct CtpForm {
279 pub broker_id: String,
280 pub td_addr: String,
281 pub td_user: String,
282 pub td_password: String,
283 pub td_auth_code: String,
284 pub td_app_id: String,
285 pub md_addr: String,
286 pub md_user: String,
287 pub md_password: String,
288 pub symbols: String,
290 pub accept_futures: bool,
293 pub accept_options: bool,
294}
295
296impl Default for CtpForm {
299 fn default() -> Self {
300 Self {
301 broker_id: String::new(),
302 td_addr: String::new(),
303 td_user: String::new(),
304 td_password: String::new(),
305 td_auth_code: String::new(),
306 td_app_id: String::new(),
307 md_addr: String::new(),
308 md_user: String::new(),
309 md_password: String::new(),
310 symbols: String::new(),
311 accept_futures: true,
312 accept_options: false,
313 }
314 }
315}
316
317impl CtpForm {
318 #[must_use]
322 pub fn product_classes(&self) -> Vec<String> {
323 let mut codes = Vec::new();
324 if self.accept_futures {
325 codes.push("1".to_string());
326 }
327 if self.accept_options {
328 codes.push("2".to_string());
329 }
330 codes
331 }
332
333 pub fn to_configure_req(&self) -> ek_ctp::plugin::ConfigureReq {
335 ek_ctp::plugin::ConfigureReq {
336 broker_id: self.broker_id.trim().to_string(),
337 td_addr: self.td_addr.trim().to_string(),
338 td_user: self.td_user.trim().to_string(),
339 td_password: self.td_password.clone(),
340 td_auth_code: self.td_auth_code.trim().to_string(),
341 td_app_id: self.td_app_id.trim().to_string(),
342 md_addr: self.md_addr.trim().to_string(),
343 md_user: self.md_user.trim().to_string(),
344 md_password: self.md_password.clone(),
345 product_classes: self.product_classes(),
346 }
347 }
348
349 pub fn parse_symbols(&self) -> Result<Vec<String>, String> {
351 let list: Vec<String> = self
352 .symbols
353 .split([',', ',', ' '])
354 .map(|s| s.trim().to_string())
355 .filter(|s| !s.is_empty())
356 .collect();
357 if list.is_empty() {
358 return Err("订阅解析: 标的位空".to_string());
359 }
360 Ok(list)
361 }
362}
363
364#[derive(Debug, Clone, PartialEq, Default)]
369pub struct AccountRow {
370 pub gateway: String,
372 pub broker_id: String,
373 pub account: String,
374 pub md_source: bool,
376 pub form: CtpForm,
378 pub connected: bool,
381}
382
383impl AccountRow {
384 pub fn derive(form: &CtpForm) -> Result<Self, String> {
389 let broker = form.broker_id.trim();
390 let account = form.td_user.trim();
391 let gateway = ek_ctp::derived_gateway_name(broker, account).ok_or_else(|| {
392 "账户: broker/账号 不能为空且只能含字母数字(且派生名 ≤32)".to_string()
393 })?;
394 Ok(Self {
395 gateway,
396 broker_id: broker.to_string(),
397 account: account.to_string(),
398 md_source: false,
399 form: form.clone(),
400 connected: false,
401 })
402 }
403}
404
405#[derive(Debug, Clone, PartialEq, Default)]
407pub struct TradeSnap {
408 pub orders: Vec<ek_data::order::OrderData>,
409 pub trades: Vec<ek_data::trade::TradeData>,
410 pub positions: Vec<ek_data::position::PositionData>,
411 pub accounts: Vec<ek_data::account::AccountData>,
413}
414
415pub fn parse_reload_csv(form: &ReloadForm) -> Result<(i64, String, Vec<BarData>), String> {
418 let interval: i64 = form
419 .interval
420 .trim()
421 .parse()
422 .map_err(|_| format!("回灌 解析: interval 非整数:{:?}", form.interval))?;
423 let symbol = form.symbol.trim().to_string();
424 if symbol.is_empty() {
425 return Err("回灌解析: symbol 空".to_string());
426 }
427 if form.csv.trim().is_empty() {
428 return Err("回灌解析: csv 空".to_string());
429 }
430 let mut bars = Vec::new();
431 for (i, raw) in form.csv.lines().enumerate() {
432 let line = raw.trim();
433 if line.is_empty() || line.starts_with('#') {
434 continue;
435 }
436 let parts: Vec<&str> = line.split(',').map(str::trim).collect();
437 if parts.len() != 6 {
438 return Err(format!("回灌解析: 第 {} 列数 ≠ 6", i + 1));
439 }
440 let num = |s: &str| -> Result<f64, String> {
441 s.parse::<f64>()
442 .map_err(|_| format!("回灌解析: 第 {} 列数值坏:{:?}", i + 1, s))
443 };
444 let dt_s: i64 = parts[0]
445 .parse()
446 .map_err(|_| format!("回灌解析: 第 {} 列 dt 坏:{:?}", i + 1, parts[0]))?;
447 bars.push(BarData {
448 symbol: symbol.clone(),
449 datetime: dt_s * 1_000_000_000,
450 interval: Some(
451 ek_dm::wire::interval_of_seconds(interval)
452 .unwrap_or(ek_data::constant::Interval::MINUTE),
453 ),
454 open_price: num(parts[1])?,
455 high_price: num(parts[2])?,
456 low_price: num(parts[3])?,
457 close_price: num(parts[4])?,
458 volume: num(parts[5])?,
459 ..BarData::default()
460 });
461 }
462 if bars.is_empty() {
463 return Err("回灌解析: 无有效行".to_string());
464 }
465 Ok((interval, symbol, bars))
466}
467
468#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
473pub enum TopLeftTab {
474 #[default]
475 Orders,
476 Active,
477}
478
479impl TopLeftTab {
480 pub fn label(self) -> &'static str {
481 match self {
482 Self::Orders => "委托",
483 Self::Active => "未成交",
484 }
485 }
486}
487
488#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
490pub enum BottomRightTab {
491 #[default]
492 Account,
493 Logs,
494}
495
496impl BottomRightTab {
497 pub fn label(self) -> &'static str {
498 match self {
499 Self::Account => "资金",
500 Self::Logs => "日志",
501 }
502 }
503}
504
505#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
509pub enum Theme {
510 #[default]
512 FinanceDark,
513 EguiDark,
515 EguiLight,
517 QtClassic,
519}
520
521impl Theme {
522 pub fn label(self) -> &'static str {
523 match self {
524 Self::FinanceDark => "金融深色",
525 Self::EguiDark => "egui深色",
526 Self::EguiLight => "egui浅色",
527 Self::QtClassic => "Qt灰白",
528 }
529 }
530
531 pub fn next(self) -> Self {
533 match self {
534 Self::FinanceDark => Self::EguiDark,
535 Self::EguiDark => Self::EguiLight,
536 Self::EguiLight => Self::QtClassic,
537 Self::QtClassic => Self::FinanceDark,
538 }
539 }
540}
541
542#[derive(Debug, Default)]
545pub struct TickStore {
546 ticks: BTreeMap<String, ek_data::tick::TickData>,
547}
548
549impl TickStore {
550 pub fn put(&mut self, t: &ek_data::tick::TickData) {
553 self.ticks
554 .insert(format!("{}.{}", t.exchange.as_str(), t.symbol), t.clone());
555 }
556
557 pub fn rows(&self) -> Vec<ek_data::tick::TickData> {
559 let mut v: Vec<ek_data::tick::TickData> = self.ticks.values().cloned().collect();
560 v.sort_by(|a, b| {
561 (a.exchange.as_str().to_string(), a.symbol.clone())
562 .cmp(&(b.exchange.as_str().to_string(), b.symbol.clone()))
563 });
564 v
565 }
566
567 pub fn groups(&self) -> Vec<(String, Vec<ek_data::tick::TickData>)> {
569 let mut out: Vec<(String, Vec<ek_data::tick::TickData>)> = Vec::new();
570 for t in self.rows() {
571 let ex = t.exchange.as_str().to_string();
572 match out.last_mut() {
573 Some((e, v)) if *e == ex => v.push(t),
574 _ => out.push((ex, vec![t])),
575 }
576 }
577 out
578 }
579
580 pub fn len(&self) -> usize {
581 self.ticks.len()
582 }
583
584 pub fn is_empty(&self) -> bool {
585 self.ticks.is_empty()
586 }
587}
588
589#[derive(Debug, Clone, PartialEq)]
591pub struct OrderForm {
592 pub exchange: String,
594 pub symbol: String,
596 pub direction: ek_data::constant::Direction,
597 pub offset: ek_data::constant::Offset,
598 pub order_type: ek_data::constant::OrderType,
599 pub price: String,
601 pub volume: String,
602 pub gateway: String,
604 pub follow_price: bool,
606 pub auto_split: bool,
609}
610
611impl Default for OrderForm {
612 fn default() -> Self {
613 Self {
614 exchange: "SHFE".to_string(),
615 symbol: String::new(),
616 direction: ek_data::constant::Direction::LONG,
617 offset: ek_data::constant::Offset::OPEN,
618 order_type: ek_data::constant::OrderType::LIMIT,
619 price: String::new(),
620 volume: "1".to_string(),
621 gateway: String::new(),
622 follow_price: true,
623 auto_split: true,
624 }
625 }
626}
627
628impl OrderForm {
629 pub fn to_request(&self) -> Result<ek_data::order_request::OrderRequest, String> {
631 use std::str::FromStr;
632 let symbol = self.symbol.trim();
633 if symbol.is_empty() {
634 return Err("下单: 代码为空".to_string());
635 }
636 if symbol.contains('.') {
637 return Err("下单: 代码不带交易所前缀(交易所走下拉)".to_string());
638 }
639 let exchange = ek_data::constant::Exchange::from_str(self.exchange.trim())
640 .map_err(|e| format!("下单: {e}"))?;
641 let price: f64 = self
642 .price
643 .trim()
644 .parse()
645 .map_err(|_| format!("下单: 价格无效 {:?}", self.price))?;
646 let volume: f64 = self
647 .volume
648 .trim()
649 .parse()
650 .map_err(|_| format!("下单: 数量无效 {:?}", self.volume))?;
651 if self.order_type != ek_data::constant::OrderType::MARKET && price <= 0.0 {
652 return Err("下单: 限价单价格须为正".to_string());
653 }
654 if volume <= 0.0 {
655 return Err("下单: 数量须为正".to_string());
656 }
657 Ok(ek_data::order_request::OrderRequest {
658 symbol: symbol.to_string(),
659 exchange,
660 direction: self.direction,
661 order_type: self.order_type,
662 volume,
663 price,
664 offset: self.offset,
665 ..ek_data::order_request::OrderRequest::default()
666 })
667 }
668}
669
670#[derive(Debug, Default)]
676pub struct ContractStore {
677 contracts: BTreeMap<String, ek_data::contract::ContractData>,
678}
679
680impl ContractStore {
681 pub fn put(&mut self, c: &ek_data::contract::ContractData) {
682 self.contracts
683 .insert(format!("{}.{}", c.exchange.as_str(), c.symbol), c.clone());
684 }
685
686 pub fn query(&self, pattern: &str) -> Vec<ek_data::contract::ContractData> {
688 let p = pattern.trim();
689 self.contracts
690 .values()
691 .filter(|c| {
692 p.is_empty()
693 || c.symbol.contains(p)
694 || c.exchange.as_str().eq_ignore_ascii_case(p)
695 || c.name.contains(p)
696 })
697 .cloned()
698 .collect()
699 }
700
701 pub fn len(&self) -> usize {
702 self.contracts.len()
703 }
704
705 pub fn is_empty(&self) -> bool {
706 self.contracts.is_empty()
707 }
708}
709
710#[derive(Debug, Default)]
712pub struct UiShared {
713 logs: Mutex<LogRing>,
714 snap: Mutex<Arc<BusSnap>>,
715 market: MarketStore,
716 ctp_form: Mutex<CtpForm>,
717 trade_snap: Mutex<Arc<TradeSnap>>,
718 tick_store: Mutex<TickStore>,
719 order_form: Mutex<OrderForm>,
720 selected_exchange: Mutex<String>,
721 topleft_tab: Mutex<TopLeftTab>,
722 bottomright_tab: Mutex<BottomRightTab>,
723 ops_window: AtomicBool,
724 accounts_window: AtomicBool,
727 contracts_window: AtomicBool,
728 theme: AtomicU8,
730 contract_store: Mutex<ContractStore>,
731 contract_query: Mutex<String>,
732 contract_selected: Mutex<Vec<String>>,
733 kline_windows: Mutex<Vec<String>>,
734 rev: AtomicU64,
735 stop: AtomicBool,
737 cmds: Mutex<VecDeque<UiCmd>>,
739 pending_confirm: Mutex<Option<String>>,
741 accounts: Mutex<Vec<AccountRow>>,
743 md_source: Mutex<String>,
745 subscriptions: Mutex<Vec<String>>,
747 row_sel: Mutex<BTreeMap<String, String>>,
750}
751
752impl UiShared {
753 pub fn new() -> Self {
755 Self::default()
756 }
757
758 pub fn update_tick(&self, t: &ek_data::tick::TickData) {
762 self.tick_store.lock().unwrap().put(t);
763 self.bump();
764 }
765
766 pub fn tick_rows(&self) -> Vec<ek_data::tick::TickData> {
768 self.tick_store.lock().unwrap().rows()
769 }
770
771 pub fn tick_groups(&self) -> Vec<(String, Vec<ek_data::tick::TickData>)> {
773 self.tick_store.lock().unwrap().groups()
774 }
775
776 pub fn tick_count(&self) -> usize {
778 self.tick_store.lock().unwrap().len()
779 }
780
781 pub fn order_form(&self) -> OrderForm {
783 self.order_form.lock().unwrap().clone()
784 }
785
786 pub fn set_order_form(&self, f: OrderForm) {
788 *self.order_form.lock().unwrap() = f;
789 self.bump();
790 }
791
792 pub fn selected_exchange(&self) -> String {
794 self.selected_exchange.lock().unwrap().clone()
795 }
796
797 pub fn set_selected_exchange(&self, s: impl Into<String>) {
799 *self.selected_exchange.lock().unwrap() = s.into();
800 self.bump();
801 }
802
803 pub fn topleft_tab(&self) -> TopLeftTab {
805 *self.topleft_tab.lock().unwrap()
806 }
807
808 pub fn set_topleft_tab(&self, t: TopLeftTab) {
810 *self.topleft_tab.lock().unwrap() = t;
811 self.bump();
812 }
813
814 pub fn bottomright_tab(&self) -> BottomRightTab {
816 *self.bottomright_tab.lock().unwrap()
817 }
818
819 pub fn set_bottomright_tab(&self, t: BottomRightTab) {
821 *self.bottomright_tab.lock().unwrap() = t;
822 self.bump();
823 }
824
825 pub fn theme(&self) -> Theme {
829 match self.theme.load(Ordering::Acquire) {
830 1 => Theme::EguiDark,
831 2 => Theme::EguiLight,
832 3 => Theme::QtClassic,
833 _ => Theme::FinanceDark,
834 }
835 }
836
837 pub fn set_theme(&self, t: Theme) {
839 self.theme.store(t as u8, Ordering::Release);
840 self.bump();
841 }
842
843 pub fn cycle_theme(&self) -> Theme {
845 let t = self.theme().next();
846 self.set_theme(t);
847 t
848 }
849
850 pub fn ops_window_open(&self) -> bool {
852 self.ops_window.load(Ordering::Acquire)
853 }
854
855 pub fn set_ops_window_open(&self, v: bool) {
857 self.ops_window.store(v, Ordering::Release);
858 self.bump();
859 }
860
861 pub fn accounts_window_open(&self) -> bool {
865 self.accounts_window.load(Ordering::Acquire)
866 }
867
868 pub fn set_accounts_window_open(&self, v: bool) {
870 self.accounts_window.store(v, Ordering::Release);
871 self.bump();
872 }
873
874 pub fn accounts(&self) -> Vec<AccountRow> {
876 self.accounts.lock().unwrap().clone()
877 }
878
879 pub fn account(&self, gateway: &str) -> Option<AccountRow> {
881 self.accounts
882 .lock()
883 .unwrap()
884 .iter()
885 .find(|a| a.gateway == gateway)
886 .cloned()
887 }
888
889 pub fn add_account(&self, row: AccountRow) -> Result<(), String> {
894 let mut acc = self.accounts.lock().unwrap();
895 if acc.iter().any(|a| a.gateway == row.gateway) {
896 return Err(format!("账户 {} 已存在(同 broker + 账号)", row.gateway));
897 }
898 acc.push(row);
899 drop(acc);
900 self.bump();
901 Ok(())
902 }
903
904 pub fn update_account_form(&self, gateway: &str, form: CtpForm) -> bool {
906 let mut acc = self.accounts.lock().unwrap();
907 match acc.iter_mut().find(|a| a.gateway == gateway) {
908 Some(a) => {
909 a.form = form;
910 drop(acc);
911 self.bump();
912 true
913 }
914 None => false,
915 }
916 }
917
918 pub fn remove_account(&self, gateway: &str) -> bool {
920 let mut acc = self.accounts.lock().unwrap();
921 let before = acc.len();
922 acc.retain(|a| a.gateway != gateway);
923 let removed = acc.len() != before;
924 if removed {
925 let mut md = self.md_source.lock().unwrap();
927 if md.as_str() == gateway {
928 md.clear();
929 }
930 drop(acc);
931 self.bump();
932 }
933 removed
934 }
935
936 pub fn sync_accounts_connected(&self, rows: &[PlugRow]) -> Vec<String> {
938 let mut newly = Vec::new();
939 {
940 let mut acc = self.accounts.lock().unwrap();
941 for a in acc.iter_mut() {
942 let up = rows.iter().any(|r| r.name == a.gateway);
943 if up && !a.connected {
944 newly.push(a.gateway.clone());
945 }
946 a.connected = up;
947 }
948 }
949 if !newly.is_empty() {
950 self.bump();
951 }
952 newly
953 }
954
955 pub fn md_source(&self) -> String {
957 self.md_source.lock().unwrap().clone()
958 }
959
960 pub fn accounts_locked(&self) -> bool {
962 self.accounts.lock().unwrap().iter().any(|a| a.connected)
963 }
964
965 pub fn set_md_source(&self, gateway: &str) -> Result<(), String> {
967 {
968 let acc = self.accounts.lock().unwrap();
969 if acc.iter().any(|a| a.connected) {
970 return Err("元账号已锁定:有账户在线,切换行情源需先断开该账户".to_string());
971 }
972 if !acc.iter().any(|a| a.gateway == gateway) {
973 return Err(format!("账户 {gateway} 不在清册"));
974 }
975 }
976 let mut acc = self.accounts.lock().unwrap();
977 for a in acc.iter_mut() {
978 a.md_source = a.gateway == gateway;
979 }
980 drop(acc);
981 let mut md = self.md_source.lock().unwrap();
982 *md = gateway.to_string();
983 self.bump();
984 Ok(())
985 }
986
987 pub fn subscriptions(&self) -> Vec<String> {
989 self.subscriptions.lock().unwrap().clone()
990 }
991
992 pub fn note_subscriptions(&self, symbols: &[String]) {
994 {
995 let mut subs = self.subscriptions.lock().unwrap();
996 for s in symbols {
997 let s = s.trim();
998 if !s.is_empty() && !subs.iter().any(|x| x == s) {
999 subs.push(s.to_string());
1000 }
1001 }
1002 }
1003 self.bump();
1004 }
1005
1006 pub fn default_gateway(&self) -> Option<String> {
1010 let chosen = {
1011 let f = self.order_form.lock().unwrap();
1012 f.gateway.trim().to_string()
1013 };
1014 if !chosen.is_empty() {
1015 return Some(chosen);
1016 }
1017 let acc = self.accounts.lock().unwrap();
1018 acc.iter()
1019 .find(|a| a.connected)
1020 .or_else(|| acc.first())
1021 .map(|a| a.gateway.clone())
1022 }
1023
1024 pub fn kline_symbols(&self) -> Vec<String> {
1026 self.kline_windows.lock().unwrap().clone()
1027 }
1028
1029 pub fn open_kline(&self, symbol: impl Into<String>) {
1031 let s = symbol.into();
1032 let mut v = self.kline_windows.lock().unwrap();
1033 if !v.contains(&s) {
1034 v.push(s.clone());
1035 }
1036 drop(v);
1037 self.log_line(format!("[kline] 打开 K线窗 {s}"));
1038 }
1039
1040 pub fn close_kline(&self, symbol: &str) {
1042 self.kline_windows.lock().unwrap().retain(|s| s != symbol);
1043 self.bump();
1044 }
1045
1046 pub fn contracts_window_open(&self) -> bool {
1050 self.contracts_window.load(Ordering::Acquire)
1051 }
1052
1053 pub fn set_contracts_window_open(&self, v: bool) {
1055 self.contracts_window.store(v, Ordering::Release);
1056 self.bump();
1057 }
1058
1059 pub fn update_contract(&self, c: &ek_data::contract::ContractData) {
1061 self.contract_store.lock().unwrap().put(c);
1062 self.bump();
1063 }
1064
1065 pub fn contracts(&self, pattern: &str) -> Vec<ek_data::contract::ContractData> {
1067 self.contract_store.lock().unwrap().query(pattern)
1068 }
1069
1070 pub fn contract_count(&self) -> usize {
1072 self.contract_store.lock().unwrap().len()
1073 }
1074
1075 pub fn contract_query(&self) -> String {
1077 self.contract_query.lock().unwrap().clone()
1078 }
1079
1080 pub fn set_contract_query(&self, q: impl Into<String>) {
1082 *self.contract_query.lock().unwrap() = q.into();
1083 self.bump();
1084 }
1085
1086 pub fn toggle_contract_selection(&self, en_symbol: &str) {
1088 let mut v = self.contract_selected.lock().unwrap();
1089 match v.iter().position(|s| s == en_symbol) {
1090 Some(i) => {
1091 v.remove(i);
1092 }
1093 None => v.push(en_symbol.to_string()),
1094 }
1095 self.bump();
1096 }
1097
1098 pub fn selected_contracts(&self) -> Vec<String> {
1100 self.contract_selected.lock().unwrap().clone()
1101 }
1102
1103 pub fn log_line(&self, line: impl Into<String>) {
1105 self.logs.lock().unwrap().push(line);
1106 self.bump();
1107 }
1108
1109 pub fn clear_logs(&self) {
1111 self.logs.lock().unwrap().clear();
1112 self.bump();
1113 }
1114
1115 pub fn logs_snapshot(&self) -> (Vec<String>, u64) {
1117 let r = self.logs.lock().unwrap();
1118 (r.snapshot(), r.spilled())
1119 }
1120
1121 pub fn logs_len(&self) -> usize {
1123 self.logs.lock().unwrap().len()
1124 }
1125
1126 pub fn set_snapshot(&self, snap: BusSnap) {
1128 *self.snap.lock().unwrap() = Arc::new(snap);
1129 self.bump();
1130 }
1131
1132 pub fn snapshot(&self) -> Arc<BusSnap> {
1134 Arc::clone(&self.snap.lock().unwrap())
1135 }
1136
1137 pub fn rev(&self) -> u64 {
1139 self.rev.load(Ordering::Acquire)
1140 }
1141
1142 pub fn bump(&self) {
1144 self.rev.fetch_add(1, Ordering::AcqRel);
1145 }
1146
1147 pub fn request_stop(&self) {
1149 self.stop.store(true, Ordering::Release);
1150 }
1151
1152 pub fn is_stop(&self) -> bool {
1154 self.stop.load(Ordering::Acquire)
1155 }
1156
1157 pub fn push_cmd(&self, cmd: UiCmd) {
1161 self.cmds.lock().unwrap().push_back(cmd);
1162 self.bump();
1163 }
1164
1165 pub fn drain_cmds(&self) -> Vec<UiCmd> {
1167 self.cmds.lock().unwrap().drain(..).collect()
1168 }
1169
1170 pub fn pending_confirm(&self) -> Option<String> {
1172 self.pending_confirm.lock().unwrap().clone()
1173 }
1174
1175 pub fn confirm_step(&self, name: &str) -> ConfirmStep {
1177 let mut g = self.pending_confirm.lock().unwrap();
1178 match g.take() {
1179 Some(p) if p == name => ConfirmStep::Fired,
1180 Some(_) => {
1181 *g = Some(name.to_string());
1182 ConfirmStep::Switched
1183 }
1184 None => {
1185 *g = Some(name.to_string());
1186 ConfirmStep::Armed
1187 }
1188 }
1189 }
1190
1191 pub fn confirm_cancel(&self) {
1193 *self.pending_confirm.lock().unwrap() = None;
1194 self.bump();
1195 }
1196
1197 pub fn market(&self) -> &MarketStore {
1201 &self.market
1202 }
1203
1204 pub fn set_trade_snap(&self, ts: TradeSnap) {
1208 *self.trade_snap.lock().unwrap() = Arc::new(ts);
1209 self.bump();
1210 }
1211
1212 pub fn trade_snap(&self) -> Arc<TradeSnap> {
1214 self.trade_snap.lock().unwrap().clone()
1215 }
1216
1217 pub fn ctp_form(&self) -> CtpForm {
1221 self.ctp_form.lock().unwrap().clone()
1222 }
1223
1224 pub fn set_ctp_form(&self, f: CtpForm) {
1226 *self.ctp_form.lock().unwrap() = f;
1227 self.bump();
1228 }
1229
1230 pub fn row_selected(&self, table: &str, key: &str) -> bool {
1234 self.row_sel
1235 .lock()
1236 .unwrap()
1237 .get(table)
1238 .is_some_and(|k| k == key)
1239 }
1240
1241 pub fn select_row(&self, table: &str, key: &str) {
1243 self.row_sel
1244 .lock()
1245 .unwrap()
1246 .insert(table.to_string(), key.to_string());
1247 }
1248}