Skip to main content

ek_ui/
state.rs

1//! 共享观测状态:`UiPlugin`(总线面)写、`render_body`(窗口面)读。
2//!
3//! 线程形貌:总线侧在**插件 worker 线程**写;窗口侧在 **eframe 主线程**读;
4//! 锁粒度 = 每次 `lock().clone()/snapshot()`,不跨帧持锁。
5
6use 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
14/// 日志环形容量(S1b 门面;溢出不丢累计计数,见 [`LogRing::spilled`])。
15pub const RING_CAP: usize = 512;
16
17/// 环形日志:push 满则挤掉最旧一条,溢出计数单调(页面显示"已丢弃 N 条")。
18#[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    /// 定容环形(测试可传小容量逼溢出)。
33    pub fn new(cap: usize) -> Self {
34        Self {
35            lines: VecDeque::with_capacity(cap),
36            cap,
37            spilled: 0,
38        }
39    }
40
41    /// 追加一行;满则挤旧 + spilled+1。
42    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    /// 当前长度。
55    pub fn len(&self) -> usize {
56        self.lines.len()
57    }
58
59    /// 是否空行。
60    pub fn is_empty(&self) -> bool {
61        self.lines.is_empty()
62    }
63
64    /// 累计已丢弃条数(只有溢出才涨)。
65    pub fn spilled(&self) -> u64 {
66        self.spilled
67    }
68
69    /// 只读快照(渲染用;不消费)。
70    pub fn snapshot(&self) -> Vec<String> {
71        self.lines.iter().cloned().collect()
72    }
73
74    /// 全清;spilled 保留(历史溢出事实不抹)。
75    pub fn clear(&mut self) {
76        self.lines.clear();
77    }
78}
79
80/// 一次 eTimer 拉取的服务/插件快照(渲染直接可用)。
81#[derive(Debug, Clone, Default)]
82pub struct BusSnap {
83    /// svctl.list 全行(svctl 独裁班;状态原文)。
84    pub rows: Vec<PlugRow>,
85    /// 逐插件服务目录(id 清单;无服务为空)。
86    pub services: Vec<(String, Vec<String>)>,
87}
88
89/// UI 操作指令(窗口面 → 总线面;eTimer 节拍被 ui 插件请辞)。
90#[derive(Debug, Clone, PartialEq)]
91pub enum UiCmd {
92    /// svctl.start(工厂名;v1 与插件注册名同,BD-U5)。
93    Start { factory: String },
94    /// svctl.remove(插件名)。
95    Remove { name: String },
96    /// dm.reload 回灌(S4;push=true 逐条广播,行情面板实时见 Rows)。
97    Reload {
98        interval: i64,
99        symbol: String,
100        bars: Vec<BarData>,
101    },
102    /// 下单(S5;网关受理即 SUBMITTING 单上总线)。
103    /// S6 起网关可选(`gateway.send_order`,空 = ctp)。
104    /// 0.1.1(BD-UI-30):改走 `oms.submit_order` 五段管线(拆单/风控/逐腿
105    /// 发送/记账,parent 聚合);`mode` = 拆单四态(勾选「自动开平」= Auto,
106    /// 取消勾选 = None 原样旁路)。
107    SendOrder {
108        req: ek_data::order_request::OrderRequest,
109        gateway: String,
110        mode: ek_oms::LockMode,
111    },
112    /// 撤单(S5;携三段号所需的 orderid;S6 起网关同选)。
113    /// 0.1.1(BD-UI-30):改走 `oms.cancel_order`(守卫 4000 不可绕过;
114    /// symbol/exchange 由 oms 订单缓存解,不再随令携带)。
115    CancelOrder { orderid: String, gateway: String },
116    /// ctp.configure 网关配置(S6 测试面;界面手工输入表单直映,插件侧校验;
117    /// 出队时 `to_configure_req` 抬成线上契约)。
118    CtpConfigure(CtpForm),
119    /// ctp.subscribe 订阅行情(S6 测试面;逗号分隔标的)。
120    CtpSubscribe { symbols: Vec<String> },
121}
122
123/// 行情缓存单格容量(防涨;丢头保尾,bar 流单调)。
124pub const KLINE_CELL_CAP: usize = 4096;
125
126/// 二次确认步进:Armed = 首点待确认;Fired = 同名二点已发令;Switched = 目标迁移。
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128pub enum ConfirmStep {
129    Armed,
130    Fired,
131    Switched,
132}
133
134/// 隔离态判定(svctl.list 状态原文含 Quarantin → 红;ePlugQDown.<名> 是定向
135/// 广播,未知名无法预订阅 —— 快照行扫描是唯一闭环面,BD-UI-17)。
136#[must_use]
137pub fn is_quarantined(state: &str) -> bool {
138    state.contains("Quarantin")
139}
140
141// ================= S4:行情库(eBar./eBarUpdate. 上屏地面) =================
142
143/// 单格 = (symbol, interval) → bar 升序序列(丢头保尾;datetime 单调)。
144#[derive(Debug, Default)]
145pub struct MarketStore {
146    cells: Mutex<BTreeMap<(String, i64), Vec<BarData>>>,
147    /// eBar. 帧接收计数(帧统计,刷新不丢帧观感面)。
148    frames: AtomicU64,
149    /// eBarUpdate. 接收计数。
150    updates: AtomicU64,
151    /// dm.reload 成功计数(回灌确认面)。
152    reloads: AtomicU64,
153}
154
155/// 一格的可读视图(渲染/测试用)。
156#[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    /// eBar. 收帧:逐 bar 并格(同 dt 替尾;更旧丢弃;更晚追加,溢出丢头)。
165    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    /// eBarUpdate. 收单 bar 快照(实时门;同 dt 替尾,新 dt 追加 = in-flight bar)。
188    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    /// dm.reload 回灌登记(RELOAD OK 计数;dm 侧行为独立)。
213    pub fn note_reload(&self) {
214        self.reloads.fetch_add(1, Ordering::AcqRel);
215    }
216
217    /// 全格只读快照(键序 = BTreeMap 序,symbol/interval 稳定)。
218    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    /// 计算(Ai) 帧统计。
232    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/// 回灌表单(v4:界面面已退役,总线口 `UiCmd::Reload` 保留;解析器留作
242/// 测试与脚本面)。
243#[derive(Debug, Default, Clone, PartialEq)]
244pub struct ReloadForm {
245    pub interval: String,
246    pub symbol: String,
247    pub csv: String,
248}
249
250/// 票据网关选择子解析(空 → "ctp";含空白 = 错。0.2.10 起 sim 退场,
251/// 兜底改真网关)。
252pub 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/// CTP 网关配置表单(S6 测试面;界面手工输入源 → UiCmd::CtpConfigure →
264/// ask `ctp.configure`。零配置口径:界面不读文件,值全部人敲;密码位掩码)。
265#[derive(Debug, 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    /// 订阅标的(逗号分隔;`订阅行情` 钮解析)。
277    pub symbols: String,
278    /// 收录勾选(ek-ctp 0.6.0 D-PC-FILTER):网关收录哪些产品类。
279    /// 缺省只勾期货 —— 期权默认不勾,因为默认口径与改动前一致(只期货上抛)。
280    pub accept_futures: bool,
281    pub accept_options: bool,
282}
283
284/// 手写 `Default`(不能 derive:`accept_futures` 必须为 `true`,derive 会给 `false`,
285/// 而两个都 false 在语义上是「什么都不收录」)。
286impl Default for CtpForm {
287    fn default() -> Self {
288        Self {
289            broker_id: String::new(),
290            td_addr: String::new(),
291            td_user: String::new(),
292            td_password: String::new(),
293            td_auth_code: String::new(),
294            td_app_id: String::new(),
295            md_addr: String::new(),
296            md_user: String::new(),
297            md_password: String::new(),
298            symbols: String::new(),
299            accept_futures: true,
300            accept_options: false,
301        }
302    }
303}
304
305impl CtpForm {
306    /// 勾选 → CTP 产品类码表(期货 `'1'`、期权 `'2'`)。
307    /// 两个都不勾交空表给插件:插件侧归一化一律回落默认期货 —— **空不等于全丢**
308    /// (共享合约表为空会让网关的持仓查询整条被跳过)。
309    #[must_use]
310    pub fn product_classes(&self) -> Vec<String> {
311        let mut codes = Vec::new();
312        if self.accept_futures {
313            codes.push("1".to_string());
314        }
315        if self.accept_options {
316            codes.push("2".to_string());
317        }
318        codes
319    }
320
321    /// 表单 → 线上契约(字段直映;md 回落由插件侧裁决,表单不预处理)。
322    pub fn to_configure_req(&self) -> ek_ctp::plugin::ConfigureReq {
323        ek_ctp::plugin::ConfigureReq {
324            broker_id: self.broker_id.trim().to_string(),
325            td_addr: self.td_addr.trim().to_string(),
326            td_user: self.td_user.trim().to_string(),
327            td_password: self.td_password.clone(),
328            td_auth_code: self.td_auth_code.trim().to_string(),
329            td_app_id: self.td_app_id.trim().to_string(),
330            md_addr: self.md_addr.trim().to_string(),
331            md_user: self.md_user.trim().to_string(),
332            md_password: self.md_password.clone(),
333            product_classes: self.product_classes(),
334        }
335    }
336
337    /// 订阅位解析(逗号/空白分隔;空表 = Err)。
338    pub fn parse_symbols(&self) -> Result<Vec<String>, String> {
339        let list: Vec<String> = self
340            .symbols
341            .split([',', ',', ' '])
342            .map(|s| s.trim().to_string())
343            .filter(|s| !s.is_empty())
344            .collect();
345        if list.is_empty() {
346            return Err("订阅解析: 标的位空".to_string());
347        }
348        Ok(list)
349    }
350}
351
352/// 交易监控快照(S5;ui 插件 eTimer 拍 ask oms 全量查询组帧)。
353#[derive(Debug, Clone, PartialEq, Default)]
354pub struct TradeSnap {
355    pub orders: Vec<ek_data::order::OrderData>,
356    pub trades: Vec<ek_data::trade::TradeData>,
357    pub positions: Vec<ek_data::position::PositionData>,
358    /// 资金帧(oms.get_all_accounts;CTP configure 登录后有数)。
359    pub accounts: Vec<ek_data::account::AccountData>,
360}
361
362/// CSV(每行 `dt,open,high,low,close,volume`;dt 为 epoch 秒)→ BarData 批。
363/// 错行报行号;空 csv = Err;升序不强制(dm.reload 自会逐行入窗)。
364pub fn parse_reload_csv(form: &ReloadForm) -> Result<(i64, String, Vec<BarData>), String> {
365    let interval: i64 = form
366        .interval
367        .trim()
368        .parse()
369        .map_err(|_| format!("回灌 解析: interval 非整数:{:?}", form.interval))?;
370    let symbol = form.symbol.trim().to_string();
371    if symbol.is_empty() {
372        return Err("回灌解析: symbol 空".to_string());
373    }
374    if form.csv.trim().is_empty() {
375        return Err("回灌解析: csv 空".to_string());
376    }
377    let mut bars = Vec::new();
378    for (i, raw) in form.csv.lines().enumerate() {
379        let line = raw.trim();
380        if line.is_empty() || line.starts_with('#') {
381            continue;
382        }
383        let parts: Vec<&str> = line.split(',').map(str::trim).collect();
384        if parts.len() != 6 {
385            return Err(format!("回灌解析: 第 {} 列数 ≠ 6", i + 1));
386        }
387        let num = |s: &str| -> Result<f64, String> {
388            s.parse::<f64>()
389                .map_err(|_| format!("回灌解析: 第 {} 列数值坏:{:?}", i + 1, s))
390        };
391        let dt_s: i64 = parts[0]
392            .parse()
393            .map_err(|_| format!("回灌解析: 第 {} 列 dt 坏:{:?}", i + 1, parts[0]))?;
394        bars.push(BarData {
395            symbol: symbol.clone(),
396            datetime: dt_s * 1_000_000_000,
397            interval: Some(
398                ek_dm::wire::interval_of_seconds(interval)
399                    .unwrap_or(ek_data::constant::Interval::MINUTE),
400            ),
401            open_price: num(parts[1])?,
402            high_price: num(parts[2])?,
403            low_price: num(parts[3])?,
404            close_price: num(parts[4])?,
405            volume: num(parts[5])?,
406            ..BarData::default()
407        });
408    }
409    if bars.is_empty() {
410        return Err("回灌解析: 无有效行".to_string());
411    }
412    Ok((interval, symbol, bars))
413}
414
415// ================= v4 布局面(带/组 tab / tick 库 / 下单表单;docs/ek-ui-layout-v2-plan.md) =================
416
417/// 左上组 tab(Python [委托|未成交];审计修正:行情是带不是 tab,K线/运维/CTP
418/// 是弹窗不是 tab,故不设全局 10 值 UiTab——那是 v2 残影)。
419#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
420pub enum TopLeftTab {
421    #[default]
422    Orders,
423    Active,
424}
425
426impl TopLeftTab {
427    pub fn label(self) -> &'static str {
428        match self {
429            Self::Orders => "委托",
430            Self::Active => "未成交",
431        }
432    }
433}
434
435/// 右下组 tab(Python [资金|日志])。
436#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
437pub enum BottomRightTab {
438    #[default]
439    Account,
440    Logs,
441}
442
443impl BottomRightTab {
444    pub fn label(self) -> &'static str {
445        match self {
446            Self::Account => "资金",
447            Self::Logs => "日志",
448        }
449    }
450}
451
452/// 界面主题(BD-UI-34,用户问「有没有 PySide6 那种可换的金融向主题」):
453/// 菜单钮一键循环切换(运行期态,无配置文件——ek-ui 零配置口径)。
454/// 语义色(多红/空绿/隔离红/选中蓝)跨主题恒定——金融语义不随主题漂移。
455#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
456pub enum Theme {
457    /// 金融深色(默认):egui dark 骨架 + 深海军蓝面板/窗底(交易终端观感)。
458    #[default]
459    FinanceDark,
460    /// egui 原生深色。
461    EguiDark,
462    /// egui 原生浅色。
463    EguiLight,
464    /// Qt Fusion 灰白近似(Python/PySide6 版观感迁移用)。
465    QtClassic,
466}
467
468impl Theme {
469    pub fn label(self) -> &'static str {
470        match self {
471            Self::FinanceDark => "金融深色",
472            Self::EguiDark => "egui深色",
473            Self::EguiLight => "egui浅色",
474            Self::QtClassic => "Qt灰白",
475        }
476    }
477
478    /// 循环下一主题(菜单钮一键循环口径,单层无下拉)。
479    pub fn next(self) -> Self {
480        match self {
481            Self::FinanceDark => Self::EguiDark,
482            Self::EguiDark => Self::EguiLight,
483            Self::EguiLight => Self::QtClassic,
484            Self::QtClassic => Self::FinanceDark,
485        }
486    }
487}
488
489/// tick 库(v2 行情 tab 唯一数据源):symbol → 最新 tick,逐 symbol 覆盖,
490/// 有界 = 订阅面(ek-ctp 只发已订阅合约;无 bar 回落,用户裁定)。
491#[derive(Debug, Default)]
492pub struct TickStore {
493    ticks: BTreeMap<String, ek_data::tick::TickData>,
494}
495
496impl TickStore {
497    /// 逐合约覆盖(审计修正:键 = en_symbol「交易所.合约」,防跨所同码相撞;
498    /// 交易所分组取 tick 自带 exchange)。
499    pub fn put(&mut self, t: &ek_data::tick::TickData) {
500        self.ticks
501            .insert(format!("{}.{}", t.exchange.as_str(), t.symbol), t.clone());
502    }
503
504    /// 只读视图(交易所 → 合约排序,tab 分组顺序稳定)。
505    pub fn rows(&self) -> Vec<ek_data::tick::TickData> {
506        let mut v: Vec<ek_data::tick::TickData> = self.ticks.values().cloned().collect();
507        v.sort_by(|a, b| {
508            (a.exchange.as_str().to_string(), a.symbol.clone())
509                .cmp(&(b.exchange.as_str().to_string(), b.symbol.clone()))
510        });
511        v
512    }
513
514    /// 按交易所分组(tab 键只列有 tick 的所,不预置全枚举)。
515    pub fn groups(&self) -> Vec<(String, Vec<ek_data::tick::TickData>)> {
516        let mut out: Vec<(String, Vec<ek_data::tick::TickData>)> = Vec::new();
517        for t in self.rows() {
518            let ex = t.exchange.as_str().to_string();
519            match out.last_mut() {
520                Some((e, v)) if *e == ex => v.push(t),
521                _ => out.push((ex, vec![t])),
522            }
523        }
524        out
525    }
526
527    pub fn len(&self) -> usize {
528        self.ticks.len()
529    }
530
531    pub fn is_empty(&self) -> bool {
532        self.ticks.is_empty()
533    }
534}
535
536/// 下单面板表单(v2 常驻右侧;Python TradingWidget 字段面等价;操作面非配置面)。
537#[derive(Debug, Clone, PartialEq)]
538pub struct OrderForm {
539    /// 交易所代码(下拉;from_str 抬成枚举)
540    pub exchange: String,
541    /// 合约代码(纯 symbol,不带前缀)
542    pub symbol: String,
543    pub direction: ek_data::constant::Direction,
544    pub offset: ek_data::constant::Offset,
545    pub order_type: ek_data::constant::OrderType,
546    /// 输入面保持字符串(同 Python QLineEdit),委托时解析
547    pub price: String,
548    pub volume: String,
549    /// 网关(下拉源 = trade_snap gateway 去重集 ∪ CTP 表单现值)
550    pub gateway: String,
551    /// 价格随行情勾选(勾选时价格框随选中合约最新 tick)
552    pub follow_price: bool,
553    /// 「自动开平」勾选(0.1.1 BD-UI-30;默认开 = LockMode::Auto 走拆单器,
554    /// 关 = LockMode::None 原样旁路——守卫不可绕,拆单可逐请求旁路)
555    pub auto_split: bool,
556}
557
558impl Default for OrderForm {
559    fn default() -> Self {
560        Self {
561            exchange: "SHFE".to_string(),
562            symbol: String::new(),
563            direction: ek_data::constant::Direction::LONG,
564            offset: ek_data::constant::Offset::OPEN,
565            order_type: ek_data::constant::OrderType::LIMIT,
566            price: String::new(),
567            volume: "1".to_string(),
568            gateway: String::new(),
569            follow_price: true,
570            auto_split: true,
571        }
572    }
573}
574
575impl OrderForm {
576    /// 表单 → OrderRequest(与票据解析同一出口;错误红字就地,不进总线)。
577    pub fn to_request(&self) -> Result<ek_data::order_request::OrderRequest, String> {
578        use std::str::FromStr;
579        let symbol = self.symbol.trim();
580        if symbol.is_empty() {
581            return Err("下单: 代码为空".to_string());
582        }
583        if symbol.contains('.') {
584            return Err("下单: 代码不带交易所前缀(交易所走下拉)".to_string());
585        }
586        let exchange = ek_data::constant::Exchange::from_str(self.exchange.trim())
587            .map_err(|e| format!("下单: {e}"))?;
588        let price: f64 = self
589            .price
590            .trim()
591            .parse()
592            .map_err(|_| format!("下单: 价格无效 {:?}", self.price))?;
593        let volume: f64 = self
594            .volume
595            .trim()
596            .parse()
597            .map_err(|_| format!("下单: 数量无效 {:?}", self.volume))?;
598        if self.order_type != ek_data::constant::OrderType::MARKET && price <= 0.0 {
599            return Err("下单: 限价单价格须为正".to_string());
600        }
601        if volume <= 0.0 {
602            return Err("下单: 数量须为正".to_string());
603        }
604        Ok(ek_data::order_request::OrderRequest {
605            symbol: symbol.to_string(),
606            exchange,
607            direction: self.direction,
608            order_type: self.order_type,
609            volume,
610            price,
611            offset: self.offset,
612            ..ek_data::order_request::OrderRequest::default()
613        })
614    }
615}
616
617/// 合约库(v4 查询合约窗数据源):en_symbol → ContractData。
618///
619/// 数据面 = ek-ctp 登录后自动 reqQryInstrument 回流(`eContract.` 裸键逐条 +
620/// `eContractFinish` 完成线,driver.rs:779)——**无 ask 口,UI 只收不催**
621/// (用户裁定:订阅不在网关配置定死,查询窗勾选订阅 / 下单代码回车)。
622#[derive(Debug, Default)]
623pub struct ContractStore {
624    contracts: BTreeMap<String, ek_data::contract::ContractData>,
625}
626
627impl ContractStore {
628    pub fn put(&mut self, c: &ek_data::contract::ContractData) {
629        self.contracts
630            .insert(format!("{}.{}", c.exchange.as_str(), c.symbol), c.clone());
631    }
632
633    /// Python ContractManager 查询口径:代码/交易所/名称模糊,留空 = 全部。
634    pub fn query(&self, pattern: &str) -> Vec<ek_data::contract::ContractData> {
635        let p = pattern.trim();
636        self.contracts
637            .values()
638            .filter(|c| {
639                p.is_empty()
640                    || c.symbol.contains(p)
641                    || c.exchange.as_str().eq_ignore_ascii_case(p)
642                    || c.name.contains(p)
643            })
644            .cloned()
645            .collect()
646    }
647
648    pub fn len(&self) -> usize {
649        self.contracts.len()
650    }
651
652    pub fn is_empty(&self) -> bool {
653        self.contracts.is_empty()
654    }
655}
656
657/// 插件面板与宿主观察线程共用的观测状态。
658#[derive(Debug, Default)]
659pub struct UiShared {
660    logs: Mutex<LogRing>,
661    snap: Mutex<Arc<BusSnap>>,
662    market: MarketStore,
663    ctp_form: Mutex<CtpForm>,
664    trade_snap: Mutex<Arc<TradeSnap>>,
665    tick_store: Mutex<TickStore>,
666    order_form: Mutex<OrderForm>,
667    selected_exchange: Mutex<String>,
668    topleft_tab: Mutex<TopLeftTab>,
669    bottomright_tab: Mutex<BottomRightTab>,
670    ops_window: AtomicBool,
671    ctp_window: AtomicBool,
672    contracts_window: AtomicBool,
673    /// 界面主题(BD-UI-34;AtomicU8 = Theme 判别值,菜单钮循环)。
674    theme: AtomicU8,
675    contract_store: Mutex<ContractStore>,
676    contract_query: Mutex<String>,
677    contract_selected: Mutex<Vec<String>>,
678    kline_windows: Mutex<Vec<String>>,
679    rev: AtomicU64,
680    /// 关窗/测试结束信号(窗口面置位,宿主观察线程消费)。
681    stop: AtomicBool,
682    /// svctl 操作指令队列(窗口面入,ui 插件 eTimer 出)。
683    cmds: Mutex<VecDeque<UiCmd>>,
684    /// 待二次确认的移除目标(单槽;再次同名点 = 发令)。
685    pending_confirm: Mutex<Option<String>>,
686    /// 表格行选中(BD-UI-31):表 id → 选中行键(每表一行;键由渲染侧构造,
687    /// 纯界面态,不发指令;悬停跳色是渲染侧即时判断,不入状态)。
688    row_sel: Mutex<BTreeMap<String, String>>,
689}
690
691impl UiShared {
692    /// 空状态(快照空、日志空、rev 0)。
693    pub fn new() -> Self {
694        Self::default()
695    }
696
697    // ---- v2 布局面访问器(tick 库 / 表单 / 选中 / tab) ----
698
699    /// 落一条 tick(eTick. 裸键上屏;逐 symbol 覆盖)。
700    pub fn update_tick(&self, t: &ek_data::tick::TickData) {
701        self.tick_store.lock().unwrap().put(t);
702        self.bump();
703    }
704
705    /// tick 只读视图(排序后)。
706    pub fn tick_rows(&self) -> Vec<ek_data::tick::TickData> {
707        self.tick_store.lock().unwrap().rows()
708    }
709
710    /// tick 按交易所分组(行情 tab 二级 tab 键表)。
711    pub fn tick_groups(&self) -> Vec<(String, Vec<ek_data::tick::TickData>)> {
712        self.tick_store.lock().unwrap().groups()
713    }
714
715    /// tick 库格数。
716    pub fn tick_count(&self) -> usize {
717        self.tick_store.lock().unwrap().len()
718    }
719
720    /// 下单表单只读快照。
721    pub fn order_form(&self) -> OrderForm {
722        self.order_form.lock().unwrap().clone()
723    }
724
725    /// 下单表单写(渲染侧每控件改动即写)。
726    pub fn set_order_form(&self, f: OrderForm) {
727        *self.order_form.lock().unwrap() = f;
728        self.bump();
729    }
730
731    /// 选中交易所(行情二级 tab 态;行情行单击 = 带入下单面板见 render 侧)。
732    pub fn selected_exchange(&self) -> String {
733        self.selected_exchange.lock().unwrap().clone()
734    }
735
736    /// 选中交易所写。
737    pub fn set_selected_exchange(&self, s: impl Into<String>) {
738        *self.selected_exchange.lock().unwrap() = s.into();
739        self.bump();
740    }
741
742    /// 左上组当前 tab。
743    pub fn topleft_tab(&self) -> TopLeftTab {
744        *self.topleft_tab.lock().unwrap()
745    }
746
747    /// 左上组 tab 写。
748    pub fn set_topleft_tab(&self, t: TopLeftTab) {
749        *self.topleft_tab.lock().unwrap() = t;
750        self.bump();
751    }
752
753    /// 右下组当前 tab。
754    pub fn bottomright_tab(&self) -> BottomRightTab {
755        *self.bottomright_tab.lock().unwrap()
756    }
757
758    /// 右下组 tab 写。
759    pub fn set_bottomright_tab(&self, t: BottomRightTab) {
760        *self.bottomright_tab.lock().unwrap() = t;
761        self.bump();
762    }
763
764    // ---- v4 弹窗面(egui::Window 开关;kittest 可断言) ----
765
766    /// 当前主题(BD-UI-34)。
767    pub fn theme(&self) -> Theme {
768        match self.theme.load(Ordering::Acquire) {
769            1 => Theme::EguiDark,
770            2 => Theme::EguiLight,
771            3 => Theme::QtClassic,
772            _ => Theme::FinanceDark,
773        }
774    }
775
776    /// 主题写。
777    pub fn set_theme(&self, t: Theme) {
778        self.theme.store(t as u8, Ordering::Release);
779        self.bump();
780    }
781
782    /// 菜单钮一键循环:切到下一主题并返回。
783    pub fn cycle_theme(&self) -> Theme {
784        let t = self.theme().next();
785        self.set_theme(t);
786        t
787    }
788
789    /// 运维窗开合。
790    pub fn ops_window_open(&self) -> bool {
791        self.ops_window.load(Ordering::Acquire)
792    }
793
794    /// 运维窗开合写。
795    pub fn set_ops_window_open(&self, v: bool) {
796        self.ops_window.store(v, Ordering::Release);
797        self.bump();
798    }
799
800    /// CTP 配置窗开合。
801    pub fn ctp_window_open(&self) -> bool {
802        self.ctp_window.load(Ordering::Acquire)
803    }
804
805    /// CTP 配置窗开合写。
806    pub fn set_ctp_window_open(&self, v: bool) {
807        self.ctp_window.store(v, Ordering::Release);
808        self.bump();
809    }
810
811    /// 已开 K线窗合约表(多开,去重)。
812    pub fn kline_symbols(&self) -> Vec<String> {
813        self.kline_windows.lock().unwrap().clone()
814    }
815
816    /// 开 K线窗(去重;日志留痕可断言)。
817    pub fn open_kline(&self, symbol: impl Into<String>) {
818        let s = symbol.into();
819        let mut v = self.kline_windows.lock().unwrap();
820        if !v.contains(&s) {
821            v.push(s.clone());
822        }
823        drop(v);
824        self.log_line(format!("[kline] 打开 K线窗 {s}"));
825    }
826
827    /// 关 K线窗(窗关闭钮 → 出表)。
828    pub fn close_kline(&self, symbol: &str) {
829        self.kline_windows.lock().unwrap().retain(|s| s != symbol);
830        self.bump();
831    }
832
833    // ---- v4 查询合约窗面(合约库 / 查询串 / 勾选集) ----
834
835    /// 查询合约窗开合。
836    pub fn contracts_window_open(&self) -> bool {
837        self.contracts_window.load(Ordering::Acquire)
838    }
839
840    /// 查询合约窗开合写。
841    pub fn set_contracts_window_open(&self, v: bool) {
842        self.contracts_window.store(v, Ordering::Release);
843        self.bump();
844    }
845
846    /// 落一条合约(`eContract.` 裸键上屏)。
847    pub fn update_contract(&self, c: &ek_data::contract::ContractData) {
848        self.contract_store.lock().unwrap().put(c);
849        self.bump();
850    }
851
852    /// 合约查询(Python ContractManager 口径:留空 = 全部)。
853    pub fn contracts(&self, pattern: &str) -> Vec<ek_data::contract::ContractData> {
854        self.contract_store.lock().unwrap().query(pattern)
855    }
856
857    /// 合约库格数。
858    pub fn contract_count(&self) -> usize {
859        self.contract_store.lock().unwrap().len()
860    }
861
862    /// 查询串读写。
863    pub fn contract_query(&self) -> String {
864        self.contract_query.lock().unwrap().clone()
865    }
866
867    /// 查询串写。
868    pub fn set_contract_query(&self, q: impl Into<String>) {
869        *self.contract_query.lock().unwrap() = q.into();
870        self.bump();
871    }
872
873    /// 勾选翻转(en_symbol)。
874    pub fn toggle_contract_selection(&self, en_symbol: &str) {
875        let mut v = self.contract_selected.lock().unwrap();
876        match v.iter().position(|s| s == en_symbol) {
877            Some(i) => {
878                v.remove(i);
879            }
880            None => v.push(en_symbol.to_string()),
881        }
882        self.bump();
883    }
884
885    /// 当前勾选集。
886    pub fn selected_contracts(&self) -> Vec<String> {
887        self.contract_selected.lock().unwrap().clone()
888    }
889
890    /// 追加一行日志(总线面/宿主观察线程写)。
891    pub fn log_line(&self, line: impl Into<String>) {
892        self.logs.lock().unwrap().push(line);
893        self.bump();
894    }
895
896    /// 清空日志(渲染"清空日志"按钮 / 测试)。
897    pub fn clear_logs(&self) {
898        self.logs.lock().unwrap().clear();
899        self.bump();
900    }
901
902    /// 日志只读快照 + 溢出计数。
903    pub fn logs_snapshot(&self) -> (Vec<String>, u64) {
904        let r = self.logs.lock().unwrap();
905        (r.snapshot(), r.spilled())
906    }
907
908    /// 日志当前长度(kittest 断言用)。
909    pub fn logs_len(&self) -> usize {
910        self.logs.lock().unwrap().len()
911    }
912
913    /// 落一次服务快照(eTimer 节流回调)。
914    pub fn set_snapshot(&self, snap: BusSnap) {
915        *self.snap.lock().unwrap() = Arc::new(snap);
916        self.bump();
917    }
918
919    /// 当前快照(Arc 克隆,零拷贝走锁)。
920    pub fn snapshot(&self) -> Arc<BusSnap> {
921        Arc::clone(&self.snap.lock().unwrap())
922    }
923
924    /// revision:任何写入 +1(插件面 ePlug 事件也用;测试判定"有事件进来过")。
925    pub fn rev(&self) -> u64 {
926        self.rev.load(Ordering::Acquire)
927    }
928
929    /// 手动推一次 revision(总线面 ePlugUp/Down 记账用)。
930    pub fn bump(&self) {
931        self.rev.fetch_add(1, Ordering::AcqRel);
932    }
933
934    /// 置关窗信号(eframe App 关窗钩子调用)。
935    pub fn request_stop(&self) {
936        self.stop.store(true, Ordering::Release);
937    }
938
939    /// 观察线程轮询用。
940    pub fn is_stop(&self) -> bool {
941        self.stop.load(Ordering::Acquire)
942    }
943
944    // ---- svctl 操作面(S2) ----
945
946    /// 入一条操作指令(窗口面离场动作唯一通道;左位不直调)。
947    pub fn push_cmd(&self, cmd: UiCmd) {
948        self.cmds.lock().unwrap().push_back(cmd);
949        self.bump();
950    }
951
952    /// 取走全部指令(eTimer 节拍辞行)。
953    pub fn drain_cmds(&self) -> Vec<UiCmd> {
954        self.cmds.lock().unwrap().drain(..).collect()
955    }
956
957    /// 当前待二次确认目标(Some = 二次确认中)。
958    pub fn pending_confirm(&self) -> Option<String> {
959        self.pending_confirm.lock().unwrap().clone()
960    }
961
962    /// 确认步进:同名二点 = 发令(Fired),异名 = 迁移(Switched),首点 = 进入待确认。
963    pub fn confirm_step(&self, name: &str) -> ConfirmStep {
964        let mut g = self.pending_confirm.lock().unwrap();
965        match g.take() {
966            Some(p) if p == name => ConfirmStep::Fired,
967            Some(_) => {
968                *g = Some(name.to_string());
969                ConfirmStep::Switched
970            }
971            None => {
972                *g = Some(name.to_string());
973                ConfirmStep::Armed
974            }
975        }
976    }
977
978    /// 清除待确认(取消钮)。
979    pub fn confirm_cancel(&self) {
980        *self.pending_confirm.lock().unwrap() = None;
981        self.bump();
982    }
983
984    // ---- S4 行情面 ----
985
986    /// 行情库直读(读写两用:worker 写,渲染读)。
987    pub fn market(&self) -> &MarketStore {
988        &self.market
989    }
990
991    // ---- S5 交易面 ----
992
993    /// 交易监控快照写(ui 插件 eTimer 拍)。
994    pub fn set_trade_snap(&self, ts: TradeSnap) {
995        *self.trade_snap.lock().unwrap() = Arc::new(ts);
996        self.bump();
997    }
998
999    /// 交易监控快照读(渲染)。
1000    pub fn trade_snap(&self) -> Arc<TradeSnap> {
1001        self.trade_snap.lock().unwrap().clone()
1002    }
1003
1004    // ---- S6 CTP 配置面 ----
1005
1006    /// CTP 配置表单读(渲染克隆面)。
1007    pub fn ctp_form(&self) -> CtpForm {
1008        self.ctp_form.lock().unwrap().clone()
1009    }
1010
1011    /// CTP 配置表单写。
1012    pub fn set_ctp_form(&self, f: CtpForm) {
1013        *self.ctp_form.lock().unwrap() = f;
1014        self.bump();
1015    }
1016
1017    // ---- BD-UI-31 表格行选中面 ----
1018
1019    /// 该行是否为所属表的选中行(渲染判断;无键 = 未选)。
1020    pub fn row_selected(&self, table: &str, key: &str) -> bool {
1021        self.row_sel
1022            .lock()
1023            .unwrap()
1024            .get(table)
1025            .is_some_and(|k| k == key)
1026    }
1027
1028    /// 设所属表的选中行(每表一行;同键重复点 = 保持)。
1029    pub fn select_row(&self, table: &str, key: &str) {
1030        self.row_sel
1031            .lock()
1032            .unwrap()
1033            .insert(table.to_string(), key.to_string());
1034    }
1035}