Skip to main content

futucli/cmd/trade_ext/
parsers.rs

1use anyhow::{Context, Result, bail};
2use futu_trd::types::{ModifyOrderOp, OrderType, TrdSide};
3
4pub(crate) fn parse_trd_side(s: &str) -> Result<TrdSide> {
5    Ok(match s.trim().to_ascii_uppercase().as_str() {
6        "BUY" => TrdSide::Buy,
7        "SELL" => TrdSide::Sell,
8        "SELL_SHORT" => TrdSide::SellShort,
9        "BUY_BACK" => TrdSide::BuyBack,
10        other => bail!("unknown trd side {other:?} (BUY|SELL|SELL_SHORT|BUY_BACK)"),
11    })
12}
13
14pub(crate) fn parse_order_type(s: &str) -> Result<OrderType> {
15    Ok(match s.trim().to_ascii_uppercase().as_str() {
16        "NORMAL" | "LIMIT" => OrderType::Normal,
17        "MARKET" => OrderType::Market,
18        "ABSOLUTE_LIMIT" => OrderType::AbsoluteLimit,
19        "AUCTION" => OrderType::Auction,
20        "AUCTION_LIMIT" => OrderType::AuctionLimit,
21        "SPECIAL_LIMIT" => OrderType::SpecialLimit,
22        "SPECIAL_LIMIT_ALL" => OrderType::SpecialLimitAll,
23        // v1.4.53 F1 条件单
24        "STOP" => OrderType::Stop,
25        "STOP_LIMIT" | "STOP-LIMIT" => OrderType::StopLimit,
26        "MIT" | "MARKET_IF_TOUCHED" => OrderType::MarketifTouched,
27        "LIT" | "LIMIT_IF_TOUCHED" => OrderType::LimitifTouched,
28        "TRAIL" | "TRAILING_STOP" | "TRAILING-STOP" => OrderType::TrailingStop,
29        "TRAIL_LIMIT" | "TRAILING_STOP_LIMIT" => OrderType::TrailingStopLimit,
30        "TWAP_MARKET" => OrderType::TwapMarket,
31        "TWAP_LIMIT" => OrderType::TwapLimit,
32        "VWAP_MARKET" => OrderType::VwapMarket,
33        "VWAP_LIMIT" => OrderType::VwapLimit,
34        other => bail!(
35            "unknown order type {other:?} (NORMAL|MARKET|ABSOLUTE_LIMIT|AUCTION|\
36             AUCTION_LIMIT|SPECIAL_LIMIT|SPECIAL_LIMIT_ALL|STOP|STOP_LIMIT|MIT|LIT|\
37             TRAILING_STOP|TRAILING_STOP_LIMIT|TWAP_MARKET|TWAP_LIMIT|VWAP_MARKET|VWAP_LIMIT)"
38        ),
39    })
40}
41
42pub(crate) fn parse_modify_op(s: &str) -> Result<ModifyOrderOp> {
43    Ok(match s.trim().to_ascii_uppercase().as_str() {
44        "NORMAL" => ModifyOrderOp::Normal,
45        "CANCEL" => ModifyOrderOp::Cancel,
46        "DISABLE" => ModifyOrderOp::Disable,
47        "ENABLE" => ModifyOrderOp::Enable,
48        "DELETE" => ModifyOrderOp::Delete,
49        other => bail!("unknown modify op {other:?} (NORMAL|CANCEL|DISABLE|ENABLE|DELETE)"),
50    })
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub(crate) struct ResolvedOrderIdArg {
55    pub(crate) order_id: u64,
56    pub(crate) order_id_ex: Option<String>,
57    pub(crate) idempotency_component: String,
58}
59
60pub(crate) fn resolve_order_id_arg(raw: &str) -> Result<ResolvedOrderIdArg> {
61    let trimmed = raw.trim();
62    if trimmed.is_empty() {
63        bail!("--order-id must not be empty");
64    }
65
66    // C++ APIServer accepts `orderIDEx` as an alternative to numeric `orderID`
67    // and hashes it at entry. Ref:
68    // /Users/leaf/ai-lab/o-src/FutuOpenD/Src/APIServer/Business/Trade/APIServer_Trd_ModifyOrder.cpp:256
69    if trimmed.bytes().all(|b| b.is_ascii_digit()) {
70        let order_id = trimmed
71            .parse::<u64>()
72            .with_context(|| format!("invalid numeric --order-id {trimmed:?}"))?;
73        return Ok(ResolvedOrderIdArg {
74            order_id,
75            order_id_ex: None,
76            idempotency_component: trimmed.to_string(),
77        });
78    }
79
80    Ok(ResolvedOrderIdArg {
81        order_id: 0,
82        order_id_ex: Some(trimmed.to_string()),
83        idempotency_component: trimmed.to_string(),
84    })
85}
86
87pub(crate) fn parse_numeric_order_id_arg(raw: &str, field: &str) -> Result<u64> {
88    let trimmed = raw.trim();
89    if trimmed.is_empty() {
90        bail!("{field} must not be empty");
91    }
92    if !trimmed.bytes().all(|b| b.is_ascii_digit()) {
93        bail!(
94            "{field} for reconfirm-order must be numeric FTAPI order_id; \
95             orderIDEx is not supported by Trd_ReconfirmOrder"
96        );
97    }
98    trimmed
99        .parse::<u64>()
100        .with_context(|| format!("invalid {field} {trimmed:?}"))
101}