Skip to main content

schwab_cli/commands/
orders.rs

1use anyhow::Result;
2use serde_json::json;
3
4use crate::cli::OrdersCommands;
5use crate::config::RuntimeConfig;
6use crate::human;
7use crate::order_schema::{order_examples, order_request_schema, order_schema_meta, validate_order_shape};
8use crate::output::ResponseEnvelope;
9use crate::safety::{execute_trading_order, require_trading_approval};
10
11pub async fn run(runtime: &RuntimeConfig, command: OrdersCommands) -> Result<()> {
12    let api = runtime.build_api()?;
13
14    match command {
15        OrdersCommands::Schema => {
16            runtime.emit(ResponseEnvelope::ok(
17                "orders schema",
18                json!({
19                    "schema": order_request_schema(),
20                    "meta": order_schema_meta(),
21                    "examples": order_examples(),
22                }),
23            ));
24        }
25        OrdersCommands::Validate {
26            order,
27            account_number,
28        } => {
29            let order_json = human::parse_order_input(&order)?;
30            validate_order_shape(&order_json)?;
31
32            let equity = if let Some(hash) = account_number.as_deref() {
33                crate::portfolio::account_equity(&api, hash)
34                    .await
35                    .ok()
36                    .flatten()
37            } else {
38                None
39            };
40
41            runtime
42                .safety
43                .validate_order(&order_json, None, equity)?;
44
45            runtime.emit(
46                ResponseEnvelope::ok("orders validate", json!({
47                    "valid": true,
48                    "order": order_json,
49                    "account_equity": equity,
50                }))
51                .with_next_actions(vec![
52                    "schwab orders preview --account-number <hash> --order '<json>' --json".into(),
53                    "schwab orders place --account-number <hash> --order '<json>' --trust --yes --json".into(),
54                ]),
55            );
56        }
57        OrdersCommands::List {
58            mut account_number,
59            from_entered_time,
60            to_entered_time,
61            status,
62            max_results,
63        } => {
64            if runtime.is_interactive() && account_number.is_empty() {
65                account_number = human::pick_account_hash(runtime, &api).await?;
66            }
67            let data = api
68                .orders()
69                .list_for_account(
70                    &account_number,
71                    from_entered_time.as_deref(),
72                    to_entered_time.as_deref(),
73                    status.as_deref(),
74                    max_results.as_deref(),
75                )
76                .await?;
77            runtime.emit(ResponseEnvelope::ok("orders list", json!(data)));
78        }
79        OrdersCommands::All {
80            from_entered_time,
81            to_entered_time,
82            status,
83            max_results,
84        } => {
85            let data = api
86                .orders()
87                .list_all(
88                    from_entered_time.as_deref(),
89                    to_entered_time.as_deref(),
90                    status.as_deref(),
91                    max_results.as_deref(),
92                )
93                .await?;
94            runtime.emit(ResponseEnvelope::ok("orders all", json!(data)));
95        }
96        OrdersCommands::Get {
97            account_number,
98            order_id,
99        } => {
100            let data = api.orders().get(&account_number, &order_id).await?;
101            runtime.emit(ResponseEnvelope::ok("orders get", json!(data)));
102        }
103        OrdersCommands::Wait {
104            account_number,
105            order_id,
106            until,
107            timeout_seconds,
108            interval_seconds,
109            proceed_on_partial_fill,
110        } => {
111            use crate::order_status::{wait_for_order, wait_result_json, WaitCondition, WaitOptions};
112            use std::time::Duration;
113
114            let condition = WaitCondition::parse(&until)?;
115            let result = wait_for_order(
116                &api,
117                &account_number,
118                &order_id,
119                WaitOptions {
120                    condition,
121                    timeout: Duration::from_secs(timeout_seconds),
122                    interval: Duration::from_secs(interval_seconds),
123                    proceed_on_partial_fill,
124                    requested_quantity: None,
125                },
126            )
127            .await?;
128
129            let envelope = if result.met {
130                ResponseEnvelope::ok("orders wait", wait_result_json(&result))
131            } else {
132                let mut e = ResponseEnvelope::err(
133                    "orders wait",
134                    result
135                        .error
136                        .clone()
137                        .unwrap_or_else(|| "Wait condition not met".into()),
138                );
139                e.data = wait_result_json(&result);
140                e
141            };
142            runtime.emit(envelope.with_inputs(json!({
143                "account_number": account_number,
144                "order_id": order_id,
145                "until": until,
146            })));
147        }
148        OrdersCommands::Place {
149            mut account_number,
150            order,
151        } => {
152            require_trading_approval(
153                runtime,
154                "orders place",
155                "Place a live brokerage order.",
156            )?;
157
158            let order_json = if runtime.is_interactive() && order.is_empty() {
159                human::read_order_json("Order JSON (file path or inline)")?
160            } else {
161                human::parse_order_input(&order)?
162            };
163
164            validate_order_shape(&order_json)?;
165
166            if runtime.dry_run {
167                let equity = crate::portfolio::account_equity(&api, &account_number)
168                    .await
169                    .ok()
170                    .flatten();
171                runtime
172                    .safety
173                    .validate_order(&order_json, None, equity)?;
174                runtime.emit(ResponseEnvelope::ok(
175                    "orders place",
176                    json!({ "dry_run": true, "order": order_json }),
177                ));
178                return Ok(());
179            }
180
181            if runtime.is_interactive() && account_number.is_empty() {
182                account_number = human::pick_account_hash(runtime, &api).await?;
183            }
184
185            let data = execute_trading_order(runtime, &api, &account_number, &order_json).await?;
186            let mut envelope = ResponseEnvelope::ok("orders place", data);
187            if runtime.safety.require_preview_before_place {
188                envelope = envelope.with_warnings(vec!["Order previewed before submission".into()]);
189            }
190            runtime.emit(envelope);
191        }
192        OrdersCommands::Preview {
193            mut account_number,
194            order,
195        } => {
196            let order_json = if runtime.is_interactive() && order.is_empty() {
197                human::read_order_json("Order JSON to preview")?
198            } else {
199                human::parse_order_input(&order)?
200            };
201
202            validate_order_shape(&order_json)?;
203
204            if runtime.is_interactive() && account_number.is_empty() {
205                account_number = human::pick_account_hash(runtime, &api).await?;
206            }
207
208            let equity = crate::portfolio::account_equity(&api, &account_number)
209                .await
210                .ok()
211                .flatten();
212            runtime
213                .safety
214                .validate_order(&order_json, None, equity)?;
215
216            let data = api.orders().preview(&account_number, &order_json).await?;
217            runtime.emit(
218                ResponseEnvelope::ok("orders preview", json!(data)).with_next_actions(vec![
219                    format!(
220                        "schwab orders place --account-number {account_number} --order '<json>' --trust --yes"
221                    ),
222                ]),
223            );
224        }
225        OrdersCommands::Cancel {
226            account_number,
227            order_id,
228        } => {
229            require_trading_approval(runtime, "orders cancel", "Cancel open order.")?;
230            if runtime.dry_run {
231                runtime.emit(ResponseEnvelope::ok(
232                    "orders cancel",
233                    json!({ "dry_run": true, "account_number": account_number, "order_id": order_id }),
234                ));
235                return Ok(());
236            }
237            let data = api.orders().cancel(&account_number, &order_id).await?;
238            runtime.emit(ResponseEnvelope::ok(
239                "orders cancel",
240                json!({ "status": data.status, "canceled": true }),
241            ));
242        }
243        OrdersCommands::Replace {
244            account_number,
245            order_id,
246            order,
247        } => {
248            require_trading_approval(runtime, "orders replace", "Replace an existing order.")?;
249            let order_json = human::parse_order_input(&order)?;
250            validate_order_shape(&order_json)?;
251            if runtime.dry_run {
252                let equity = crate::portfolio::account_equity(&api, &account_number)
253                    .await
254                    .ok()
255                    .flatten();
256                runtime
257                    .safety
258                    .validate_order(&order_json, None, equity)?;
259                runtime.emit(ResponseEnvelope::ok(
260                    "orders replace",
261                    json!({ "dry_run": true, "order": order_json }),
262                ));
263                return Ok(());
264            }
265            let data = api
266                .orders()
267                .replace(&account_number, &order_id, &order_json)
268                .await?;
269            runtime.emit(ResponseEnvelope::ok(
270                "orders replace",
271                json!({
272                    "status": data.status,
273                    "location": data.location,
274                }),
275            ));
276        }
277    }
278
279    Ok(())
280}