1use anyhow::{Context, Result, bail};
20
21use crate::cmd::account::{parse_trd_env, parse_trd_market_for_write};
22use crate::common::connect_gateway;
23use crate::output::OutputFormat;
24
25mod cash_flow;
26mod hints;
27mod history;
28mod idempotency;
29mod margin_fee;
30mod max_qtys;
31mod parsers;
32mod write_output;
33
34#[cfg(test)]
35mod tests;
36
37pub use cash_flow::{AccCashFlowRangeCommand, run_acc_cash_flow, run_acc_cash_flow_range};
38pub use history::{
39 HistoryDealsCommand, HistoryOrdersCommand, run_history_deals, run_history_orders,
40};
41pub use margin_fee::{run_margin_ratio, run_order_fee};
42pub use max_qtys::{MaxQtysCommand, run_max_qtys};
43
44#[cfg(test)]
45pub(crate) use cash_flow::acc_cash_flow_advance_day;
46pub(crate) use hints::emit_trade_hint_if_known;
47#[cfg(test)]
48pub(crate) use hints::translate_trade_ret_msg;
49#[cfg(test)]
50pub(crate) use history::validate_history_time_range;
51pub(crate) use idempotency::{IdempotencyParams, resolve_auto_idempotency_key};
52pub(crate) use parsers::{
53 parse_modify_op, parse_numeric_order_id_arg, parse_order_type, parse_trd_side,
54 resolve_order_id_arg,
55};
56#[cfg(test)]
57pub(crate) use write_output::render_trade_write_success;
58pub(crate) use write_output::{TradeWriteSuccess, emit_trade_write_success};
59
60use futu_trd::misc::reconfirm_order;
61use futu_trd::order::{modify_order, place_order};
62use futu_trd::types::{ModifyOrderOp, ModifyOrderParams, PlaceOrderParams, TrdEnv, TrdHeader};
63
64pub struct PlaceOrderCommand<'a> {
67 pub gateway: &'a str,
68 pub env: &'a str,
69 pub acc_id: u64,
70 pub market: &'a str,
71 pub side: &'a str,
72 pub order_type: &'a str,
73 pub code: &'a str,
74 pub qty: f64,
75 pub price: Option<f64>,
76 pub jp_acc_type: Option<i32>,
77 pub confirm: bool,
78 pub idempotency_key: Option<String>,
79 pub stop_price: Option<f64>,
81 pub trail_type: Option<i32>,
82 pub trail_value: Option<f64>,
83 pub trail_spread: Option<f64>,
84 pub output: OutputFormat,
85}
86
87pub async fn run_place_order(input: PlaceOrderCommand<'_>) -> Result<()> {
88 let idempotency_key = resolve_auto_idempotency_key(
90 input.idempotency_key,
91 &IdempotencyParams {
92 acc_id: input.acc_id,
93 market: input.market,
94 code: input.code,
95 side: input.side,
96 qty: input.qty,
97 price: input.price,
98 order_type: input.order_type,
99 },
100 );
101 let env_p = parse_trd_env(input.env)?;
102 let market_p = parse_trd_market_for_write(input.market)?;
103 let side_p = parse_trd_side(input.side)?;
104 let order_type_p = parse_order_type(input.order_type)?;
105
106 if matches!(env_p, TrdEnv::Real) && !input.confirm {
108 bail!(
109 "real-env place_order requires --confirm for safety. \
110 Re-run with --confirm after double-checking all params. \
111 (Or use --env simulate for paper trading.)"
112 );
113 }
114
115 let placing_msg = format!(
116 "placing {} {:?} × {} @ {} {:?} (env={:?}, acc={}, market={:?}, code={})",
117 input.order_type,
118 side_p,
119 input.qty,
120 input.price.unwrap_or(0.0),
121 order_type_p,
122 env_p,
123 input.acc_id,
124 market_p,
125 input.code
126 );
127 if matches!(input.output, OutputFormat::Table) {
128 println!("{placing_msg}");
129 } else {
130 eprintln!("{placing_msg}");
131 }
132
133 let params = PlaceOrderParams {
134 header: TrdHeader {
135 trd_env: env_p,
136 acc_id: input.acc_id,
137 trd_market: market_p,
138 jp_acc_type: input.jp_acc_type,
139 },
140 trd_side: side_p,
141 order_type: order_type_p,
142 code: input.code.to_string(),
143 qty: input.qty,
144 price: input.price,
145 adjust_price: None,
146 adjust_side_and_limit: None,
147 idempotency_key,
148 aux_price: input.stop_price,
150 trail_type: input.trail_type,
151 trail_value: input.trail_value,
152 trail_spread: input.trail_spread,
153 };
154
155 let (client, _push_rx) = connect_gateway(input.gateway, "futucli-place-order")
156 .await
157 .context("connect gateway")?;
158 let result = match place_order(&client, ¶ms).await {
160 Ok(r) => r,
161 Err(e) => {
162 let wrapped = anyhow::Error::from(e).context("place_order RPC");
163 emit_trade_hint_if_known(&wrapped);
164 return Err(wrapped);
165 }
166 };
167
168 emit_trade_write_success(
169 input.output,
170 TradeWriteSuccess {
171 operation: "place_order",
172 order_id: result.order_id,
173 returned_order_id: None,
174 },
175 )?;
176 if matches!(input.output, OutputFormat::Table) {
177 println!(
178 " (use `futucli order --market {} --acc-id {} --env {}` to verify)",
179 input.market, input.acc_id, input.env
180 );
181 }
182 Ok(())
183}
184
185pub struct ModifyOrderCommand<'a> {
188 pub gateway: &'a str,
189 pub env: &'a str,
190 pub acc_id: u64,
191 pub market: &'a str,
192 pub order_id: String,
193 pub op: &'a str,
194 pub qty: Option<f64>,
195 pub price: Option<f64>,
196 pub jp_acc_type: Option<i32>,
197 pub confirm: bool,
198 pub idempotency_key: Option<String>,
199 pub output: OutputFormat,
200}
201
202pub async fn run_modify_order(input: ModifyOrderCommand<'_>) -> Result<()> {
203 let resolved_order_id = resolve_order_id_arg(&input.order_id)?;
204 let idempotency_key = resolve_auto_idempotency_key(
207 input.idempotency_key,
208 &IdempotencyParams {
209 acc_id: input.acc_id,
210 market: input.market,
211 code: "", side: input.op, qty: input.qty.unwrap_or(0.0),
214 price: input.price,
215 order_type: &resolved_order_id.idempotency_component, },
217 );
218 let env_p = parse_trd_env(input.env)?;
219 let market_p = parse_trd_market_for_write(input.market)?;
220 let op_p = parse_modify_op(input.op)?;
221
222 if matches!(env_p, TrdEnv::Real) && !input.confirm {
223 bail!("real-env modify_order requires --confirm for safety");
224 }
225
226 let params = ModifyOrderParams {
227 header: TrdHeader {
228 trd_env: env_p,
229 acc_id: input.acc_id,
230 trd_market: market_p,
231 jp_acc_type: input.jp_acc_type,
232 },
233 order_id: resolved_order_id.order_id,
234 order_id_ex: resolved_order_id.order_id_ex.clone(),
235 modify_order_op: op_p,
236 qty: input.qty,
237 price: input.price,
238 for_all: None,
239 idempotency_key,
240 };
241
242 let (client, _push_rx) = connect_gateway(input.gateway, "futucli-trade-ext").await?;
243 let ret_order_id = match modify_order(&client, ¶ms).await {
245 Ok(r) => r,
246 Err(e) => {
247 let wrapped = anyhow::Error::from(e).context("modify_order RPC");
248 emit_trade_hint_if_known(&wrapped);
249 return Err(wrapped);
250 }
251 };
252 emit_trade_write_success(
253 input.output,
254 TradeWriteSuccess {
255 operation: "modify_order",
256 order_id: if resolved_order_id.order_id != 0 {
257 resolved_order_id.order_id
258 } else {
259 ret_order_id
260 },
261 returned_order_id: Some(ret_order_id),
262 },
263 )?;
264 Ok(())
265}
266
267pub struct CancelOrderCommand<'a> {
268 pub gateway: &'a str,
269 pub env: &'a str,
270 pub acc_id: u64,
271 pub market: &'a str,
272 pub order_id: String,
273 pub jp_acc_type: Option<i32>,
274 pub confirm: bool,
275 pub idempotency_key: Option<String>,
276 pub output: OutputFormat,
277}
278
279pub async fn run_cancel_order(input: CancelOrderCommand<'_>) -> Result<()> {
280 let resolved_order_id = resolve_order_id_arg(&input.order_id)?;
281 let env_p = parse_trd_env(input.env)?;
282 let market_p = parse_trd_market_for_write(input.market)?;
283
284 if matches!(env_p, TrdEnv::Real) && !input.confirm {
285 bail!("real-env cancel_order requires --confirm for safety");
286 }
287
288 let header = TrdHeader {
289 trd_env: env_p,
290 acc_id: input.acc_id,
291 trd_market: market_p,
292 jp_acc_type: input.jp_acc_type,
293 };
294 let (client, _push_rx) = connect_gateway(input.gateway, "futucli-trade-ext").await?;
295 let params = ModifyOrderParams {
296 header: header.clone(),
297 order_id: resolved_order_id.order_id,
298 order_id_ex: resolved_order_id.order_id_ex.clone(),
299 modify_order_op: ModifyOrderOp::Cancel,
300 qty: None,
301 price: None,
302 for_all: None,
303 idempotency_key: input.idempotency_key,
304 };
305 let ret_order_id = match modify_order(&client, ¶ms).await {
307 Ok(id) => id,
308 Err(e) => {
309 let wrapped = anyhow::Error::from(e).context("cancel_order RPC");
310 emit_trade_hint_if_known(&wrapped);
311 return Err(wrapped);
312 }
313 };
314 emit_trade_write_success(
315 input.output,
316 TradeWriteSuccess {
317 operation: "cancel_order",
318 order_id: if resolved_order_id.order_id != 0 {
319 resolved_order_id.order_id
320 } else {
321 ret_order_id
322 },
323 returned_order_id: None,
324 },
325 )?;
326 Ok(())
327}
328
329pub struct ReconfirmOrderCommand<'a> {
330 pub gateway: &'a str,
331 pub env: &'a str,
332 pub acc_id: u64,
333 pub market: &'a str,
334 pub order_id: String,
335 pub reason: i32,
336 pub jp_acc_type: Option<i32>,
337 pub confirm: bool,
338 pub output: OutputFormat,
339}
340
341pub async fn run_reconfirm_order(input: ReconfirmOrderCommand<'_>) -> Result<()> {
342 let parsed_order_id = parse_numeric_order_id_arg(&input.order_id, "--order-id")?;
343 let env_p = parse_trd_env(input.env)?;
344 let market_p = parse_trd_market_for_write(input.market)?;
345
346 if matches!(env_p, TrdEnv::Real) && !input.confirm {
347 bail!("real-env reconfirm_order requires --confirm for safety");
348 }
349
350 let header = TrdHeader {
351 trd_env: env_p,
352 acc_id: input.acc_id,
353 trd_market: market_p,
354 jp_acc_type: input.jp_acc_type,
355 };
356 let (client, _push_rx) = connect_gateway(input.gateway, "futucli-trade-ext").await?;
357 let ret_order_id = match reconfirm_order(&client, &header, parsed_order_id, input.reason).await
358 {
359 Ok(id) => id,
360 Err(e) => {
361 let wrapped = anyhow::Error::from(e).context("reconfirm_order RPC");
362 emit_trade_hint_if_known(&wrapped);
363 return Err(wrapped);
364 }
365 };
366 emit_trade_write_success(
367 input.output,
368 TradeWriteSuccess {
369 operation: "reconfirm_order",
370 order_id: parsed_order_id,
371 returned_order_id: Some(ret_order_id),
372 },
373 )?;
374 Ok(())
375}
376
377pub async fn run_sub_acc_push(
379 gateway: &str,
380 acc_ids: &[u64],
381 _format: crate::output::OutputFormat,
382) -> Result<()> {
383 if acc_ids.is_empty() {
384 bail!("need at least one acc_id");
385 }
386 let (client, _rx) = connect_gateway(gateway, "futucli-sub-acc-push").await?;
387 futu_trd::misc::sub_acc_push(&client, acc_ids).await?;
388 println!("✅ sub_acc_push ok: {acc_ids:?}");
389 Ok(())
390}
391
392pub async fn run_cancel_all_order(
399 gateway: &str,
400 acc_id: u64,
401 env: &str,
402 market: Option<&str>,
403 jp_acc_type: Option<i32>,
404 confirm: bool,
405 _format: crate::output::OutputFormat,
406) -> Result<()> {
407 let env_p = parse_trd_env(env)?;
408 if matches!(env_p, TrdEnv::Real) && !confirm {
409 bail!("real-env cancel_all_order requires --confirm for safety");
410 }
411 let market_p = match market {
413 Some(m) => parse_trd_market_for_write(m)?,
414 None => {
415 bail!("--market required (HK|US|CN|HKCC); per-account all-markets cancel not wired")
416 }
417 };
418 let header = TrdHeader {
419 trd_env: env_p,
420 acc_id,
421 trd_market: market_p,
422 jp_acc_type,
423 };
424 let params = ModifyOrderParams {
425 header: header.clone(),
426 order_id: 0,
427 order_id_ex: None,
428 modify_order_op: ModifyOrderOp::Cancel,
429 qty: None,
430 price: None,
431 for_all: Some(true),
432 idempotency_key: None,
433 };
434 let (client, _push_rx) = connect_gateway(gateway, "futucli-trade-ext").await?;
435 modify_order(&client, ¶ms).await?;
436 println!(
437 "✅ cancel_all_order ok: acc_id={} env={:?} market={:?}",
438 acc_id, env_p, market_p
439 );
440 Ok(())
441}