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 confirm: bool,
77 pub idempotency_key: Option<String>,
78 pub stop_price: Option<f64>,
80 pub trail_type: Option<i32>,
81 pub trail_value: Option<f64>,
82 pub trail_spread: Option<f64>,
83 pub output: OutputFormat,
84}
85
86pub async fn run_place_order(input: PlaceOrderCommand<'_>) -> Result<()> {
87 let idempotency_key = resolve_auto_idempotency_key(
89 input.idempotency_key,
90 &IdempotencyParams {
91 acc_id: input.acc_id,
92 market: input.market,
93 code: input.code,
94 side: input.side,
95 qty: input.qty,
96 price: input.price,
97 order_type: input.order_type,
98 },
99 );
100 let env_p = parse_trd_env(input.env)?;
101 let market_p = parse_trd_market_for_write(input.market)?;
102 let side_p = parse_trd_side(input.side)?;
103 let order_type_p = parse_order_type(input.order_type)?;
104
105 if matches!(env_p, TrdEnv::Real) && !input.confirm {
107 bail!(
108 "real-env place_order requires --confirm for safety. \
109 Re-run with --confirm after double-checking all params. \
110 (Or use --env simulate for paper trading.)"
111 );
112 }
113
114 let placing_msg = format!(
115 "placing {} {:?} × {} @ {} {:?} (env={:?}, acc={}, market={:?}, code={})",
116 input.order_type,
117 side_p,
118 input.qty,
119 input.price.unwrap_or(0.0),
120 order_type_p,
121 env_p,
122 input.acc_id,
123 market_p,
124 input.code
125 );
126 if matches!(input.output, OutputFormat::Table) {
127 println!("{placing_msg}");
128 } else {
129 eprintln!("{placing_msg}");
130 }
131
132 let params = PlaceOrderParams {
133 header: TrdHeader {
134 trd_env: env_p,
135 acc_id: input.acc_id,
136 trd_market: market_p,
137 jp_acc_type: None,
138 },
139 trd_side: side_p,
140 order_type: order_type_p,
141 code: input.code.to_string(),
142 qty: input.qty,
143 price: input.price,
144 adjust_price: None,
145 adjust_side_and_limit: None,
146 idempotency_key,
147 aux_price: input.stop_price,
149 trail_type: input.trail_type,
150 trail_value: input.trail_value,
151 trail_spread: input.trail_spread,
152 };
153
154 let (client, _push_rx) = connect_gateway(input.gateway, "futucli-place-order")
155 .await
156 .context("connect gateway")?;
157 let result = match place_order(&client, ¶ms).await {
159 Ok(r) => r,
160 Err(e) => {
161 let wrapped = anyhow::Error::from(e).context("place_order RPC");
162 emit_trade_hint_if_known(&wrapped);
163 return Err(wrapped);
164 }
165 };
166
167 emit_trade_write_success(
168 input.output,
169 TradeWriteSuccess {
170 operation: "place_order",
171 order_id: result.order_id,
172 returned_order_id: None,
173 },
174 )?;
175 if matches!(input.output, OutputFormat::Table) {
176 println!(
177 " (use `futucli order --market {} --acc-id {} --env {}` to verify)",
178 input.market, input.acc_id, input.env
179 );
180 }
181 Ok(())
182}
183
184pub struct ModifyOrderCommand<'a> {
187 pub gateway: &'a str,
188 pub env: &'a str,
189 pub acc_id: u64,
190 pub market: &'a str,
191 pub order_id: String,
192 pub op: &'a str,
193 pub qty: Option<f64>,
194 pub price: Option<f64>,
195 pub confirm: bool,
196 pub idempotency_key: Option<String>,
197 pub output: OutputFormat,
198}
199
200pub async fn run_modify_order(input: ModifyOrderCommand<'_>) -> Result<()> {
201 let resolved_order_id = resolve_order_id_arg(&input.order_id)?;
202 let idempotency_key = resolve_auto_idempotency_key(
205 input.idempotency_key,
206 &IdempotencyParams {
207 acc_id: input.acc_id,
208 market: input.market,
209 code: "", side: input.op, qty: input.qty.unwrap_or(0.0),
212 price: input.price,
213 order_type: &resolved_order_id.idempotency_component, },
215 );
216 let env_p = parse_trd_env(input.env)?;
217 let market_p = parse_trd_market_for_write(input.market)?;
218 let op_p = parse_modify_op(input.op)?;
219
220 if matches!(env_p, TrdEnv::Real) && !input.confirm {
221 bail!("real-env modify_order requires --confirm for safety");
222 }
223
224 let params = ModifyOrderParams {
225 header: TrdHeader {
226 trd_env: env_p,
227 acc_id: input.acc_id,
228 trd_market: market_p,
229 jp_acc_type: None,
230 },
231 order_id: resolved_order_id.order_id,
232 order_id_ex: resolved_order_id.order_id_ex.clone(),
233 modify_order_op: op_p,
234 qty: input.qty,
235 price: input.price,
236 for_all: None,
237 idempotency_key,
238 };
239
240 let (client, _push_rx) = connect_gateway(input.gateway, "futucli-trade-ext").await?;
241 let ret_order_id = match modify_order(&client, ¶ms).await {
243 Ok(r) => r,
244 Err(e) => {
245 let wrapped = anyhow::Error::from(e).context("modify_order RPC");
246 emit_trade_hint_if_known(&wrapped);
247 return Err(wrapped);
248 }
249 };
250 emit_trade_write_success(
251 input.output,
252 TradeWriteSuccess {
253 operation: "modify_order",
254 order_id: if resolved_order_id.order_id != 0 {
255 resolved_order_id.order_id
256 } else {
257 ret_order_id
258 },
259 returned_order_id: Some(ret_order_id),
260 },
261 )?;
262 Ok(())
263}
264
265#[allow(clippy::too_many_arguments)]
266pub async fn run_cancel_order(
267 gateway: &str,
268 env: &str,
269 acc_id: u64,
270 market: &str,
271 order_id: String,
272 confirm: bool,
273 idempotency_key: Option<String>,
274 output: OutputFormat,
275) -> Result<()> {
276 let resolved_order_id = resolve_order_id_arg(&order_id)?;
277 let env_p = parse_trd_env(env)?;
278 let market_p = parse_trd_market_for_write(market)?;
279
280 if matches!(env_p, TrdEnv::Real) && !confirm {
281 bail!("real-env cancel_order requires --confirm for safety");
282 }
283
284 let header = TrdHeader {
285 trd_env: env_p,
286 acc_id,
287 trd_market: market_p,
288 jp_acc_type: None,
289 };
290 let (client, _push_rx) = connect_gateway(gateway, "futucli-trade-ext").await?;
291 let params = ModifyOrderParams {
292 header: header.clone(),
293 order_id: resolved_order_id.order_id,
294 order_id_ex: resolved_order_id.order_id_ex.clone(),
295 modify_order_op: ModifyOrderOp::Cancel,
296 qty: None,
297 price: None,
298 for_all: None,
299 idempotency_key,
300 };
301 let ret_order_id = match modify_order(&client, ¶ms).await {
303 Ok(id) => id,
304 Err(e) => {
305 let wrapped = anyhow::Error::from(e).context("cancel_order RPC");
306 emit_trade_hint_if_known(&wrapped);
307 return Err(wrapped);
308 }
309 };
310 emit_trade_write_success(
311 output,
312 TradeWriteSuccess {
313 operation: "cancel_order",
314 order_id: if resolved_order_id.order_id != 0 {
315 resolved_order_id.order_id
316 } else {
317 ret_order_id
318 },
319 returned_order_id: None,
320 },
321 )?;
322 Ok(())
323}
324
325#[allow(clippy::too_many_arguments)]
326pub async fn run_reconfirm_order(
327 gateway: &str,
328 env: &str,
329 acc_id: u64,
330 market: &str,
331 order_id: String,
332 reason: i32,
333 confirm: bool,
334 output: OutputFormat,
335) -> Result<()> {
336 let parsed_order_id = parse_numeric_order_id_arg(&order_id, "--order-id")?;
337 let env_p = parse_trd_env(env)?;
338 let market_p = parse_trd_market_for_write(market)?;
339
340 if matches!(env_p, TrdEnv::Real) && !confirm {
341 bail!("real-env reconfirm_order requires --confirm for safety");
342 }
343
344 let header = TrdHeader {
345 trd_env: env_p,
346 acc_id,
347 trd_market: market_p,
348 jp_acc_type: None,
349 };
350 let (client, _push_rx) = connect_gateway(gateway, "futucli-trade-ext").await?;
351 let ret_order_id = match reconfirm_order(&client, &header, parsed_order_id, reason).await {
352 Ok(id) => id,
353 Err(e) => {
354 let wrapped = anyhow::Error::from(e).context("reconfirm_order RPC");
355 emit_trade_hint_if_known(&wrapped);
356 return Err(wrapped);
357 }
358 };
359 emit_trade_write_success(
360 output,
361 TradeWriteSuccess {
362 operation: "reconfirm_order",
363 order_id: parsed_order_id,
364 returned_order_id: Some(ret_order_id),
365 },
366 )?;
367 Ok(())
368}
369
370pub async fn run_sub_acc_push(
372 gateway: &str,
373 acc_ids: &[u64],
374 _format: crate::output::OutputFormat,
375) -> Result<()> {
376 if acc_ids.is_empty() {
377 bail!("need at least one acc_id");
378 }
379 let (client, _rx) = connect_gateway(gateway, "futucli-sub-acc-push").await?;
380 futu_trd::misc::sub_acc_push(&client, acc_ids).await?;
381 println!("✅ sub_acc_push ok: {acc_ids:?}");
382 Ok(())
383}
384
385pub async fn run_cancel_all_order(
392 gateway: &str,
393 acc_id: u64,
394 env: &str,
395 market: Option<&str>,
396 confirm: bool,
397 _format: crate::output::OutputFormat,
398) -> Result<()> {
399 let env_p = parse_trd_env(env)?;
400 if matches!(env_p, TrdEnv::Real) && !confirm {
401 bail!("real-env cancel_all_order requires --confirm for safety");
402 }
403 let market_p = match market {
405 Some(m) => parse_trd_market_for_write(m)?,
406 None => {
407 bail!("--market required (HK|US|CN|HKCC); per-account all-markets cancel not wired")
408 }
409 };
410 let header = TrdHeader {
411 trd_env: env_p,
412 acc_id,
413 trd_market: market_p,
414 jp_acc_type: None,
415 };
416 let params = ModifyOrderParams {
417 header: header.clone(),
418 order_id: 0,
419 order_id_ex: None,
420 modify_order_op: ModifyOrderOp::Cancel,
421 qty: None,
422 price: None,
423 for_all: Some(true),
424 idempotency_key: None,
425 };
426 let (client, _push_rx) = connect_gateway(gateway, "futucli-trade-ext").await?;
427 modify_order(&client, ¶ms).await?;
428 println!(
429 "✅ cancel_all_order ok: acc_id={} env={:?} market={:?}",
430 acc_id, env_p, market_p
431 );
432 Ok(())
433}