1use std::sync::Arc;
5
6use futu_net::client::FutuClient;
7use serde::Serialize;
8
9use crate::state::parse_symbol;
10use anyhow::Result;
11
12#[derive(Serialize)]
13struct SnapshotOut {
14 symbol: String,
15 name: Option<String>,
16 update_time: String,
17 cur_price: f64,
18 last_close: f64,
19 change_rate: f64,
20 open: f64,
21 high: f64,
22 low: f64,
23 volume: i64,
24 turnover: f64,
25 turnover_rate: f64,
26 amplitude: Option<f64>,
27 avg_price: Option<f64>,
28 volume_ratio: Option<f64>,
29 highest52: Option<f64>,
30 lowest52: Option<f64>,
31 ask_price: Option<f64>,
32 bid_price: Option<f64>,
33 is_suspend: bool,
34 lot_size: i32,
35 #[serde(skip_serializing_if = "Option::is_none")]
39 overnight: Option<OvernightOut>,
40 #[serde(skip_serializing_if = "Option::is_none")]
45 exchange_code: Option<String>,
46 #[serde(skip_serializing_if = "Option::is_none")]
54 option_greeks: Option<OptionGreeksOut>,
55
56 #[serde(skip_serializing_if = "Option::is_none")]
61 equity_fundamental: Option<EquityFundamentalOut>,
62
63 #[serde(skip_serializing_if = "Option::is_none")]
67 warrant_data: Option<WarrantDataOut>,
68
69 #[serde(skip_serializing_if = "Option::is_none")]
73 future_data: Option<FutureDataOut>,
74}
75
76#[derive(Serialize)]
79struct EquityFundamentalOut {
80 issued_shares: i64,
82 issued_market_val: f64,
84 net_asset: f64,
86 net_profit: f64,
88 earnings_per_share: f64,
90 outstanding_shares: i64,
92 outstanding_market_val: f64,
94 net_asset_per_share: f64,
96 ey_rate: f64,
98 pe_rate: f64,
100 pb_rate: f64,
102 pe_ttm_rate: f64,
104 dividend_ttm: Option<f64>,
106 dividend_ratio_ttm: Option<f64>,
108 dividend_lfy: Option<f64>,
110 dividend_lfy_ratio: Option<f64>,
112}
113
114#[derive(Serialize)]
117struct WarrantDataOut {
118 conversion_rate: f64,
120 warrant_type: i32,
122 strike_price: f64,
124 maturity_time: String,
126 end_trade_time: String,
128 recovery_price: f64,
130 street_volume: i64,
132 issue_volume: i64,
134 street_rate: f64,
136 delta: f64,
138 implied_volatility: f64,
140 premium: f64,
142 leverage: Option<f64>,
144 ipop: Option<f64>,
146 break_even_point: Option<f64>,
148 conversion_price: Option<f64>,
150}
151
152#[derive(Serialize)]
155struct FutureDataOut {
156 last_settle_price: f64,
158 position: i32,
160 position_change: i32,
162 last_trade_time: String,
164 is_main_contract: bool,
166}
167
168#[derive(Serialize)]
173struct OptionGreeksOut {
174 implied_volatility: f64,
176 premium: f64,
178 delta: f64,
180 gamma: f64,
182 vega: f64,
184 theta: f64,
186 rho: f64,
188 #[serde(skip_serializing_if = "Option::is_none")]
190 open_interest: Option<i32>,
191 #[serde(skip_serializing_if = "Option::is_none")]
193 strike_price: Option<f64>,
194 #[serde(skip_serializing_if = "Option::is_none")]
196 contract_size: Option<i32>,
197 #[serde(skip_serializing_if = "Option::is_none")]
199 expiry_date_distance: Option<i32>,
200 #[serde(skip_serializing_if = "Option::is_none")]
202 contract_nominal_value: Option<f64>,
203 #[serde(skip_serializing_if = "Option::is_none")]
205 net_open_interest: Option<i32>,
206}
207
208#[derive(Serialize)]
210struct OvernightOut {
211 #[serde(skip_serializing_if = "Option::is_none")]
213 price: Option<f64>,
214 #[serde(skip_serializing_if = "Option::is_none")]
216 high: Option<f64>,
217 #[serde(skip_serializing_if = "Option::is_none")]
219 low: Option<f64>,
220 #[serde(skip_serializing_if = "Option::is_none")]
222 volume: Option<i64>,
223 #[serde(skip_serializing_if = "Option::is_none")]
225 turnover: Option<f64>,
226 #[serde(skip_serializing_if = "Option::is_none")]
228 change_val: Option<f64>,
229 #[serde(skip_serializing_if = "Option::is_none")]
231 change_rate: Option<f64>,
232 #[serde(skip_serializing_if = "Option::is_none")]
234 amplitude: Option<f64>,
235}
236
237pub async fn get_snapshot(client: &Arc<FutuClient>, symbol: &str) -> Result<String> {
238 let sec = parse_symbol(symbol)?;
239 let s2c = futu_qot::snapshot::get_security_snapshot(client, std::slice::from_ref(&sec)).await?;
240 let snap = s2c
241 .snapshot_list
242 .first()
243 .ok_or_else(|| anyhow::anyhow!("empty snapshot result"))?;
244 let b = &snap.basic;
245
246 let change_rate = if b.last_close_price.abs() > f64::EPSILON {
247 (b.cur_price - b.last_close_price) / b.last_close_price * 100.0
248 } else {
249 0.0
250 };
251
252 let market_prefix = match b.security.market {
253 1 => "HK",
254 2 => "HK_FUTURE",
255 11 => "US",
256 21 => "SH",
257 22 => "SZ",
258 31 => "SG",
259 41 => "JP",
260 51 => "AU",
261 61 => "MY",
262 71 => "CA",
263 81 => "FX",
264 91 => "CRYPTO",
265 _ => "UNKNOWN",
266 };
267
268 let exchange_code: Option<String> =
272 match futu_qot::static_info::get_static_info(client, std::slice::from_ref(&sec)).await {
273 Ok(infos) => infos
274 .first()
275 .and_then(|i| i.exchange_code())
276 .map(String::from),
277 Err(e) => {
278 tracing::warn!(
279 "get_static_info for exchange_code lookup failed: {e}; \
280 exchange_code 字段省略"
281 );
282 None
283 }
284 };
285
286 let out = SnapshotOut {
287 symbol: format!("{market_prefix}.{}", b.security.code),
288 name: b.name.clone(),
289 update_time: b.update_time.clone(),
290 cur_price: b.cur_price,
291 last_close: b.last_close_price,
292 change_rate,
293 open: b.open_price,
294 high: b.high_price,
295 low: b.low_price,
296 volume: b.volume,
297 turnover: b.turnover,
298 turnover_rate: b.turnover_rate,
299 amplitude: b.amplitude,
300 avg_price: b.avg_price,
301 volume_ratio: b.volume_ratio,
302 highest52: b.highest52_weeks_price,
303 lowest52: b.lowest52_weeks_price,
304 ask_price: b.ask_price,
305 bid_price: b.bid_price,
306 is_suspend: b.is_suspend,
307 lot_size: b.lot_size,
308 overnight: b.overnight.as_ref().map(|on| OvernightOut {
310 price: on.price,
311 high: on.high_price,
312 low: on.low_price,
313 volume: on.volume,
314 turnover: on.turnover,
315 change_val: on.change_val,
316 change_rate: on.change_rate,
317 amplitude: on.amplitude,
318 }),
319 exchange_code,
321 option_greeks: snap.option_ex_data.as_ref().map(|o| OptionGreeksOut {
324 implied_volatility: o.implied_volatility,
325 premium: o.premium,
326 delta: o.delta,
327 gamma: o.gamma,
328 vega: o.vega,
329 theta: o.theta,
330 rho: o.rho,
331 open_interest: Some(o.open_interest),
332 strike_price: Some(o.strike_price),
333 contract_size: Some(o.contract_size),
334 expiry_date_distance: o.expiry_date_distance,
335 contract_nominal_value: o.contract_nominal_value,
336 net_open_interest: o.net_open_interest,
337 }),
338 equity_fundamental: snap.equity_ex_data.as_ref().map(|e| EquityFundamentalOut {
340 issued_shares: e.issued_shares,
341 issued_market_val: e.issued_market_val,
342 net_asset: e.net_asset,
343 net_profit: e.net_profit,
344 earnings_per_share: e.earnings_pershare,
345 outstanding_shares: e.outstanding_shares,
346 outstanding_market_val: e.outstanding_market_val,
347 net_asset_per_share: e.net_asset_pershare,
348 ey_rate: e.ey_rate,
349 pe_rate: e.pe_rate,
350 pb_rate: e.pb_rate,
351 pe_ttm_rate: e.pe_ttm_rate,
352 dividend_ttm: e.dividend_ttm,
353 dividend_ratio_ttm: e.dividend_ratio_ttm,
354 dividend_lfy: e.dividend_lfy,
355 dividend_lfy_ratio: e.dividend_lfy_ratio,
356 }),
357 warrant_data: snap.warrant_ex_data.as_ref().map(|w| WarrantDataOut {
359 conversion_rate: w.conversion_rate,
360 warrant_type: w.warrant_type,
361 strike_price: w.strike_price,
362 maturity_time: w.maturity_time.clone(),
363 end_trade_time: w.end_trade_time.clone(),
364 recovery_price: w.recovery_price,
365 street_volume: w.street_volumn,
366 issue_volume: w.issue_volumn,
367 street_rate: w.street_rate,
368 delta: w.delta,
369 implied_volatility: w.implied_volatility,
370 premium: w.premium,
371 leverage: w.leverage,
372 ipop: w.ipop,
373 break_even_point: w.break_even_point,
374 conversion_price: w.conversion_price,
375 }),
376 future_data: snap.future_ex_data.as_ref().map(|f| FutureDataOut {
378 last_settle_price: f.last_settle_price,
379 position: f.position,
380 position_change: f.position_change,
381 last_trade_time: f.last_trade_time.clone(),
382 is_main_contract: f.is_main_contract,
383 }),
384 };
385 Ok(serde_json::to_string_pretty(&out)?)
386}
387
388