futu_mcp/handlers/
core.rs

1//! 核心 handler:ping / quote / snapshot
2
3use std::sync::Arc;
4use std::time::Duration;
5
6use anyhow::Result;
7use futu_net::client::FutuClient;
8use futu_qot::types::SubType;
9use serde::Serialize;
10
11use crate::state::{format_symbol, parse_symbol};
12
13#[derive(Serialize)]
14pub struct PingOut {
15    pub gateway: String,
16    pub ok: bool,
17    pub rtt_ms: f64,
18    pub message: String,
19}
20
21pub async fn ping(client: &Arc<FutuClient>, gateway: &str) -> PingOut {
22    let t0 = std::time::Instant::now();
23    match futu_qot::market_misc::get_sub_info(client, false).await {
24        Ok(_) => PingOut {
25            gateway: gateway.to_string(),
26            ok: true,
27            rtt_ms: t0.elapsed().as_secs_f64() * 1000.0,
28            message: "pong".into(),
29        },
30        Err(e) => PingOut {
31            gateway: gateway.to_string(),
32            ok: false,
33            rtt_ms: 0.0,
34            message: format!("request failed: {e}"),
35        },
36    }
37}
38
39#[derive(Serialize)]
40struct QuoteOut {
41    symbol: String,
42    update_time: String,
43    cur_price: f64,
44    last_close: f64,
45    open: f64,
46    high: f64,
47    low: f64,
48    volume: i64,
49    turnover: f64,
50    turnover_rate: f64,
51    amplitude: f64,
52    change_rate: f64,
53}
54
55pub async fn get_quote(client: &Arc<FutuClient>, symbol: &str) -> Result<String> {
56    let sec = parse_symbol(symbol)?;
57
58    futu_qot::sub::subscribe(
59        client,
60        std::slice::from_ref(&sec),
61        &[SubType::Basic],
62        true,
63        true,
64    )
65    .await?;
66    tokio::time::sleep(Duration::from_millis(300)).await;
67
68    let qots = futu_qot::basic_qot::get_basic_qot(client, std::slice::from_ref(&sec)).await?;
69    let q = qots
70        .first()
71        .ok_or_else(|| anyhow::anyhow!("empty quote result"))?;
72
73    let change_rate = if q.last_close_price.abs() > f64::EPSILON {
74        (q.cur_price - q.last_close_price) / q.last_close_price * 100.0
75    } else {
76        0.0
77    };
78
79    let out = QuoteOut {
80        symbol: format_symbol(&q.security),
81        update_time: q.update_time.clone(),
82        cur_price: q.cur_price,
83        last_close: q.last_close_price,
84        open: q.open_price,
85        high: q.high_price,
86        low: q.low_price,
87        volume: q.volume,
88        turnover: q.turnover,
89        turnover_rate: q.turnover_rate,
90        amplitude: q.amplitude,
91        change_rate,
92    };
93    Ok(serde_json::to_string_pretty(&out)?)
94}
95
96#[derive(Serialize)]
97struct SnapshotOut {
98    symbol: String,
99    name: Option<String>,
100    update_time: String,
101    cur_price: f64,
102    last_close: f64,
103    change_rate: f64,
104    open: f64,
105    high: f64,
106    low: f64,
107    volume: i64,
108    turnover: f64,
109    turnover_rate: f64,
110    amplitude: Option<f64>,
111    avg_price: Option<f64>,
112    volume_ratio: Option<f64>,
113    highest52: Option<f64>,
114    lowest52: Option<f64>,
115    ask_price: Option<f64>,
116    bid_price: Option<f64>,
117    is_suspend: bool,
118    lot_size: i32,
119}
120
121pub async fn get_snapshot(client: &Arc<FutuClient>, symbol: &str) -> Result<String> {
122    let sec = parse_symbol(symbol)?;
123    let s2c = futu_qot::snapshot::get_security_snapshot(client, std::slice::from_ref(&sec)).await?;
124    let snap = s2c
125        .snapshot_list
126        .first()
127        .ok_or_else(|| anyhow::anyhow!("empty snapshot result"))?;
128    let b = &snap.basic;
129
130    let change_rate = if b.last_close_price.abs() > f64::EPSILON {
131        (b.cur_price - b.last_close_price) / b.last_close_price * 100.0
132    } else {
133        0.0
134    };
135
136    let market_prefix = match b.security.market {
137        1 => "HK",
138        2 => "HK_FUTURE",
139        11 => "US",
140        21 => "SH",
141        22 => "SZ",
142        _ => "UNKNOWN",
143    };
144
145    let out = SnapshotOut {
146        symbol: format!("{market_prefix}.{}", b.security.code),
147        name: b.name.clone(),
148        update_time: b.update_time.clone(),
149        cur_price: b.cur_price,
150        last_close: b.last_close_price,
151        change_rate,
152        open: b.open_price,
153        high: b.high_price,
154        low: b.low_price,
155        volume: b.volume,
156        turnover: b.turnover,
157        turnover_rate: b.turnover_rate,
158        amplitude: b.amplitude,
159        avg_price: b.avg_price,
160        volume_ratio: b.volume_ratio,
161        highest52: b.highest52_weeks_price,
162        lowest52: b.lowest52_weeks_price,
163        ask_price: b.ask_price,
164        bid_price: b.bid_price,
165        is_suspend: b.is_suspend,
166        lot_size: b.lot_size,
167    };
168    Ok(serde_json::to_string_pretty(&out)?)
169}