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