Skip to main content

schwab_cli/commands/
options.rs

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(&params)?;
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(&params)?;
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(&params)?;
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}