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::{
8    order_examples, order_request_schema, order_schema_meta, validate_order_shape,
9};
10use crate::output::ResponseEnvelope;
11use crate::safety::{execute_trading_order, require_trading_approval};
12
13pub async fn run(runtime: &RuntimeConfig, command: OrdersCommands) -> Result<()> {
14    let api = runtime.build_api()?;
15
16    match command {
17        OrdersCommands::Schema => {
18            runtime.emit(ResponseEnvelope::ok(
19                "orders schema",
20                json!({
21                    "schema": order_request_schema(),
22                    "meta": order_schema_meta(),
23                    "examples": order_examples(),
24                }),
25            ));
26        }
27        OrdersCommands::Validate {
28            order,
29            account_number,
30        } => {
31            let order_json = human::parse_order_input(&order)?;
32            validate_order_shape(&order_json)?;
33
34            let equity = if let Some(hash) = account_number.as_deref() {
35                crate::portfolio::account_equity(&api, hash)
36                    .await
37                    .ok()
38                    .flatten()
39            } else {
40                None
41            };
42
43            runtime.safety.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::{
112                wait_for_order, wait_result_json, WaitCondition, WaitOptions,
113            };
114            use std::time::Duration;
115
116            let condition = WaitCondition::parse(&until)?;
117            let result = wait_for_order(
118                &api,
119                &account_number,
120                &order_id,
121                WaitOptions {
122                    condition,
123                    timeout: Duration::from_secs(timeout_seconds),
124                    interval: Duration::from_secs(interval_seconds),
125                    proceed_on_partial_fill,
126                    requested_quantity: None,
127                },
128            )
129            .await?;
130
131            let envelope = if result.met {
132                ResponseEnvelope::ok("orders wait", wait_result_json(&result))
133            } else {
134                let mut e = ResponseEnvelope::err(
135                    "orders wait",
136                    result
137                        .error
138                        .clone()
139                        .unwrap_or_else(|| "Wait condition not met".into()),
140                );
141                e.data = wait_result_json(&result);
142                e
143            };
144            runtime.emit(envelope.with_inputs(json!({
145                "account_number": account_number,
146                "order_id": order_id,
147                "until": until,
148            })));
149        }
150        OrdersCommands::Place {
151            mut account_number,
152            order,
153        } => {
154            require_trading_approval(runtime, "orders place", "Place a live brokerage order.")?;
155
156            let order_json = if runtime.is_interactive() && order.is_empty() {
157                human::read_order_json("Order JSON (file path or inline)")?
158            } else {
159                human::parse_order_input(&order)?
160            };
161
162            validate_order_shape(&order_json)?;
163
164            if runtime.dry_run {
165                let equity = crate::portfolio::account_equity(&api, &account_number)
166                    .await
167                    .ok()
168                    .flatten();
169                runtime.safety.validate_order(&order_json, None, equity)?;
170                runtime.emit(ResponseEnvelope::ok(
171                    "orders place",
172                    json!({ "dry_run": true, "order": order_json }),
173                ));
174                return Ok(());
175            }
176
177            if runtime.is_interactive() && account_number.is_empty() {
178                account_number = human::pick_account_hash(runtime, &api).await?;
179            }
180
181            let data = execute_trading_order(runtime, &api, &account_number, &order_json).await?;
182            let mut envelope = ResponseEnvelope::ok("orders place", data);
183            if runtime.safety.require_preview_before_place {
184                envelope = envelope.with_warnings(vec!["Order previewed before submission".into()]);
185            }
186            runtime.emit(envelope);
187        }
188        OrdersCommands::Preview {
189            mut account_number,
190            order,
191        } => {
192            let order_json = if runtime.is_interactive() && order.is_empty() {
193                human::read_order_json("Order JSON to preview")?
194            } else {
195                human::parse_order_input(&order)?
196            };
197
198            validate_order_shape(&order_json)?;
199
200            if runtime.is_interactive() && account_number.is_empty() {
201                account_number = human::pick_account_hash(runtime, &api).await?;
202            }
203
204            let equity = crate::portfolio::account_equity(&api, &account_number)
205                .await
206                .ok()
207                .flatten();
208            runtime.safety.validate_order(&order_json, None, equity)?;
209
210            let data = api.orders().preview(&account_number, &order_json).await?;
211            runtime.emit(
212                ResponseEnvelope::ok("orders preview", json!(data)).with_next_actions(vec![
213                    format!(
214                        "schwab orders place --account-number {account_number} --order '<json>' --trust --yes"
215                    ),
216                ]),
217            );
218        }
219        OrdersCommands::Cancel {
220            account_number,
221            order_id,
222        } => {
223            require_trading_approval(runtime, "orders cancel", "Cancel open order.")?;
224            if runtime.dry_run {
225                runtime.emit(ResponseEnvelope::ok(
226                    "orders cancel",
227                    json!({ "dry_run": true, "account_number": account_number, "order_id": order_id }),
228                ));
229                return Ok(());
230            }
231            let data = api.orders().cancel(&account_number, &order_id).await?;
232            runtime.emit(ResponseEnvelope::ok(
233                "orders cancel",
234                json!({ "status": data.status, "canceled": true }),
235            ));
236        }
237        OrdersCommands::Replace {
238            account_number,
239            order_id,
240            order,
241        } => {
242            require_trading_approval(runtime, "orders replace", "Replace an existing order.")?;
243            let order_json = human::parse_order_input(&order)?;
244            validate_order_shape(&order_json)?;
245            if runtime.dry_run {
246                let equity = crate::portfolio::account_equity(&api, &account_number)
247                    .await
248                    .ok()
249                    .flatten();
250                runtime.safety.validate_order(&order_json, None, equity)?;
251                runtime.emit(ResponseEnvelope::ok(
252                    "orders replace",
253                    json!({ "dry_run": true, "order": order_json }),
254                ));
255                return Ok(());
256            }
257            let data = api
258                .orders()
259                .replace(&account_number, &order_id, &order_json)
260                .await?;
261            runtime.emit(ResponseEnvelope::ok(
262                "orders replace",
263                json!({
264                    "status": data.status,
265                    "location": data.location,
266                }),
267            ));
268        }
269    }
270
271    Ok(())
272}