Skip to main content

schwab_cli/
cli.rs

1use std::path::PathBuf;
2
3use clap::{Parser, Subcommand};
4
5use crate::disclaimer::HELP_DISCLAIMER;
6use crate::mode::CliMode;
7use crate::output::OutputFormat;
8
9#[derive(Debug, Parser)]
10#[command(
11    name = "schwab",
12    version,
13    about = "Agent-first CLI for Charles Schwab Trader API (experimental — use at your own risk)",
14    long_about = "Schwab Trader API CLI (Accounts and Trading Production).\n\n\
15        ⚠️  EXPERIMENTAL — USE AT YOUR OWN RISK. Can submit real trades.\n\
16        Run `schwab disclaimer show` and `schwab disclaimer accept --yes` before live trading.\n\n\
17        AGENTS: Prefer --mode agent (default). Discover commands with:\n\
18          schwab --help --json\n\
19          schwab capabilities --json\n\
20          schwab env schema --json\n\
21          schwab instructions --json\n\n\
22        HUMANS: Use --mode human for guided prompts when arguments are omitted.",
23    after_help = HELP_DISCLAIMER
24)]
25pub struct Cli {
26    /// Operating mode: agent (structured, default) or human (interactive prompts)
27    #[arg(long, env = "SCHWAB_MODE", default_value = "agent")]
28    pub mode: CliMode,
29
30    /// Output format
31    #[arg(long, env = "SCHWAB_OUTPUT", default_value = "pretty")]
32    pub output: OutputFormat,
33
34    /// Shorthand for --output json
35    #[arg(long, short = 'j', global = true)]
36    pub json: bool,
37
38    /// Shorthand for --output md
39    #[arg(long, global = true)]
40    pub md: bool,
41
42    /// Auto-confirm mutations (required in non-interactive agent mode)
43    #[arg(long, global = true)]
44    pub yes: bool,
45
46    /// Validate mutation without executing
47    #[arg(long, global = true)]
48    pub dry_run: bool,
49
50    /// Trusted agent mode: allow autonomous trading with --trust --yes (safety.json limits still enforced)
51    #[arg(long, global = true)]
52    pub trust: bool,
53
54    /// Emit full command tree as JSON (agent discovery)
55    #[arg(long, global = true)]
56    pub help_json: bool,
57
58    #[command(subcommand)]
59    pub command: Option<Commands>,
60}
61
62#[derive(Debug, Subcommand)]
63pub enum Commands {
64    /// Machine-readable command catalog for agents
65    Capabilities,
66
67    /// Environment variable schema and precedence
68    Env {
69        #[command(subcommand)]
70        command: EnvCommands,
71    },
72
73    /// Agent system prompt / tool-use instructions
74    Instructions,
75
76    /// Trading risk disclaimer (required before live trades)
77    Disclaimer {
78        #[command(subcommand)]
79        command: DisclaimerCommands,
80    },
81
82    /// OAuth authentication and token management
83    Auth {
84        #[command(subcommand)]
85        command: AuthCommands,
86    },
87
88    /// Account numbers, balances, and positions
89    Accounts {
90        #[command(subcommand)]
91        command: AccountsCommands,
92    },
93
94    /// Order entry, preview, cancel, replace
95    Orders {
96        #[command(subcommand)]
97        command: OrdersCommands,
98    },
99
100    /// Transaction history
101    Transactions {
102        #[command(subcommand)]
103        command: TransactionsCommands,
104    },
105
106    /// User preferences and streamer metadata
107    User {
108        #[command(subcommand)]
109        command: UserCommands,
110    },
111
112    /// Portfolio summary across linked accounts
113    Portfolio {
114        #[command(subcommand)]
115        command: PortfolioCommands,
116    },
117
118    /// Buy or sell equities with safety guardrails
119    Trade {
120        #[command(subcommand)]
121        command: TradeCommands,
122    },
123
124    /// Safety limits config (safety.json) for agent trading guardrails
125    Safety {
126        #[command(subcommand)]
127        command: SafetyCommands,
128    },
129
130    /// Multi-step trade plans (YAML/JSON) for LLM-generated rebalances
131    Plan {
132        #[command(subcommand)]
133        command: PlanCommands,
134    },
135
136    /// Market Data API — quotes, history, instruments, hours
137    Market {
138        #[command(subcommand)]
139        command: MarketCommands,
140    },
141
142    /// Options chain, positions, and strategy orders (vertical, iron condor)
143    Options {
144        #[command(subcommand)]
145        command: OptionsCommands,
146    },
147
148    /// Long-running options agent driven by rules.yaml
149    Agent {
150        #[command(subcommand)]
151        command: AgentCommands,
152    },
153}
154
155#[derive(Debug, Subcommand)]
156pub enum EnvCommands {
157    /// JSON schema of supported environment variables
158    Schema,
159}
160
161#[derive(Debug, Subcommand)]
162pub enum AuthCommands {
163    /// Start OAuth login (opens browser, captures redirect code)
164    Login {
165        /// Authorization code if already obtained (skips browser)
166        #[arg(long)]
167        code: Option<String>,
168    },
169    /// Show token status
170    Status,
171    /// Refresh access token using refresh token
172    Refresh,
173    /// Remove stored tokens
174    Logout,
175}
176
177#[derive(Debug, Subcommand)]
178pub enum AccountsCommands {
179    /// GET /accounts/accountNumbers
180    Numbers,
181    /// GET /accounts
182    List {
183        #[arg(long)]
184        fields: Option<String>,
185    },
186    /// GET /accounts/{accountNumber}
187    Get {
188        account_number: String,
189        #[arg(long)]
190        fields: Option<String>,
191    },
192}
193
194#[derive(Debug, Subcommand)]
195pub enum OrdersCommands {
196    /// JSON Schema + Schwab order examples for agents
197    Schema,
198    /// Validate order JSON (shape + safety.json limits)
199    Validate {
200        /// Path to order JSON file or inline JSON string
201        #[arg(long)]
202        order: String,
203        /// Account hash for equity % checks (optional)
204        #[arg(long)]
205        account_number: Option<String>,
206    },
207    /// GET /accounts/{accountNumber}/orders
208    List {
209        account_number: String,
210        #[arg(long)]
211        from_entered_time: Option<String>,
212        #[arg(long)]
213        to_entered_time: Option<String>,
214        #[arg(long)]
215        status: Option<String>,
216        #[arg(long)]
217        max_results: Option<String>,
218    },
219    /// GET /orders (all linked accounts)
220    All {
221        #[arg(long)]
222        from_entered_time: Option<String>,
223        #[arg(long)]
224        to_entered_time: Option<String>,
225        #[arg(long)]
226        status: Option<String>,
227        #[arg(long)]
228        max_results: Option<String>,
229    },
230    /// GET /accounts/{accountNumber}/orders/{orderId}
231    Get {
232        account_number: String,
233        order_id: String,
234    },
235    /// Poll order status until filled, terminal, or timeout
236    Wait {
237        account_number: String,
238        order_id: String,
239        /// Wait condition: accepted | filled | terminal
240        #[arg(long, default_value = "filled")]
241        until: String,
242        /// Max seconds to poll before giving up
243        #[arg(long, default_value = "3600")]
244        timeout_seconds: u64,
245        /// Seconds between status polls
246        #[arg(long, default_value = "5")]
247        interval_seconds: u64,
248        /// Treat partial fill as success when waiting for filled
249        #[arg(long, default_value = "false")]
250        proceed_on_partial_fill: bool,
251    },
252    /// POST /accounts/{accountNumber}/orders
253    Place {
254        account_number: String,
255        /// Path to order JSON file or inline JSON string
256        #[arg(long)]
257        order: String,
258    },
259    /// POST /accounts/{accountNumber}/previewOrder
260    Preview {
261        account_number: String,
262        #[arg(long)]
263        order: String,
264    },
265    /// DELETE /accounts/{accountNumber}/orders/{orderId}
266    Cancel {
267        account_number: String,
268        order_id: String,
269    },
270    /// PUT /accounts/{accountNumber}/orders/{orderId}
271    Replace {
272        account_number: String,
273        order_id: String,
274        #[arg(long)]
275        order: String,
276    },
277}
278
279#[derive(Debug, Subcommand)]
280pub enum TransactionsCommands {
281    /// GET /accounts/{accountNumber}/transactions
282    List {
283        account_number: String,
284        #[arg(long)]
285        start_date: Option<String>,
286        #[arg(long)]
287        end_date: Option<String>,
288        #[arg(long)]
289        types: Option<String>,
290        #[arg(long)]
291        symbol: Option<String>,
292    },
293    /// GET /accounts/{accountNumber}/transactions/{transactionId}
294    Get {
295        account_number: String,
296        transaction_id: String,
297    },
298}
299
300#[derive(Debug, Subcommand)]
301pub enum UserCommands {
302    /// GET /userPreference
303    Preference,
304}
305
306#[derive(Debug, Subcommand)]
307pub enum PortfolioCommands {
308    /// Aggregate positions and equity across all linked accounts
309    Summary,
310    /// Cash available for trading on one account (required before buys)
311    BuyingPower {
312        /// Account hash from `schwab accounts numbers`
313        #[arg(long)]
314        account_number: String,
315    },
316}
317
318#[derive(Debug, Subcommand)]
319pub enum TradeCommands {
320    /// Buy shares (equity, single-leg)
321    Buy {
322        /// Account hash from `schwab accounts numbers`
323        #[arg(long)]
324        account_number: String,
325        /// Ticker symbol
326        #[arg(long)]
327        symbol: String,
328        /// Share quantity
329        #[arg(long)]
330        quantity: f64,
331        /// Order type: market | limit
332        #[arg(long, default_value = "market")]
333        order_type: String,
334        /// Limit price (required for limit orders)
335        #[arg(long)]
336        price: Option<f64>,
337        /// Duration: day | gtc | fok
338        #[arg(long)]
339        duration: Option<String>,
340        /// Session: normal | am | pm | seamless
341        #[arg(long)]
342        session: Option<String>,
343    },
344    /// Sell shares (equity, single-leg)
345    Sell {
346        #[arg(long)]
347        account_number: String,
348        #[arg(long)]
349        symbol: String,
350        #[arg(long)]
351        quantity: f64,
352        #[arg(long, default_value = "market")]
353        order_type: String,
354        #[arg(long)]
355        price: Option<f64>,
356        #[arg(long)]
357        duration: Option<String>,
358        #[arg(long)]
359        session: Option<String>,
360    },
361}
362
363#[derive(Debug, Subcommand)]
364pub enum SafetyCommands {
365    /// Show active safety.json limits and config path
366    Show,
367    /// Write default safety.json to the config directory
368    Init,
369    /// Print safety.json path only
370    Path,
371}
372
373#[derive(Debug, Subcommand)]
374pub enum DisclaimerCommands {
375    /// Print the full trading risk disclaimer
376    Show,
377    /// Record acceptance (required before live trading; use --yes in agent mode)
378    Accept,
379    /// Show whether disclaimer has been accepted on this machine
380    Status,
381}
382
383#[derive(Debug, Subcommand)]
384pub enum PlanCommands {
385    /// JSON Schema for trade plan files
386    Schema,
387    /// LLM prompt and workflow for generating trade plans
388    Prompt,
389    /// Validate plan structure and safety limits
390    Validate {
391        /// Path to .yaml, .yml, or .json trade plan
392        file: PathBuf,
393    },
394    /// Show parsed plan contents
395    Show {
396        file: PathBuf,
397    },
398    /// Execute plan steps (requires --trust --yes in agent mode, or --dry-run)
399    Run {
400        file: PathBuf,
401        /// Run only this step id
402        #[arg(long)]
403        step: Option<String>,
404        /// Run from this step id through the end
405        #[arg(long)]
406        from_step: Option<String>,
407    },
408}
409
410#[derive(Debug, Subcommand)]
411pub enum MarketCommands {
412    /// Agent dossier — quote + fundamentals + price context + research hints
413    Info {
414        /// One symbol or comma-separated list (e.g. SGOV or SGOV,JPST,AAPL)
415        symbol: String,
416        /// Skip price history fetch
417        #[arg(long)]
418        no_history: bool,
419        #[arg(long, default_value = "month")]
420        history_period_type: String,
421        #[arg(long, default_value_t = 1)]
422        history_period: u32,
423        #[arg(long, default_value = "daily")]
424        history_frequency_type: String,
425    },
426    /// GET /quotes — quotes for multiple symbols (comma-separated)
427    Quotes {
428        /// Comma-separated tickers (e.g. SGOV,JPST,AAPL)
429        #[arg(long)]
430        symbols: String,
431        /// Quote fields: all, quote, fundamental, reference, extended, regular
432        #[arg(long)]
433        fields: Option<String>,
434        #[arg(long)]
435        indicative: Option<bool>,
436    },
437    /// GET /{symbol}/quotes — single symbol quote
438    Quote {
439        symbol: String,
440        #[arg(long)]
441        fields: Option<String>,
442        #[arg(long)]
443        indicative: Option<bool>,
444    },
445    /// GET /pricehistory — OHLCV candles
446    History {
447        symbol: String,
448        #[arg(long)]
449        period_type: Option<String>,
450        #[arg(long)]
451        period: Option<u32>,
452        #[arg(long)]
453        frequency_type: Option<String>,
454        #[arg(long)]
455        frequency: Option<u32>,
456        /// Epoch milliseconds
457        #[arg(long)]
458        start_date: Option<i64>,
459        #[arg(long)]
460        end_date: Option<i64>,
461        #[arg(long)]
462        need_extended_hours_data: Option<bool>,
463        #[arg(long)]
464        need_previous_close: Option<bool>,
465    },
466    /// GET /instruments — symbol search / fundamentals (company info)
467    Instrument {
468        /// Symbol or search text
469        #[arg(long)]
470        symbol: String,
471        /// Projection: symbol-search, fundamental, search, etc.
472        #[arg(long, default_value = "fundamental")]
473        projection: String,
474    },
475    /// GET /instruments/{cusip}
476    InstrumentByCusip {
477        cusip: String,
478    },
479    /// GET /markets — hours for multiple markets (comma-separated)
480    Hours {
481        /// equity, option, bond, future, forex (comma-separated)
482        #[arg(long, default_value = "equity")]
483        markets: String,
484        /// YYYY-MM-DD (defaults to today)
485        #[arg(long)]
486        date: Option<String>,
487    },
488    /// GET /markets/{market_id} — hours for one market
489    HoursFor {
490        /// equity | option | bond | future | forex
491        market: String,
492        #[arg(long)]
493        date: Option<String>,
494    },
495}
496
497#[derive(Debug, Subcommand)]
498pub enum OptionsCommands {
499    /// GET /chains — option chain for an underlying
500    Chain {
501        #[arg(long)]
502        symbol: String,
503        /// CALL | PUT | ALL
504        #[arg(long, name = "type")]
505        contract_type: Option<String>,
506        #[arg(long)]
507        strike_count: Option<u32>,
508        #[arg(long)]
509        from_date: Option<String>,
510        #[arg(long)]
511        to_date: Option<String>,
512    },
513    /// List option positions (grouped into spreads where possible)
514    Positions {
515        #[arg(long)]
516        account_number: Option<String>,
517    },
518    /// Strategy templates and symbology for agents
519    Schema,
520    /// Validate strategy params + safety.json
521    Validate {
522        #[arg(long)]
523        strategy: String,
524        /// JSON string or path to .json/.yaml params file
525        #[arg(long)]
526        params: String,
527        #[arg(long)]
528        account_number: Option<String>,
529        #[arg(long, default_value = "margin")]
530        account_type: Option<String>,
531    },
532    /// Preview strategy order at Schwab
533    Preview {
534        #[arg(long)]
535        account_number: String,
536        #[arg(long)]
537        strategy: String,
538        #[arg(long)]
539        params: String,
540    },
541    /// Open a new options position (vertical or iron_condor)
542    Open {
543        #[arg(long)]
544        account_number: String,
545        #[arg(long)]
546        strategy: String,
547        #[arg(long)]
548        params: String,
549    },
550    /// Close an open spread by position group id
551    Close {
552        #[arg(long)]
553        account_number: String,
554        #[arg(long)]
555        position_id: String,
556    },
557}
558
559#[derive(Debug, Subcommand)]
560pub enum AgentCommands {
561    /// JSON Schema for rules.yaml
562    Schema,
563    /// Validate rules.yaml structure
564    Validate {
565        file: PathBuf,
566    },
567    /// Show persisted agent state
568    Status {
569        #[arg(long)]
570        rules_file: Option<PathBuf>,
571    },
572    /// Run the options agent loop (requires --trust --yes for live trades)
573    Run {
574        file: PathBuf,
575        /// Execute a single tick then exit
576        #[arg(long)]
577        once: bool,
578        /// Detach as a background daemon (writes agent.pid and agent.log next to rules)
579        #[arg(long)]
580        background: bool,
581    },
582    /// Stop a background agent started with `agent run --background`
583    Stop {
584        file: PathBuf,
585    },
586}
587
588/// Resolved output path for clap parse result.
589impl Cli {
590    pub fn effective_output(&self) -> OutputFormat {
591        if self.json {
592            OutputFormat::Json
593        } else if self.md {
594            OutputFormat::Md
595        } else {
596            self.output
597        }
598    }
599}