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