Skip to content
Ask AI about this page: Open in Claude Open in ChatGPT View as Markdown

How to use

Once it's running, this is a set-and-forget setup. What you tune is your rules, your ticker universe, your spend caps and, if you want, the schedule. Claude can read all of these but can't change any of them.

What File Takes effect
Budget and risk limits config/risk.toml next run
Tradable tickers config/universe/*.csv next run
LLM provider, model, spend caps, broker port .env next run (Docker: docker compose up -d to reload env)
Run times scripts/scheduled_run.py → SLOTS next tick (Docker: rebuild the image)

In Docker, config/ is bind-mounted read-only, so edits on the host apply without a rebuild.

Rules: config/risk.toml

[budget]
capital = 10000.0      # total virtual budget the bot may use
currency = "AUD"       # base currency for all accounting

[limits]
max_position_pct = 25.0         # no single holding above 25% of portfolio value
max_trades_per_month = 10       # orders submitted per calendar month
max_drawdown_pct = 25.0         # kill switch: sell all + halt at this drawdown from peak
max_price_deviation_pct = 2.0   # limit price must be within 2% of reference price
max_commission_pct = 10.0       # block orders whose commission exceeds 10% of order value
allow_fractional = false        # IBKR rejects fractional orders via the API
  • The budget is deposited into the journal's ledger once, by scripts/journal_cli.py init, so set capital and currency before you run init.
  • To raise the budget later, raise capital and run scripts/journal_cli.py topup (a dry run that prints the difference), then topup --confirm to deposit it. It deposits capital minus what is already deposited, so running it twice deposits once. Deposits are append-only: lowering capital below the total deposited is refused, there are no withdrawals. A top-up is not a drawdown (the peak only rises after two consecutive runs at the higher value) and the per-holding cap, which is a percentage of portfolio value, grows with it.
  • Values are validated on load. A missing or out-of-range value stops the run (fail-closed).
  • Tighter is always safe. To see which tickers fit your position limit at current prices, run scripts/affordability.py.

Universe: which tickers may be traded

[universe] files lists CSV files. Only instruments in these files can be bought; no files or empty files means nothing can be bought.

[universe]
files = [
  "config/universe/my_picks.csv",
  "config/universe/top_sp500.csv",
]

Each CSV has the columns symbol,exchange,currency,name,source:

symbol,exchange,currency,name,source
NVDA,SMART,USD,NVIDIA Corp,my-picks
CBA,ASX,AUD,Commonwealth Bank,my-picks

US stocks and ETFs use SMART/USD; ASX uses ASX/AUD. To use your own list, add a CSV and point files at it. scripts/refresh_universe.py rebuilds top_sp500.csv from the SPY holdings file (largest S&P 500 names by weight); run it monthly if you use that list.

Keep the universe small

Every ticker is priced on every run and summarised for Claude. About 50 keeps prompts small, pricing fast and cost low.

LLM provider and spend caps

In .env:

LLM_PROVIDER=anthropic            # anthropic | openrouter
ANTHROPIC_API_KEY=...             # or OPENROUTER_API_KEY=...
CLAUDE_MODEL=                     # blank = default Sonnet; Haiku is cheaper
LLM_RUN_BUDGET_USD=0.10           # stop the loop (no trades) if one run costs more
LLM_MONTHLY_BUDGET_USD=2.50       # skip Claude for the rest of the month once spent
ENABLE_WEB_SEARCH=0               # 1 = allow news search (extra cost per search)
LLM_REQUEST_TIMEOUT_S=60          # per API call (SDK default is 10 min)
AGENT_MAX_SECONDS=300             # whole agent loop; over it = no trades

Cost is computed from actual token usage and a price table in llm.py. A model with no known price is refused, so caps can't be bypassed by picking an unpriced model.

Schedule

Two slots per US trading day, in New York time, so daylight-saving changes on either side are handled automatically:

SLOTS = {"open": (time(10, 30), time(11, 30)), "close": (time(15, 0), time(15, 40))}
MAX_ATTEMPTS = 3

The scheduler ticks every 15 minutes. Each slot runs at most once per day (up to 3 attempts if IB Gateway is briefly unavailable). Weekends and US holidays are skipped using IBKR's own trading hours, with no LLM call.

Going live

Not recommended until you've watched paper trading for a while. It needs three deliberate changes: TRADING_MODE=live, a live port (4001, or 4003 in Docker), and a live account. The code refuses any mismatch between mode, port and account ID.