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}