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