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