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 },
121 Connect { gateway: String, form: CtpForm },
126 CtpSubscribe {
129 gateway: String,
130 symbols: Vec<String>,
131 },
132}
133
134pub const KLINE_CELL_CAP: usize = 4096;
136
137pub const MAX_ACCOUNTS: usize = 8;
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143pub enum ConfirmStep {
144 Armed,
145 Fired,
146 Switched,
147}
148
149#[must_use]
152pub fn is_quarantined(state: &str) -> bool {
153 state.contains("Quarantin")
154}
155
156#[derive(Debug, Default)]
160pub struct MarketStore {
161 cells: Mutex<BTreeMap<(String, i64), Vec<BarData>>>,
162 frames: AtomicU64,
164 updates: AtomicU64,
166 reloads: AtomicU64,
168}
169
170#[derive(Debug, Clone, PartialEq)]
172pub struct CellView {
173 pub symbol: String,
174 pub interval: i64,
175 pub bars: Vec<BarData>,
176}
177
178impl MarketStore {
179 pub fn on_frame(&self, frame: &BarsFrame) {
181 let mut cells = self.cells.lock().unwrap();
182 for b in &frame.bars {
183 let key = (b.symbol.clone(), frame.interval);
184 let cell = cells.entry(key).or_default();
185 match cell.last() {
186 Some(last) if b.datetime < last.datetime => continue,
187 Some(last) if b.datetime == last.datetime => {
188 let slot = cell.last_mut().expect("len>1");
189 *slot = b.clone();
190 }
191 _ => {
192 cell.push(b.clone());
193 if cell.len() > KLINE_CELL_CAP {
194 cell.remove(0);
195 }
196 }
197 }
198 }
199 self.frames.fetch_add(1, Ordering::AcqRel);
200 }
201
202 pub fn on_update(&self, b: &BarData) {
204 let mut cells = self.cells.lock().unwrap();
205 let iv = b
206 .interval
207 .unwrap_or(ek_data::constant::Interval::MINUTE)
208 .seconds();
209 let key = (b.symbol.clone(), iv);
210 let cell = cells.entry(key).or_default();
211 match cell.last() {
212 Some(last) if b.datetime == last.datetime => {
213 let slot = cell.last_mut().expect("len>1");
214 *slot = b.clone();
215 }
216 Some(last) if b.datetime < last.datetime => {}
217 _ => {
218 cell.push(b.clone());
219 if cell.len() > KLINE_CELL_CAP {
220 cell.remove(0);
221 }
222 }
223 }
224 self.updates.fetch_add(1, Ordering::AcqRel);
225 }
226
227 pub fn note_reload(&self) {
229 self.reloads.fetch_add(1, Ordering::AcqRel);
230 }
231
232 pub fn cells(&self) -> Vec<CellView> {
234 self.cells
235 .lock()
236 .unwrap()
237 .iter()
238 .map(|((sym, iv), bars)| CellView {
239 symbol: sym.clone(),
240 interval: *iv,
241 bars: bars.clone(),
242 })
243 .collect()
244 }
245
246 pub fn counts(&self) -> (u64, u64, u64) {
248 (
249 self.frames.load(Ordering::Acquire),
250 self.updates.load(Ordering::Acquire),
251 self.reloads.load(Ordering::Acquire),
252 )
253 }
254}
255
256#[derive(Debug, Default, Clone, PartialEq)]
259pub struct ReloadForm {
260 pub interval: String,
261 pub symbol: String,
262 pub csv: String,
263}
264
265pub fn parse_gateway(raw: &str, fallback: Option<&str>) -> Result<String, String> {
272 let g = raw.trim();
273 if !g.is_empty() {
274 if g.chars().any(|c| c.is_whitespace()) {
275 return Err(format!("网关解析: 含空白:{raw:?}"));
276 }
277 return Ok(g.to_string());
278 }
279 match fallback.map(|f| f.trim()).filter(|f| !f.is_empty()) {
280 Some(f) => Ok(f.to_string()),
281 None => Err("网关解析: 未选择账户(先在账户面板添加账户)".to_string()),
282 }
283}
284
285#[derive(Debug, Clone, PartialEq)]
288pub struct CtpForm {
289 pub broker_id: String,
290 pub td_addr: String,
291 pub td_user: String,
292 pub td_password: String,
293 pub td_auth_code: String,
294 pub td_app_id: String,
295 pub md_addr: String,
296 pub md_user: String,
297 pub md_password: String,
298 pub symbols: String,
300 pub accept_futures: bool,
303 pub accept_options: bool,
304}
305
306impl Default for CtpForm {
309 fn default() -> Self {
310 Self {
311 broker_id: String::new(),
312 td_addr: String::new(),
313 td_user: String::new(),
314 td_password: String::new(),
315 td_auth_code: String::new(),
316 td_app_id: String::new(),
317 md_addr: String::new(),
318 md_user: String::new(),
319 md_password: String::new(),
320 symbols: String::new(),
321 accept_futures: true,
322 accept_options: false,
323 }
324 }
325}
326
327impl CtpForm {
328 #[must_use]
332 pub fn product_classes(&self) -> Vec<String> {
333 let mut codes = Vec::new();
334 if self.accept_futures {
335 codes.push("1".to_string());
336 }
337 if self.accept_options {
338 codes.push("2".to_string());
339 }
340 codes
341 }
342
343 pub fn to_configure_req(&self) -> ek_ctp::plugin::ConfigureReq {
345 ek_ctp::plugin::ConfigureReq {
346 broker_id: self.broker_id.trim().to_string(),
347 td_addr: self.td_addr.trim().to_string(),
348 td_user: self.td_user.trim().to_string(),
349 td_password: self.td_password.clone(),
350 td_auth_code: self.td_auth_code.trim().to_string(),
351 td_app_id: self.td_app_id.trim().to_string(),
352 md_addr: self.md_addr.trim().to_string(),
353 md_user: self.md_user.trim().to_string(),
354 md_password: self.md_password.clone(),
355 product_classes: self.product_classes(),
356 }
357 }
358
359 pub fn parse_symbols(&self) -> Result<Vec<String>, String> {
361 let list: Vec<String> = self
362 .symbols
363 .split([',', ',', ' '])
364 .map(|s| s.trim().to_string())
365 .filter(|s| !s.is_empty())
366 .collect();
367 if list.is_empty() {
368 return Err("订阅解析: 标的位空".to_string());
369 }
370 Ok(list)
371 }
372
373 pub fn validate(&self) -> Result<(), String> {
382 fn front(name: &str, v: &str) -> Result<(), String> {
383 let v = v.trim();
384 let Some(rest) = v.strip_prefix("tcp://") else {
385 return Err(format!("{name} 形制应为 tcp://host:port(当前 {:?})", v));
386 };
387 let Some((host, port)) = rest.rsplit_once(':') else {
388 return Err(format!("{name} 缺端口:{name} {:?}", v));
389 };
390 if host.trim().is_empty() {
391 return Err(format!("{name} 主机位空:{}", v));
392 }
393 let port: u32 = port
394 .parse()
395 .map_err(|_| format!("{name} 端口不是数字:{}", v))?;
396 if port == 0 || port > 65535 {
397 return Err(format!("{name} 端口越界(1..=65535):{}", v));
398 }
399 Ok(())
400 }
401 if self.broker_id.trim().is_empty() {
402 return Err("broker 位空".to_string());
403 }
404 if self.td_user.trim().is_empty() {
405 return Err("交易账号位空".to_string());
406 }
407 front("td 前置", &self.td_addr)?;
408 if !self.md_addr.trim().is_empty() {
409 front("md 前置", &self.md_addr)?;
410 }
411 if self.td_app_id.trim().is_empty() {
412 return Err("AppID 位空(CTP 登录必需)".to_string());
413 }
414 if self.td_auth_code.trim().is_empty() {
415 return Err("AuthCode 位空(CTP 登录必需)".to_string());
416 }
417 Ok(())
418 }
419}
420
421#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
428pub enum AccountStage {
429 #[default]
431 NotAssembled,
432 Assembling,
434 Assembled,
436 AssembleFailed,
438}
439
440impl AccountStage {
441 #[must_use]
443 pub fn label(self) -> &'static str {
444 match self {
445 AccountStage::NotAssembled => "未装配",
446 AccountStage::Assembling => "装配中…",
447 AccountStage::Assembled => "已装配·登录态未知",
450 AccountStage::AssembleFailed => "装配失败",
451 }
452 }
453}
454
455#[derive(Debug, Clone, PartialEq, Default)]
461pub struct AccountRow {
462 pub gateway: String,
464 pub alias: String,
466 pub broker_id: String,
467 pub account: String,
468 pub md_source: bool,
470 pub form: CtpForm,
472 pub stage: AccountStage,
474 pub fail_note: Option<String>,
476 pub effective_classes: Vec<String>,
481 pub connected: bool,
484}
485
486impl AccountRow {
487 pub fn derive(form: &CtpForm) -> Result<Self, String> {
492 let broker = form.broker_id.trim();
493 let account = form.td_user.trim();
494 let gateway = ek_ctp::derived_gateway_name(broker, account).ok_or_else(|| {
495 "账户: broker/账号 不能为空且只能含字母数字(且派生名 ≤32)".to_string()
496 })?;
497 Ok(Self {
498 gateway,
499 alias: String::new(),
500 broker_id: broker.to_string(),
501 account: account.to_string(),
502 md_source: false,
503 form: form.clone(),
504 stage: AccountStage::NotAssembled,
505 fail_note: None,
506 effective_classes: Vec::new(),
507 connected: false,
508 })
509 }
510
511 #[must_use]
513 pub fn display_name(&self) -> String {
514 let a = self.alias.trim();
515 if a.is_empty() {
516 self.account.clone()
517 } else {
518 a.to_string()
519 }
520 }
521}
522
523#[derive(Debug, Clone, PartialEq, Default)]
525pub struct TradeSnap {
526 pub orders: Vec<ek_data::order::OrderData>,
527 pub trades: Vec<ek_data::trade::TradeData>,
528 pub positions: Vec<ek_data::position::PositionData>,
529 pub accounts: Vec<ek_data::account::AccountData>,
531}
532
533pub fn parse_reload_csv(form: &ReloadForm) -> Result<(i64, String, Vec<BarData>), String> {
536 let interval: i64 = form
537 .interval
538 .trim()
539 .parse()
540 .map_err(|_| format!("回灌 解析: interval 非整数:{:?}", form.interval))?;
541 let symbol = form.symbol.trim().to_string();
542 if symbol.is_empty() {
543 return Err("回灌解析: symbol 空".to_string());
544 }
545 if form.csv.trim().is_empty() {
546 return Err("回灌解析: csv 空".to_string());
547 }
548 let mut bars = Vec::new();
549 for (i, raw) in form.csv.lines().enumerate() {
550 let line = raw.trim();
551 if line.is_empty() || line.starts_with('#') {
552 continue;
553 }
554 let parts: Vec<&str> = line.split(',').map(str::trim).collect();
555 if parts.len() != 6 {
556 return Err(format!("回灌解析: 第 {} 列数 ≠ 6", i + 1));
557 }
558 let num = |s: &str| -> Result<f64, String> {
559 s.parse::<f64>()
560 .map_err(|_| format!("回灌解析: 第 {} 列数值坏:{:?}", i + 1, s))
561 };
562 let dt_s: i64 = parts[0]
563 .parse()
564 .map_err(|_| format!("回灌解析: 第 {} 列 dt 坏:{:?}", i + 1, parts[0]))?;
565 bars.push(BarData {
566 symbol: symbol.clone(),
567 datetime: dt_s * 1_000_000_000,
568 interval: Some(
569 ek_dm::wire::interval_of_seconds(interval)
570 .unwrap_or(ek_data::constant::Interval::MINUTE),
571 ),
572 open_price: num(parts[1])?,
573 high_price: num(parts[2])?,
574 low_price: num(parts[3])?,
575 close_price: num(parts[4])?,
576 volume: num(parts[5])?,
577 ..BarData::default()
578 });
579 }
580 if bars.is_empty() {
581 return Err("回灌解析: 无有效行".to_string());
582 }
583 Ok((interval, symbol, bars))
584}
585
586#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
591pub enum TopLeftTab {
592 #[default]
593 Orders,
594 Active,
595}
596
597impl TopLeftTab {
598 pub fn label(self) -> &'static str {
599 match self {
600 Self::Orders => "委托",
601 Self::Active => "未成交",
602 }
603 }
604}
605
606#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
608pub enum BottomRightTab {
609 #[default]
610 Account,
611 Logs,
612}
613
614impl BottomRightTab {
615 pub fn label(self) -> &'static str {
616 match self {
617 Self::Account => "资金",
618 Self::Logs => "日志",
619 }
620 }
621}
622
623#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
627pub enum Theme {
628 #[default]
630 FinanceDark,
631 EguiDark,
633 EguiLight,
635 QtClassic,
637}
638
639impl Theme {
640 pub fn label(self) -> &'static str {
641 match self {
642 Self::FinanceDark => "金融深色",
643 Self::EguiDark => "egui深色",
644 Self::EguiLight => "egui浅色",
645 Self::QtClassic => "Qt灰白",
646 }
647 }
648
649 pub fn next(self) -> Self {
651 match self {
652 Self::FinanceDark => Self::EguiDark,
653 Self::EguiDark => Self::EguiLight,
654 Self::EguiLight => Self::QtClassic,
655 Self::QtClassic => Self::FinanceDark,
656 }
657 }
658}
659
660#[derive(Debug, Default)]
663pub struct TickStore {
664 ticks: BTreeMap<String, ek_data::tick::TickData>,
665}
666
667impl TickStore {
668 pub fn put(&mut self, t: &ek_data::tick::TickData) {
671 self.ticks
672 .insert(format!("{}.{}", t.exchange.as_str(), t.symbol), t.clone());
673 }
674
675 pub fn rows(&self) -> Vec<ek_data::tick::TickData> {
677 let mut v: Vec<ek_data::tick::TickData> = self.ticks.values().cloned().collect();
678 v.sort_by(|a, b| {
679 (a.exchange.as_str().to_string(), a.symbol.clone())
680 .cmp(&(b.exchange.as_str().to_string(), b.symbol.clone()))
681 });
682 v
683 }
684
685 pub fn groups(&self) -> Vec<(String, Vec<ek_data::tick::TickData>)> {
687 let mut out: Vec<(String, Vec<ek_data::tick::TickData>)> = Vec::new();
688 for t in self.rows() {
689 let ex = t.exchange.as_str().to_string();
690 match out.last_mut() {
691 Some((e, v)) if *e == ex => v.push(t),
692 _ => out.push((ex, vec![t])),
693 }
694 }
695 out
696 }
697
698 pub fn len(&self) -> usize {
699 self.ticks.len()
700 }
701
702 pub fn is_empty(&self) -> bool {
703 self.ticks.is_empty()
704 }
705}
706
707#[derive(Debug, Clone, PartialEq)]
709pub struct OrderForm {
710 pub exchange: String,
712 pub symbol: String,
714 pub direction: ek_data::constant::Direction,
715 pub offset: ek_data::constant::Offset,
716 pub order_type: ek_data::constant::OrderType,
717 pub price: String,
719 pub volume: String,
720 pub gateway: String,
722 pub follow_price: bool,
724 pub auto_split: bool,
727}
728
729impl Default for OrderForm {
730 fn default() -> Self {
731 Self {
732 exchange: "SHFE".to_string(),
733 symbol: String::new(),
734 direction: ek_data::constant::Direction::LONG,
735 offset: ek_data::constant::Offset::OPEN,
736 order_type: ek_data::constant::OrderType::LIMIT,
737 price: String::new(),
738 volume: "1".to_string(),
739 gateway: String::new(),
740 follow_price: true,
741 auto_split: true,
742 }
743 }
744}
745
746impl OrderForm {
747 pub fn to_request(&self) -> Result<ek_data::order_request::OrderRequest, String> {
749 use std::str::FromStr;
750 let symbol = self.symbol.trim();
751 if symbol.is_empty() {
752 return Err("下单: 代码为空".to_string());
753 }
754 if symbol.contains('.') {
755 return Err("下单: 代码不带交易所前缀(交易所走下拉)".to_string());
756 }
757 let exchange = ek_data::constant::Exchange::from_str(self.exchange.trim())
758 .map_err(|e| format!("下单: {e}"))?;
759 let price: f64 = self
760 .price
761 .trim()
762 .parse()
763 .map_err(|_| format!("下单: 价格无效 {:?}", self.price))?;
764 let volume: f64 = self
765 .volume
766 .trim()
767 .parse()
768 .map_err(|_| format!("下单: 数量无效 {:?}", self.volume))?;
769 if self.order_type != ek_data::constant::OrderType::MARKET && price <= 0.0 {
770 return Err("下单: 限价单价格须为正".to_string());
771 }
772 if volume <= 0.0 {
773 return Err("下单: 数量须为正".to_string());
774 }
775 Ok(ek_data::order_request::OrderRequest {
776 symbol: symbol.to_string(),
777 exchange,
778 direction: self.direction,
779 order_type: self.order_type,
780 volume,
781 price,
782 offset: self.offset,
783 ..ek_data::order_request::OrderRequest::default()
784 })
785 }
786}
787
788#[derive(Debug, Default)]
794pub struct ContractStore {
795 contracts: BTreeMap<String, ek_data::contract::ContractData>,
796}
797
798impl ContractStore {
799 pub fn put(&mut self, c: &ek_data::contract::ContractData) {
800 self.contracts
801 .insert(format!("{}.{}", c.exchange.as_str(), c.symbol), c.clone());
802 }
803
804 pub fn query(&self, pattern: &str) -> Vec<ek_data::contract::ContractData> {
806 let p = pattern.trim();
807 self.contracts
808 .values()
809 .filter(|c| {
810 p.is_empty()
811 || c.symbol.contains(p)
812 || c.exchange.as_str().eq_ignore_ascii_case(p)
813 || c.name.contains(p)
814 })
815 .cloned()
816 .collect()
817 }
818
819 pub fn len(&self) -> usize {
820 self.contracts.len()
821 }
822
823 pub fn is_empty(&self) -> bool {
824 self.contracts.is_empty()
825 }
826}
827
828#[derive(Debug, Default)]
830pub struct UiShared {
831 logs: Mutex<LogRing>,
832 snap: Mutex<Arc<BusSnap>>,
833 market: MarketStore,
834 ctp_form: Mutex<CtpForm>,
835 trade_snap: Mutex<Arc<TradeSnap>>,
836 tick_store: Mutex<TickStore>,
837 order_form: Mutex<OrderForm>,
838 selected_exchange: Mutex<String>,
839 topleft_tab: Mutex<TopLeftTab>,
840 bottomright_tab: Mutex<BottomRightTab>,
841 ops_window: AtomicBool,
842 accounts_window: AtomicBool,
845 contracts_window: AtomicBool,
846 theme: AtomicU8,
848 contract_store: Mutex<ContractStore>,
849 contract_query: Mutex<String>,
850 contract_selected: Mutex<Vec<String>>,
851 kline_windows: Mutex<Vec<String>>,
852 rev: AtomicU64,
853 stop: AtomicBool,
855 cmds: Mutex<VecDeque<UiCmd>>,
857 pending_confirm: Mutex<Option<String>>,
859 accounts: Mutex<Vec<AccountRow>>,
861 account_edit_window: AtomicBool,
863 editing_account: Mutex<Option<String>>,
865 edit_alias: Mutex<String>,
867 edit_error: Mutex<Option<String>>,
869 md_source: Mutex<String>,
871 subscriptions: Mutex<Vec<String>>,
873 row_sel: Mutex<BTreeMap<String, String>>,
876}
877
878impl UiShared {
879 pub fn new() -> Self {
881 Self::default()
882 }
883
884 pub fn update_tick(&self, t: &ek_data::tick::TickData) {
888 self.tick_store.lock().unwrap().put(t);
889 self.bump();
890 }
891
892 pub fn tick_rows(&self) -> Vec<ek_data::tick::TickData> {
894 self.tick_store.lock().unwrap().rows()
895 }
896
897 pub fn tick_groups(&self) -> Vec<(String, Vec<ek_data::tick::TickData>)> {
899 self.tick_store.lock().unwrap().groups()
900 }
901
902 pub fn tick_count(&self) -> usize {
904 self.tick_store.lock().unwrap().len()
905 }
906
907 pub fn order_form(&self) -> OrderForm {
909 self.order_form.lock().unwrap().clone()
910 }
911
912 pub fn set_order_form(&self, f: OrderForm) {
914 *self.order_form.lock().unwrap() = f;
915 self.bump();
916 }
917
918 pub fn selected_exchange(&self) -> String {
920 self.selected_exchange.lock().unwrap().clone()
921 }
922
923 pub fn set_selected_exchange(&self, s: impl Into<String>) {
925 *self.selected_exchange.lock().unwrap() = s.into();
926 self.bump();
927 }
928
929 pub fn topleft_tab(&self) -> TopLeftTab {
931 *self.topleft_tab.lock().unwrap()
932 }
933
934 pub fn set_topleft_tab(&self, t: TopLeftTab) {
936 *self.topleft_tab.lock().unwrap() = t;
937 self.bump();
938 }
939
940 pub fn bottomright_tab(&self) -> BottomRightTab {
942 *self.bottomright_tab.lock().unwrap()
943 }
944
945 pub fn set_bottomright_tab(&self, t: BottomRightTab) {
947 *self.bottomright_tab.lock().unwrap() = t;
948 self.bump();
949 }
950
951 pub fn theme(&self) -> Theme {
955 match self.theme.load(Ordering::Acquire) {
956 1 => Theme::EguiDark,
957 2 => Theme::EguiLight,
958 3 => Theme::QtClassic,
959 _ => Theme::FinanceDark,
960 }
961 }
962
963 pub fn set_theme(&self, t: Theme) {
965 self.theme.store(t as u8, Ordering::Release);
966 self.bump();
967 }
968
969 pub fn cycle_theme(&self) -> Theme {
971 let t = self.theme().next();
972 self.set_theme(t);
973 t
974 }
975
976 pub fn ops_window_open(&self) -> bool {
978 self.ops_window.load(Ordering::Acquire)
979 }
980
981 pub fn set_ops_window_open(&self, v: bool) {
983 self.ops_window.store(v, Ordering::Release);
984 self.bump();
985 }
986
987 pub fn accounts_window_open(&self) -> bool {
991 self.accounts_window.load(Ordering::Acquire)
992 }
993
994 pub fn set_accounts_window_open(&self, v: bool) {
996 self.accounts_window.store(v, Ordering::Release);
997 self.bump();
998 }
999
1000 pub fn account_edit_open(&self) -> bool {
1002 self.account_edit_window.load(Ordering::Acquire)
1003 }
1004
1005 pub fn editing_account(&self) -> Option<String> {
1007 self.editing_account.lock().unwrap().clone()
1008 }
1009
1010 pub fn begin_account_edit(&self, gateway: Option<&str>) -> bool {
1014 match gateway {
1015 Some(gw) => {
1016 let Some(row) = self.account(gw) else {
1017 return false;
1018 };
1019 *self.editing_account.lock().unwrap() = Some(gw.to_string());
1020 *self.edit_alias.lock().unwrap() = row.alias.clone();
1021 self.set_ctp_form(row.form.clone());
1022 }
1023 None => {
1024 *self.editing_account.lock().unwrap() = None;
1025 *self.edit_alias.lock().unwrap() = String::new();
1026 let draft = self.draft_from_existing();
1027 self.set_ctp_form(draft);
1028 }
1029 }
1030 *self.edit_error.lock().unwrap() = None;
1031 self.account_edit_window.store(true, Ordering::Release);
1032 self.bump();
1033 true
1034 }
1035
1036 pub fn end_account_edit(&self) {
1038 *self.editing_account.lock().unwrap() = None;
1039 *self.edit_alias.lock().unwrap() = String::new();
1040 *self.edit_error.lock().unwrap() = None;
1041 self.account_edit_window.store(false, Ordering::Release);
1042 let draft = self.draft_from_existing();
1043 self.set_ctp_form(draft);
1044 self.bump();
1045 }
1046
1047 pub fn end_account_edit_if_editing(&self, gateway: &str) {
1053 if self.editing_account().as_deref() == Some(gateway) {
1054 self.end_account_edit();
1055 }
1056 }
1057
1058 pub fn edit_alias(&self) -> String {
1060 self.edit_alias.lock().unwrap().clone()
1061 }
1062
1063 pub fn set_edit_alias(&self, v: impl Into<String>) {
1065 *self.edit_alias.lock().unwrap() = v.into();
1066 self.bump();
1067 }
1068
1069 pub fn edit_error(&self) -> Option<String> {
1071 self.edit_error.lock().unwrap().clone()
1072 }
1073
1074 pub fn set_edit_error(&self, e: Option<String>) {
1076 *self.edit_error.lock().unwrap() = e;
1077 self.bump();
1078 }
1079
1080 pub fn account_edit_dirty(&self) -> bool {
1082 let editing = self.editing_account();
1083 let draft = self.ctp_form();
1084 match editing {
1085 Some(gw) => self.account(&gw).map(|a| a.form != draft).unwrap_or(false),
1086 None => draft != CtpForm::default() && !draft.td_user.trim().is_empty(),
1087 }
1088 }
1089
1090 pub fn accounts(&self) -> Vec<AccountRow> {
1092 self.accounts.lock().unwrap().clone()
1093 }
1094
1095 pub fn account(&self, gateway: &str) -> Option<AccountRow> {
1097 self.accounts
1098 .lock()
1099 .unwrap()
1100 .iter()
1101 .find(|a| a.gateway == gateway)
1102 .cloned()
1103 }
1104
1105 pub fn add_account(&self, row: AccountRow) -> Result<(), String> {
1111 let mut acc = self.accounts.lock().unwrap();
1112 if acc.iter().any(|a| a.gateway == row.gateway) {
1113 return Err(format!("账户 {} 已存在(同 broker + 账号)", row.gateway));
1114 }
1115 let alias = row.alias.trim();
1116 if !alias.is_empty()
1117 && acc
1118 .iter()
1119 .any(|a| a.alias.trim().eq_ignore_ascii_case(alias))
1120 {
1121 return Err(format!("别名 {alias:?} 已被其他账户占用(别名须唯一)"));
1122 }
1123 if acc.len() >= MAX_ACCOUNTS {
1124 return Err(format!(
1125 "账户已达软上限 {MAX_ACCOUNTS} 个(先移除一个再添加)"
1126 ));
1127 }
1128 acc.push(row);
1129 drop(acc);
1130 self.bump();
1131 Ok(())
1132 }
1133
1134 #[must_use]
1136 pub fn can_add_account(&self) -> bool {
1137 self.accounts.lock().unwrap().len() < MAX_ACCOUNTS
1138 }
1139
1140 pub fn set_account_alias(&self, gateway: &str, alias: &str) -> bool {
1146 let alias = alias.trim().to_string();
1147 let mut acc = self.accounts.lock().unwrap();
1148 if !alias.is_empty()
1149 && acc
1150 .iter()
1151 .any(|a| a.gateway != gateway && a.alias.eq_ignore_ascii_case(&alias))
1152 {
1153 return false;
1154 }
1155 match acc.iter_mut().find(|a| a.gateway == gateway) {
1156 Some(a) => {
1157 a.alias = alias;
1158 drop(acc);
1159 self.bump();
1160 true
1161 }
1162 None => false,
1163 }
1164 }
1165
1166 pub fn note_connect_sent(&self, gateway: &str) {
1168 let mut acc = self.accounts.lock().unwrap();
1169 if let Some(a) = acc.iter_mut().find(|a| a.gateway == gateway) {
1170 a.stage = AccountStage::Assembling;
1171 a.fail_note = None;
1172 drop(acc);
1173 self.bump();
1174 }
1175 }
1176
1177 pub fn note_connect_result(&self, gateway: &str, result: Result<(), String>) {
1181 let mut acc = self.accounts.lock().unwrap();
1182 if let Some(a) = acc.iter_mut().find(|a| a.gateway == gateway) {
1183 match result {
1184 Ok(()) => {
1185 a.stage = AccountStage::Assembled;
1186 a.fail_note = None;
1187 }
1188 Err(e) => {
1189 a.stage = AccountStage::AssembleFailed;
1190 a.fail_note = Some(e);
1191 }
1192 }
1193 drop(acc);
1194 self.bump();
1195 }
1196 }
1197
1198 pub fn note_effective_product_classes(&self, gateway: &str, classes: &[String]) {
1202 let mut acc = self.accounts.lock().unwrap();
1203 if let Some(a) = acc.iter_mut().find(|a| a.gateway == gateway) {
1204 a.effective_classes = classes.to_vec();
1205 drop(acc);
1206 self.bump();
1207 }
1208 }
1209
1210 pub fn prepare_connect_form(&self, gateway: &str, form: CtpForm) -> CtpForm {
1213 let source = self.md_source();
1214 let mut f = form;
1215 if source.as_str() != gateway {
1216 f.md_addr.clear();
1217 f.md_user.clear();
1218 f.md_password.clear();
1219 }
1220 self.update_account_form(gateway, f.clone());
1221 f
1222 }
1223
1224 #[must_use]
1228 pub fn draft_from_existing(&self) -> CtpForm {
1229 let acc = self.accounts.lock().unwrap();
1230 let mut draft = CtpForm::default();
1231 if let Some(a) = acc.last() {
1232 draft.broker_id = a.form.broker_id.clone();
1233 draft.td_auth_code = a.form.td_auth_code.clone();
1234 draft.td_app_id = a.form.td_app_id.clone();
1235 draft.accept_futures = a.form.accept_futures;
1236 draft.accept_options = a.form.accept_options;
1237 }
1238 draft
1239 }
1240
1241 pub fn update_account_form(&self, gateway: &str, form: CtpForm) -> bool {
1243 let mut acc = self.accounts.lock().unwrap();
1244 match acc.iter_mut().find(|a| a.gateway == gateway) {
1245 Some(a) => {
1246 a.form = form;
1247 drop(acc);
1248 self.bump();
1249 true
1250 }
1251 None => false,
1252 }
1253 }
1254
1255 pub fn remove_account(&self, gateway: &str) -> bool {
1257 let mut acc = self.accounts.lock().unwrap();
1258 let before = acc.len();
1259 acc.retain(|a| a.gateway != gateway);
1260 let removed = acc.len() != before;
1261 if removed {
1262 let mut md = self.md_source.lock().unwrap();
1264 if md.as_str() == gateway {
1265 md.clear();
1266 }
1267 drop(acc);
1268 self.bump();
1269 }
1270 removed
1271 }
1272
1273 pub fn sync_accounts_connected(&self, rows: &[PlugRow]) -> Vec<String> {
1279 let mut newly = Vec::new();
1280 {
1281 let mut acc = self.accounts.lock().unwrap();
1282 for a in acc.iter_mut() {
1283 let up = rows.iter().any(|r| r.name == a.gateway);
1284 if up && !a.connected {
1285 newly.push(a.gateway.clone());
1286 }
1287 a.connected = up;
1288 if up {
1289 a.stage = AccountStage::Assembled;
1290 a.fail_note = None;
1291 } else if a.stage == AccountStage::Assembled {
1292 a.stage = AccountStage::NotAssembled;
1293 }
1294 }
1295 }
1296 if !newly.is_empty() {
1297 self.bump();
1298 }
1299 newly
1300 }
1301
1302 pub fn md_source(&self) -> String {
1304 self.md_source.lock().unwrap().clone()
1305 }
1306
1307 pub fn accounts_locked(&self) -> bool {
1309 self.accounts.lock().unwrap().iter().any(|a| a.connected)
1310 }
1311
1312 pub fn set_md_source(&self, gateway: &str) -> Result<(), String> {
1314 {
1315 let acc = self.accounts.lock().unwrap();
1316 if acc.iter().any(|a| a.connected) {
1317 return Err("元账号已锁定:有账户在线,切换行情源需先断开该账户".to_string());
1318 }
1319 if !acc.iter().any(|a| a.gateway == gateway) {
1320 return Err(format!("账户 {gateway} 不在清册"));
1321 }
1322 }
1323 let mut acc = self.accounts.lock().unwrap();
1324 for a in acc.iter_mut() {
1325 a.md_source = a.gateway == gateway;
1326 }
1327 drop(acc);
1328 let mut md = self.md_source.lock().unwrap();
1329 *md = gateway.to_string();
1330 self.bump();
1331 Ok(())
1332 }
1333
1334 pub fn subscriptions(&self) -> Vec<String> {
1336 self.subscriptions.lock().unwrap().clone()
1337 }
1338
1339 pub fn note_subscriptions(&self, symbols: &[String]) {
1341 {
1342 let mut subs = self.subscriptions.lock().unwrap();
1343 for s in symbols {
1344 let s = s.trim();
1345 if !s.is_empty() && !subs.iter().any(|x| x == s) {
1346 subs.push(s.to_string());
1347 }
1348 }
1349 }
1350 self.bump();
1351 }
1352
1353 pub fn default_gateway(&self) -> Option<String> {
1360 let chosen = {
1361 let f = self.order_form.lock().unwrap();
1362 f.gateway.trim().to_string()
1363 };
1364 if !chosen.is_empty() && self.account(&chosen).is_some_and(|a| a.connected) {
1365 return Some(chosen);
1366 }
1367 let acc = self.accounts.lock().unwrap();
1368 acc.iter().find(|a| a.connected).map(|a| a.gateway.clone())
1369 }
1370
1371 pub fn kline_symbols(&self) -> Vec<String> {
1373 self.kline_windows.lock().unwrap().clone()
1374 }
1375
1376 pub fn open_kline(&self, symbol: impl Into<String>) {
1378 let s = symbol.into();
1379 let mut v = self.kline_windows.lock().unwrap();
1380 if !v.contains(&s) {
1381 v.push(s.clone());
1382 }
1383 drop(v);
1384 self.log_line(format!("[kline] 打开 K线窗 {s}"));
1385 }
1386
1387 pub fn close_kline(&self, symbol: &str) {
1389 self.kline_windows.lock().unwrap().retain(|s| s != symbol);
1390 self.bump();
1391 }
1392
1393 pub fn contracts_window_open(&self) -> bool {
1397 self.contracts_window.load(Ordering::Acquire)
1398 }
1399
1400 pub fn set_contracts_window_open(&self, v: bool) {
1402 self.contracts_window.store(v, Ordering::Release);
1403 self.bump();
1404 }
1405
1406 pub fn update_contract(&self, c: &ek_data::contract::ContractData) {
1408 self.contract_store.lock().unwrap().put(c);
1409 self.bump();
1410 }
1411
1412 pub fn contracts(&self, pattern: &str) -> Vec<ek_data::contract::ContractData> {
1414 self.contract_store.lock().unwrap().query(pattern)
1415 }
1416
1417 pub fn contract_count(&self) -> usize {
1419 self.contract_store.lock().unwrap().len()
1420 }
1421
1422 pub fn contract_query(&self) -> String {
1424 self.contract_query.lock().unwrap().clone()
1425 }
1426
1427 pub fn set_contract_query(&self, q: impl Into<String>) {
1429 *self.contract_query.lock().unwrap() = q.into();
1430 self.bump();
1431 }
1432
1433 pub fn toggle_contract_selection(&self, en_symbol: &str) {
1435 let mut v = self.contract_selected.lock().unwrap();
1436 match v.iter().position(|s| s == en_symbol) {
1437 Some(i) => {
1438 v.remove(i);
1439 }
1440 None => v.push(en_symbol.to_string()),
1441 }
1442 self.bump();
1443 }
1444
1445 pub fn selected_contracts(&self) -> Vec<String> {
1447 self.contract_selected.lock().unwrap().clone()
1448 }
1449
1450 pub fn log_line(&self, line: impl Into<String>) {
1452 self.logs.lock().unwrap().push(line);
1453 self.bump();
1454 }
1455
1456 pub fn clear_logs(&self) {
1458 self.logs.lock().unwrap().clear();
1459 self.bump();
1460 }
1461
1462 pub fn logs_snapshot(&self) -> (Vec<String>, u64) {
1464 let r = self.logs.lock().unwrap();
1465 (r.snapshot(), r.spilled())
1466 }
1467
1468 pub fn logs_len(&self) -> usize {
1470 self.logs.lock().unwrap().len()
1471 }
1472
1473 pub fn set_snapshot(&self, snap: BusSnap) {
1475 *self.snap.lock().unwrap() = Arc::new(snap);
1476 self.bump();
1477 }
1478
1479 pub fn snapshot(&self) -> Arc<BusSnap> {
1481 Arc::clone(&self.snap.lock().unwrap())
1482 }
1483
1484 pub fn rev(&self) -> u64 {
1486 self.rev.load(Ordering::Acquire)
1487 }
1488
1489 pub fn bump(&self) {
1491 self.rev.fetch_add(1, Ordering::AcqRel);
1492 }
1493
1494 pub fn request_stop(&self) {
1496 self.stop.store(true, Ordering::Release);
1497 }
1498
1499 pub fn is_stop(&self) -> bool {
1501 self.stop.load(Ordering::Acquire)
1502 }
1503
1504 pub fn push_cmd(&self, cmd: UiCmd) {
1508 self.cmds.lock().unwrap().push_back(cmd);
1509 self.bump();
1510 }
1511
1512 pub fn drain_cmds(&self) -> Vec<UiCmd> {
1514 self.cmds.lock().unwrap().drain(..).collect()
1515 }
1516
1517 pub fn pending_confirm(&self) -> Option<String> {
1519 self.pending_confirm.lock().unwrap().clone()
1520 }
1521
1522 pub fn confirm_step(&self, name: &str) -> ConfirmStep {
1524 let mut g = self.pending_confirm.lock().unwrap();
1525 match g.take() {
1526 Some(p) if p == name => ConfirmStep::Fired,
1527 Some(_) => {
1528 *g = Some(name.to_string());
1529 ConfirmStep::Switched
1530 }
1531 None => {
1532 *g = Some(name.to_string());
1533 ConfirmStep::Armed
1534 }
1535 }
1536 }
1537
1538 pub fn confirm_cancel(&self) {
1540 *self.pending_confirm.lock().unwrap() = None;
1541 self.bump();
1542 }
1543
1544 pub fn market(&self) -> &MarketStore {
1548 &self.market
1549 }
1550
1551 pub fn set_trade_snap(&self, ts: TradeSnap) {
1555 *self.trade_snap.lock().unwrap() = Arc::new(ts);
1556 self.bump();
1557 }
1558
1559 pub fn trade_snap(&self) -> Arc<TradeSnap> {
1561 self.trade_snap.lock().unwrap().clone()
1562 }
1563
1564 pub fn ctp_form(&self) -> CtpForm {
1568 self.ctp_form.lock().unwrap().clone()
1569 }
1570
1571 pub fn set_ctp_form(&self, f: CtpForm) {
1573 *self.ctp_form.lock().unwrap() = f;
1574 self.bump();
1575 }
1576
1577 pub fn row_selected(&self, table: &str, key: &str) -> bool {
1581 self.row_sel
1582 .lock()
1583 .unwrap()
1584 .get(table)
1585 .is_some_and(|k| k == key)
1586 }
1587
1588 pub fn select_row(&self, table: &str, key: &str) {
1590 self.row_sel
1591 .lock()
1592 .unwrap()
1593 .insert(table.to_string(), key.to_string());
1594 }
1595}