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}