1use anyhow::Result;
2use schwab_market_data::endpoints::chains::ChainQuery;
3use serde_json::json;
4
5use crate::cli::OptionsCommands;
6use crate::config::RuntimeConfig;
7use crate::human;
8use crate::options::{
9 build_close_order_for_group, build_order_for_strategy, ensure_option_buying_power,
10 estimate_order_margin, find_position_group, group_option_legs, list_option_positions,
11 options_schema, params_from_value, validate_account_for_strategy,
12 StrategyKind,
13};
14use crate::output::ResponseEnvelope;
15use crate::portfolio::account_equity;
16use crate::rules::AccountType;
17use crate::safety::{execute_trading_order, require_trading_approval};
18
19pub async fn run(runtime: &RuntimeConfig, command: OptionsCommands) -> Result<()> {
20 let trader = runtime.build_api()?;
21 let market = runtime.build_market_api()?;
22
23 match command {
24 OptionsCommands::Chain {
25 symbol,
26 contract_type,
27 strike_count,
28 from_date,
29 to_date,
30 } => {
31 let ct = contract_type.as_deref().unwrap_or("ALL");
32 let data = market
33 .chains()
34 .get(&ChainQuery {
35 symbol: &symbol,
36 contract_type: Some(ct),
37 strike_count,
38 include_underlying_quote: Some(true),
39 from_date: from_date.as_deref(),
40 to_date: to_date.as_deref(),
41 ..Default::default()
42 })
43 .await?;
44 runtime.emit(ResponseEnvelope::ok("options chain", json!(data)).with_inputs(json!({
45 "symbol": symbol,
46 "contract_type": ct,
47 })));
48 }
49 OptionsCommands::Positions { account_number } => {
50 let hash = account_number.as_deref();
51 let legs = list_option_positions(&trader, hash).await?;
52 let groups = group_option_legs(&legs);
53 runtime.emit(ResponseEnvelope::ok(
54 "options positions",
55 json!({ "legs": legs, "groups": groups }),
56 ));
57 }
58 OptionsCommands::Schema => {
59 runtime.emit(ResponseEnvelope::ok("options schema", options_schema()));
60 }
61 OptionsCommands::Validate {
62 strategy,
63 params,
64 account_number,
65 account_type,
66 } => {
67 let kind = StrategyKind::parse(&strategy)?;
68 let raw = human::parse_order_input(¶ms)?;
69 let normalized = params_from_value(kind, &raw)?;
70 let order = build_order_for_strategy(kind, &normalized)?;
71 let equity = if let Some(hash) = account_number.as_deref() {
72 account_equity(&trader, hash).await.ok().flatten()
73 } else {
74 None
75 };
76 runtime.safety.validate_order(&order, None, equity)?;
77 let acct_type = parse_account_type(account_type.as_deref())?;
78 validate_account_for_strategy(acct_type, kind)?;
79 let margin = estimate_order_margin(&order, kind, &normalized)?;
80 runtime.emit(ResponseEnvelope::ok(
81 "options validate",
82 json!({
83 "valid": true,
84 "strategy": kind.as_str(),
85 "estimated_margin_usd": margin,
86 "order": order,
87 }),
88 ));
89 }
90 OptionsCommands::Preview {
91 account_number,
92 strategy,
93 params,
94 } => {
95 let kind = StrategyKind::parse(&strategy)?;
96 let raw = human::parse_order_input(¶ms)?;
97 let normalized = params_from_value(kind, &raw)?;
98 let order = build_order_for_strategy(kind, &normalized)?;
99 let equity = account_equity(&trader, &account_number).await.ok().flatten();
100 runtime.safety.validate_order(&order, None, equity)?;
101 let margin = estimate_order_margin(&order, kind, &normalized)?;
102 ensure_option_buying_power(&trader, &account_number, margin).await?;
103 let preview = trader.orders().preview(&account_number, &order).await?;
104 runtime.emit(ResponseEnvelope::ok(
105 "options preview",
106 json!({ "preview": preview, "order": order, "estimated_margin_usd": margin }),
107 ));
108 }
109 OptionsCommands::Open {
110 account_number,
111 strategy,
112 params,
113 } => {
114 let kind = StrategyKind::parse(&strategy)?;
115 let raw = human::parse_order_input(¶ms)?;
116 let normalized = params_from_value(kind, &raw)?;
117 let order = build_order_for_strategy(kind, &normalized)?;
118 let margin = estimate_order_margin(&order, kind, &normalized)?;
119 require_trading_approval(
120 runtime,
121 "options open",
122 &format!("Open {} {} on {}", kind.as_str(), margin, account_number),
123 )?;
124 ensure_option_buying_power(&trader, &account_number, margin).await?;
125 let data = if runtime.dry_run {
126 json!({
127 "dry_run": true,
128 "order": order,
129 "estimated_margin_usd": margin,
130 })
131 } else {
132 execute_trading_order(runtime, &trader, &account_number, &order).await?
133 };
134 runtime.emit(ResponseEnvelope::ok("options open", data));
135 }
136 OptionsCommands::Close {
137 account_number,
138 position_id,
139 } => {
140 let legs = list_option_positions(&trader, Some(&account_number)).await?;
141 let groups = group_option_legs(&legs);
142 let group = find_position_group(&groups, &position_id)
143 .ok_or_else(|| anyhow::anyhow!("position not found: {position_id}"))?;
144 let order = build_close_order_for_group(group)?;
145 require_trading_approval(
146 runtime,
147 "options close",
148 &format!("Close position {position_id}"),
149 )?;
150 let data = if runtime.dry_run {
151 json!({ "dry_run": true, "order": order, "position": group })
152 } else {
153 execute_trading_order(runtime, &trader, &account_number, &order).await?
154 };
155 runtime.emit(ResponseEnvelope::ok("options close", data));
156 }
157 }
158 Ok(())
159}
160
161fn parse_account_type(raw: Option<&str>) -> Result<AccountType> {
162 match raw.unwrap_or("margin").to_ascii_lowercase().as_str() {
163 "margin" => Ok(AccountType::Margin),
164 "ira" => Ok(AccountType::Ira),
165 "cash" => Ok(AccountType::Cash),
166 other => anyhow::bail!("unknown account type `{other}`"),
167 }
168}