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    /// Paper trading: virtual fills from live chain marks (no broker orders; separate state file)
51    #[arg(long, global = true)]
52    pub simulate: bool,
53
54    /// Trusted agent mode: allow autonomous trading with --trust --yes (safety.json limits still enforced)
55    #[arg(long, global = true)]
56    pub trust: bool,
57
58    /// Disable spoken trade-event audio cues (agent/watch)
59    #[arg(long, global = true, env = "SCHWAB_NO_AUDIO")]
60    pub no_audio: bool,
61
62    /// Emit full command tree as JSON (agent discovery)
63    #[arg(long, global = true)]
64    pub help_json: bool,
65
66    #[command(subcommand)]
67    pub command: Option<Commands>,
68}
69
70#[derive(Debug, Subcommand)]
71pub enum Commands {
72    /// Machine-readable command catalog for agents
73    Capabilities,
74
75    /// Environment variable schema and precedence
76    Env {
77        #[command(subcommand)]
78        command: EnvCommands,
79    },
80
81    /// Agent system prompt / tool-use instructions
82    Instructions,
83
84    /// Trading risk disclaimer (required before live trades)
85    Disclaimer {
86        #[command(subcommand)]
87        command: DisclaimerCommands,
88    },
89
90    /// OAuth authentication and token management
91    Auth {
92        #[command(subcommand)]
93        command: AuthCommands,
94    },
95
96    /// Account numbers, balances, and positions
97    Accounts {
98        #[command(subcommand)]
99        command: AccountsCommands,
100    },
101
102    /// Order entry, preview, cancel, replace
103    Orders {
104        #[command(subcommand)]
105        command: OrdersCommands,
106    },
107
108    /// Transaction history
109    Transactions {
110        #[command(subcommand)]
111        command: TransactionsCommands,
112    },
113
114    /// User preferences and streamer metadata
115    User {
116        #[command(subcommand)]
117        command: UserCommands,
118    },
119
120    /// Portfolio summary across linked accounts
121    Portfolio {
122        #[command(subcommand)]
123        command: PortfolioCommands,
124    },
125
126    /// Buy or sell equities with safety guardrails
127    Trade {
128        #[command(subcommand)]
129        command: TradeCommands,
130    },
131
132    /// Safety limits config (safety.json) for agent trading guardrails
133    Safety {
134        #[command(subcommand)]
135        command: SafetyCommands,
136    },
137
138    /// Multi-step trade plans (YAML/JSON) for LLM-generated rebalances
139    Plan {
140        #[command(subcommand)]
141        command: PlanCommands,
142    },
143
144    /// Market Data API — quotes, history, instruments, hours
145    Market {
146        #[command(subcommand)]
147        command: MarketCommands,
148    },
149
150    /// Options chain, positions, and strategy orders (vertical, iron condor)
151    Options {
152        #[command(subcommand)]
153        command: OptionsCommands,
154    },
155
156    /// Long-running options agent driven by rules.yaml
157    Agent {
158        #[command(subcommand)]
159        command: AgentCommands,
160    },
161
162    /// Rich terminal dashboard (agent status, rules summary, positions)
163    Dashboard {
164        /// Path to rules.yaml (or set SCHWAB_RULES)
165        file: Option<PathBuf>,
166    },
167
168    /// Live TUI watch — runs the agent in-process (or attach to an existing daemon)
169    Watch {
170        /// Path to rules.yaml (or set SCHWAB_RULES)
171        file: Option<PathBuf>,
172        /// Monitor only — do not start the agent loop (attach to existing daemon or view state)
173        #[arg(long)]
174        monitor_only: bool,
175    },
176
177    /// Human-readable rules configuration
178    Rules {
179        #[command(subcommand)]
180        command: RulesCommands,
181    },
182}
183
184#[derive(Debug, Subcommand)]
185pub enum EnvCommands {
186    /// JSON schema of supported environment variables
187    Schema,
188}
189
190#[derive(Debug, Subcommand)]
191pub enum AuthCommands {
192    /// Start OAuth login (opens browser, captures redirect code)
193    Login {
194        /// Authorization code if already obtained (skips browser)
195        #[arg(long)]
196        code: Option<String>,
197    },
198    /// Show token status
199    Status,
200    /// Refresh access token using refresh token
201    Refresh,
202    /// Remove stored tokens
203    Logout,
204}
205
206#[derive(Debug, Subcommand)]
207pub enum AccountsCommands {
208    /// GET /accounts/accountNumbers
209    Numbers,
210    /// GET /accounts
211    List {
212        #[arg(long)]
213        fields: Option<String>,
214    },
215    /// GET /accounts/{accountNumber}
216    Get {
217        account_number: String,
218        #[arg(long)]
219        fields: Option<String>,
220    },
221}
222
223#[derive(Debug, Subcommand)]
224pub enum OrdersCommands {
225    /// JSON Schema + Schwab order examples for agents
226    Schema,
227    /// Validate order JSON (shape + safety.json limits)
228    Validate {
229        /// Path to order JSON file or inline JSON string
230        #[arg(long)]
231        order: String,
232        /// Account hash for equity % checks (optional)
233        #[arg(long)]
234        account_number: Option<String>,
235    },
236    /// GET /accounts/{accountNumber}/orders
237    List {
238        account_number: String,
239        #[arg(long)]
240        from_entered_time: Option<String>,
241        #[arg(long)]
242        to_entered_time: Option<String>,
243        #[arg(long)]
244        status: Option<String>,
245        #[arg(long)]
246        max_results: Option<String>,
247    },
248    /// GET /orders (all linked accounts)
249    All {
250        #[arg(long)]
251        from_entered_time: Option<String>,
252        #[arg(long)]
253        to_entered_time: Option<String>,
254        #[arg(long)]
255        status: Option<String>,
256        #[arg(long)]
257        max_results: Option<String>,
258    },
259    /// GET /accounts/{accountNumber}/orders/{orderId}
260    Get {
261        account_number: String,
262        order_id: String,
263    },
264    /// Poll order status until filled, terminal, or timeout
265    Wait {
266        account_number: String,
267        order_id: String,
268        /// Wait condition: accepted | filled | terminal
269        #[arg(long, default_value = "filled")]
270        until: String,
271        /// Max seconds to poll before giving up
272        #[arg(long, default_value = "3600")]
273        timeout_seconds: u64,
274        /// Seconds between status polls
275        #[arg(long, default_value = "5")]
276        interval_seconds: u64,
277        /// Treat partial fill as success when waiting for filled
278        #[arg(long, default_value = "false")]
279        proceed_on_partial_fill: bool,
280    },
281    /// POST /accounts/{accountNumber}/orders
282    Place {
283        account_number: String,
284        /// Path to order JSON file or inline JSON string
285        #[arg(long)]
286        order: String,
287    },
288    /// POST /accounts/{accountNumber}/previewOrder
289    Preview {
290        account_number: String,
291        #[arg(long)]
292        order: String,
293    },
294    /// DELETE /accounts/{accountNumber}/orders/{orderId}
295    Cancel {
296        account_number: String,
297        order_id: String,
298    },
299    /// PUT /accounts/{accountNumber}/orders/{orderId}
300    Replace {
301        account_number: String,
302        order_id: String,
303        #[arg(long)]
304        order: String,
305    },
306}
307
308#[derive(Debug, Subcommand)]
309pub enum TransactionsCommands {
310    /// GET /accounts/{accountNumber}/transactions
311    List {
312        account_number: String,
313        #[arg(long)]
314        start_date: Option<String>,
315        #[arg(long)]
316        end_date: Option<String>,
317        #[arg(long)]
318        types: Option<String>,
319        #[arg(long)]
320        symbol: Option<String>,
321    },
322    /// GET /accounts/{accountNumber}/transactions/{transactionId}
323    Get {
324        account_number: String,
325        transaction_id: String,
326    },
327}
328
329#[derive(Debug, Subcommand)]
330pub enum UserCommands {
331    /// GET /userPreference
332    Preference,
333}
334
335#[derive(Debug, Subcommand)]
336pub enum PortfolioCommands {
337    /// Aggregate positions and equity across all linked accounts
338    Summary,
339    /// Cash available for trading on one account (required before buys)
340    BuyingPower {
341        /// Account hash from `schwab accounts numbers`
342        #[arg(long)]
343        account_number: String,
344    },
345}
346
347#[derive(Debug, Subcommand)]
348pub enum TradeCommands {
349    /// Buy shares (equity, single-leg)
350    Buy {
351        /// Account hash from `schwab accounts numbers`
352        #[arg(long)]
353        account_number: String,
354        /// Ticker symbol
355        #[arg(long)]
356        symbol: String,
357        /// Share quantity
358        #[arg(long)]
359        quantity: f64,
360        /// Order type: market | limit
361        #[arg(long, default_value = "market")]
362        order_type: String,
363        /// Limit price (required for limit orders)
364        #[arg(long)]
365        price: Option<f64>,
366        /// Duration: day | gtc | fok
367        #[arg(long)]
368        duration: Option<String>,
369        /// Session: normal | am | pm | seamless
370        #[arg(long)]
371        session: Option<String>,
372    },
373    /// Sell shares (equity, single-leg)
374    Sell {
375        #[arg(long)]
376        account_number: String,
377        #[arg(long)]
378        symbol: String,
379        #[arg(long)]
380        quantity: f64,
381        #[arg(long, default_value = "market")]
382        order_type: String,
383        #[arg(long)]
384        price: Option<f64>,
385        #[arg(long)]
386        duration: Option<String>,
387        #[arg(long)]
388        session: Option<String>,
389    },
390}
391
392#[derive(Debug, Subcommand)]
393pub enum SafetyCommands {
394    /// Show active safety.json limits and config path
395    Show,
396    /// Write default safety.json to the config directory
397    Init,
398    /// Print safety.json path only
399    Path,
400}
401
402#[derive(Debug, Subcommand)]
403pub enum DisclaimerCommands {
404    /// Print the full trading risk disclaimer
405    Show,
406    /// Record acceptance (required before live trading; use --yes in agent mode)
407    Accept,
408    /// Show whether disclaimer has been accepted on this machine
409    Status,
410}
411
412#[derive(Debug, Subcommand)]
413pub enum PlanCommands {
414    /// JSON Schema for trade plan files
415    Schema,
416    /// LLM prompt and workflow for generating trade plans
417    Prompt,
418    /// Validate plan structure and safety limits
419    Validate {
420        /// Path to .yaml, .yml, or .json trade plan
421        file: PathBuf,
422    },
423    /// Show parsed plan contents
424    Show { file: PathBuf },
425    /// Execute plan steps (requires --trust --yes in agent mode, or --dry-run)
426    Run {
427        file: PathBuf,
428        /// Run only this step id
429        #[arg(long)]
430        step: Option<String>,
431        /// Run from this step id through the end
432        #[arg(long)]
433        from_step: Option<String>,
434    },
435}
436
437#[derive(Debug, Subcommand)]
438pub enum MarketCommands {
439    /// Agent dossier — quote + fundamentals + price context + research hints
440    Info {
441        /// One symbol or comma-separated list (e.g. SGOV or SGOV,JPST,AAPL)
442        symbol: String,
443        /// Skip price history fetch
444        #[arg(long)]
445        no_history: bool,
446        #[arg(long, default_value = "month")]
447        history_period_type: String,
448        #[arg(long, default_value_t = 1)]
449        history_period: u32,
450        #[arg(long, default_value = "daily")]
451        history_frequency_type: String,
452    },
453    /// GET /quotes — quotes for multiple symbols (comma-separated)
454    Quotes {
455        /// Comma-separated tickers (e.g. SGOV,JPST,AAPL)
456        #[arg(long)]
457        symbols: String,
458        /// Quote fields: all, quote, fundamental, reference, extended, regular
459        #[arg(long)]
460        fields: Option<String>,
461        #[arg(long)]
462        indicative: Option<bool>,
463    },
464    /// GET /{symbol}/quotes — single symbol quote
465    Quote {
466        symbol: String,
467        #[arg(long)]
468        fields: Option<String>,
469        #[arg(long)]
470        indicative: Option<bool>,
471    },
472    /// GET /pricehistory — OHLCV candles
473    History {
474        symbol: String,
475        #[arg(long)]
476        period_type: Option<String>,
477        #[arg(long)]
478        period: Option<u32>,
479        #[arg(long)]
480        frequency_type: Option<String>,
481        #[arg(long)]
482        frequency: Option<u32>,
483        /// Epoch milliseconds
484        #[arg(long)]
485        start_date: Option<i64>,
486        #[arg(long)]
487        end_date: Option<i64>,
488        #[arg(long)]
489        need_extended_hours_data: Option<bool>,
490        #[arg(long)]
491        need_previous_close: Option<bool>,
492    },
493    /// GET /instruments — symbol search / fundamentals (company info)
494    Instrument {
495        /// Symbol or search text
496        #[arg(long)]
497        symbol: String,
498        /// Projection: symbol-search, fundamental, search, etc.
499        #[arg(long, default_value = "fundamental")]
500        projection: String,
501    },
502    /// GET /instruments/{cusip}
503    InstrumentByCusip { cusip: String },
504    /// GET /markets — hours for multiple markets (comma-separated)
505    Hours {
506        /// equity, option, bond, future, forex (comma-separated)
507        #[arg(long, default_value = "equity")]
508        markets: String,
509        /// YYYY-MM-DD (defaults to today)
510        #[arg(long)]
511        date: Option<String>,
512    },
513    /// GET /markets/{market_id} — hours for one market
514    HoursFor {
515        /// equity | option | bond | future | forex
516        market: String,
517        #[arg(long)]
518        date: Option<String>,
519    },
520}
521
522#[derive(Debug, Subcommand)]
523pub enum OptionsCommands {
524    /// GET /chains — option chain for an underlying
525    Chain {
526        #[arg(long)]
527        symbol: String,
528        /// CALL | PUT | ALL
529        #[arg(long, name = "type")]
530        contract_type: Option<String>,
531        #[arg(long)]
532        strike_count: Option<u32>,
533        #[arg(long)]
534        from_date: Option<String>,
535        #[arg(long)]
536        to_date: Option<String>,
537    },
538    /// List option positions (grouped into spreads where possible)
539    Positions {
540        #[arg(long)]
541        account_number: Option<String>,
542    },
543    /// Strategy templates and symbology for agents
544    Schema,
545    /// Validate strategy params + safety.json
546    Validate {
547        #[arg(long)]
548        strategy: String,
549        /// JSON string or path to .json/.yaml params file
550        #[arg(long)]
551        params: String,
552        #[arg(long)]
553        account_number: Option<String>,
554        #[arg(long, default_value = "margin")]
555        account_type: Option<String>,
556    },
557    /// Preview strategy order at Schwab
558    Preview {
559        #[arg(long)]
560        account_number: String,
561        #[arg(long)]
562        strategy: String,
563        #[arg(long)]
564        params: String,
565    },
566    /// Open a new options position (vertical or iron_condor)
567    Open {
568        #[arg(long)]
569        account_number: String,
570        #[arg(long)]
571        strategy: String,
572        #[arg(long)]
573        params: String,
574    },
575    /// Close an open spread by position group id
576    Close {
577        #[arg(long)]
578        account_number: String,
579        #[arg(long)]
580        position_id: String,
581    },
582}
583
584#[derive(Debug, Subcommand)]
585pub enum AgentCommands {
586    /// JSON Schema for rules.yaml
587    Schema,
588    /// Validate rules.yaml structure
589    Validate { file: PathBuf },
590    /// Show persisted agent state
591    Status {
592        #[arg(long)]
593        rules_file: Option<PathBuf>,
594    },
595    /// Run the options agent loop (requires --trust --yes for live trades)
596    Run {
597        file: PathBuf,
598        /// Execute a single tick then exit
599        #[arg(long)]
600        once: bool,
601        /// Detach as a background daemon (writes agent.pid and agent.log next to rules)
602        #[arg(long)]
603        background: bool,
604    },
605    /// Stop a background agent started with `agent run --background`
606    Stop { file: PathBuf },
607    /// Close all spreads tracked in agent state for this rules file (NOT whole account)
608    CloseAll { file: PathBuf },
609    /// Paper-trading stats and ledger for --simulate runs
610    Sim {
611        #[command(subcommand)]
612        command: AgentSimCommands,
613    },
614    /// Historical options backtest (synthetic BS marks over Schwab daily bars)
615    Backtest {
616        #[command(subcommand)]
617        command: AgentBacktestCommands,
618    },
619}
620
621#[derive(Debug, Subcommand)]
622pub enum AgentBacktestCommands {
623    /// Download daily OHLCV (watchlist + SPY + VIX) into a local cache
624    Prefetch {
625        #[arg(long)]
626        rules_file: PathBuf,
627        #[arg(long)]
628        from: Option<String>,
629        #[arg(long)]
630        to: Option<String>,
631        #[arg(long)]
632        force: bool,
633    },
634    /// Replay trading days with synthetic vertical/condor marks
635    Run {
636        #[arg(long)]
637        rules_file: PathBuf,
638        #[arg(long)]
639        from: Option<String>,
640        #[arg(long)]
641        to: Option<String>,
642        #[arg(long)]
643        fresh: bool,
644    },
645    /// Analysis report from backtest journal + ledger
646    Report {
647        #[arg(long)]
648        rules_file: PathBuf,
649        #[arg(long)]
650        output: Option<PathBuf>,
651    },
652}
653
654#[derive(Debug, Subcommand)]
655pub enum AgentSimCommands {
656    /// ROI, win rate, open risk summary
657    Stats { file: PathBuf },
658    /// Full analysis from sim ledger + open positions
659    Report {
660        file: PathBuf,
661        #[arg(long)]
662        output: Option<PathBuf>,
663    },
664    /// Reset paper portfolio (keeps live agent-state untouched)
665    Reset { file: PathBuf },
666}
667
668#[derive(Debug, Subcommand)]
669pub enum RulesCommands {
670    /// Full rules breakdown (entry, exit, risk, LLM)
671    Show { file: Option<PathBuf> },
672    /// List discoverable rules/*.yaml files
673    List,
674}
675
676/// Resolved output path for clap parse result.
677impl Cli {
678    pub fn effective_output(&self) -> OutputFormat {
679        if self.json {
680            OutputFormat::Json
681        } else if self.md {
682            OutputFormat::Md
683        } else {
684            self.output
685        }
686    }
687}