Skip to main content

futucli/cmd/proto_json/
combo.rs

1//! combo 下单 / combo max qty proto-JSON 命令 + PacketID 派生
2//! (v1.8.0 large-file split, 纯搬移自 `proto_json.rs`).
3
4use std::collections::hash_map::DefaultHasher;
5use std::hash::{Hash, Hasher};
6use std::sync::atomic::{AtomicU32, Ordering};
7
8use anyhow::{Result, anyhow, bail};
9use prost::Message;
10
11use crate::common::connect_gateway;
12use crate::output::OutputFormat;
13
14use super::contract::{parse_combo_max_c2s_json, parse_place_combo_c2s_json};
15use super::transport::{ensure_success, print_proto_response, send_proto};
16
17static PACKET_SERIAL: AtomicU32 = AtomicU32::new(1);
18
19pub async fn run_combo_max_trd_qtys(
20    gateway: &str,
21    c2s_json: &str,
22    output: OutputFormat,
23) -> Result<()> {
24    let c2s = parse_combo_max_c2s_json(c2s_json)?;
25    let request = futu_proto::trd_get_combo_max_trd_qtys::Request { c2s };
26    let response: futu_proto::trd_get_combo_max_trd_qtys::Response = send_proto(
27        gateway,
28        "futucli-combo-max-trd-qtys",
29        futu_core::proto_id::TRD_GET_COMBO_MAX_TRD_QTYS,
30        request,
31    )
32    .await?;
33    ensure_success(
34        "combo-max-trd-qtys",
35        response.ret_type,
36        response.ret_msg.as_deref(),
37        response.err_code,
38    )?;
39    print_proto_response(output, &response)
40}
41
42pub async fn run_place_combo_order(
43    gateway: &str,
44    c2s_json: &str,
45    confirm: bool,
46    idempotency_key: Option<String>,
47    output: OutputFormat,
48) -> Result<()> {
49    let mut c2s = parse_place_combo_c2s_json(c2s_json)?;
50
51    ensure_combo_order_confirmed(c2s.header.trd_env, confirm)?;
52
53    let (client, _rx) = connect_gateway(gateway, "futucli-combo-order").await?;
54    c2s.packet_id = match idempotency_key.as_deref() {
55        Some(key) => packet_id_for_idempotency_key(key),
56        None => {
57            let conn_id = client
58                .conn_id()
59                .ok_or_else(|| anyhow!("combo-order missing InitConnect conn_id"))?;
60            next_packet_id(conn_id)
61        }
62    };
63
64    let request = futu_proto::trd_place_combo_order::Request { c2s };
65    let frame = client
66        .request(
67            futu_core::proto_id::TRD_PLACE_COMBO_ORDER,
68            request.encode_to_vec(),
69        )
70        .await?;
71    let response = futu_proto::trd_place_combo_order::Response::decode(frame.body.as_ref())
72        .map_err(|err| anyhow!("decode combo-order response: {err}"))?;
73    ensure_success(
74        "combo-order",
75        response.ret_type,
76        response.ret_msg.as_deref(),
77        response.err_code,
78    )?;
79    print_proto_response(output, &response)
80}
81
82fn next_packet_id(conn_id: u64) -> futu_proto::common::PacketId {
83    let serial_no = PACKET_SERIAL.fetch_add(1, Ordering::Relaxed);
84    futu_proto::common::PacketId { conn_id, serial_no }
85}
86
87pub(super) fn ensure_combo_order_confirmed(trd_env: i32, confirm: bool) -> Result<()> {
88    if trd_env == 1 && !confirm {
89        bail!("combo-order real env requires --confirm");
90    }
91    Ok(())
92}
93
94// Same design as `futu-trd::order`: deterministic key-derived PacketID lets
95// the daemon replay guard identify an explicit retry without turning every
96// identical-looking combo order into an accidental duplicate.
97pub(super) fn packet_id_for_idempotency_key(key: &str) -> futu_proto::common::PacketId {
98    let mut hasher = DefaultHasher::new();
99    key.hash(&mut hasher);
100    futu_proto::common::PacketId {
101        conn_id: hasher.finish(),
102        serial_no: 0,
103    }
104}