Personal saving and investment bot that rebalances a portfolio of NASDAQ-100–derived tickers toward equal notional exposure on the Alpaca API (paper or live).
- Ticker source: Builds the universe from SlickCharts NASDAQ-100, filtered for Alpaca-tradable, fractionable, active assets (excluding PTP exceptions).
- Rebalance logic: Targets equal notional value per ticker with a configurable margin; buys/sells to bring positions back within the band; uses limit orders when the market is in extended hours.
- Paper and live: Switch via
VERSIONin.env; uses Alpaca paper or live API and keys accordingly. - Extended hours: Supports pre/post market; places limit orders when the main session is closed.
- State: Persists tickers, equity, market state, and open limit orders in
trading_state.json. - Trade log: Every order and liquidation is appended to
trades.jsonlfor offline analysis. - Optional remote logging/recording: When
REMOTE_LOGGING_ENABLED=true, posts logs, heartbeats, and daily records to bmd-studios.com (orREMOTE_BASE_URL). Default is off; failures never stop the bot.
- Python 3.9+
- Dependencies in
requirements.txt: python-dotenv, requests, beautifulsoup4, requests-ratelimiter, textual
-
Virtual environment (recommended):
python -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate pip install -r requirements.txt
-
Environment variables: Copy
.env.exampleto.envand fill in values. Do not commit.env.Variable Description VERSIONPAPERfor paper trading, any other value (e.g.real) for live.PAPER_KEY/PAPER_SECRETAlpaca paper credentials (when VERSION=PAPER).API_KEY/API_SECRETAlpaca live credentials (when not paper). MARGINRebalance margin (float). Recommended range 0.02–0.15. REMOTE_LOGGING_ENABLEDtrueto enable remote posts; defaultfalse.REMOTE_BASE_URLBase URL for remote endpoints (default https://www.bmd-studios.com).LOG_LEVELStandard logging level (default INFO).LOG_FILEOptional path for rotating log file (leave empty for console only). -
First run: If
trading_state.jsonis missing, the TUI creates it automatically when you openpython -m tui. Headless mode creates it on startup, or runpython bot.py --initto create the file only.
Headless bot (scheduler in foreground):
python bot.pyTerminal UI (start/stop bot, margin, live status, analytics):
python -m tuiBot logs go to alpaca_bot.log when using the TUI (console logging is disabled so the UI stays readable). Set LOG_FILE in .env to customize.
Scheduler: adaptive bot tick (60s when open/extended, up to 30m when closed), hourly balance check, day-end at 22:00 America/New_York when remote logging is enabled.
| Key | Action |
|---|---|
t |
Toggle start/stop bot |
r |
Refresh state, trades, and Alpaca data (Logs tab reloads from file) |
q |
Quit (prompts if bot is running) |
1 |
Dashboard |
2 |
Positions |
3 |
Trades |
4 |
Analytics |
5 / 6 |
Logs |
7 |
Backtest |
8 |
Settings |
Use the tab bar or footer keys 1–5, 7, and 8 to switch views. The status bar also shows the active view name.
Status bar: Bot tick is the last scheduler loop completion (about every 1 min when the market is open/extended, less often when closed). UI updated (on the Dashboard) is when the TUI last read local state/logs (every 2 s while the bot runs or on Logs/Backtest; otherwise about every 5 s for the active tab only).
SSH / tmux: Use UTF-8 (export LANG=en_US.UTF-8, start tmux with tmux -u). If bar charts or borders look wrong, run ALPACA_TUI_ASCII=1 python -m tui for ASCII activity bars.
On the Logs tab, new lines are appended without clearing the viewer (scroll position is preserved). Press r to reload the full log window.
Tables: Click a column header to sort. Numeric columns sort by value, not as text. Trades rows use a muted green (buy) or blue-gray (sell) tint. Analytics rows use a muted green/red tint by trading P/L sign.
Headless (python bot.py):
| Flag | Effect |
|---|---|
-v / --verbose |
DEBUG |
-q / --quiet |
WARNING and above |
--log-level debug |
Explicit level (debug, info, warning, error, critical) |
--log-file PATH |
Also write to a rotating log file |
.env LOG_LEVEL applies when no CLI flag is set. Example: python bot.py -v --log-file alpaca_bot.log
TUI (python -m tui): logs go to alpaca_bot.log (or LOG_FILE). Open the Logs tab to filter by level and change what the bot writes. Same CLI flags: python -m tui -v.
Dashboard shows the last few WARNING+ lines; the Logs tab shows up to 400 lines with color by level.
| Path | Description |
|---|---|
bot.py |
Headless CLI entry point |
runner.py |
BotRunner start/stop scheduler (CLI + TUI) |
tui/ |
Textual terminal UI (python -m tui) |
analytics.py |
Trade/portfolio aggregates for TUI |
env_config.py |
Read/write MARGIN in .env |
config.py |
Environment loading and validation |
state.py |
trading_state.json load/save |
alpaca_client.py |
Alpaca HTTP session and account/positions |
ticker_source.py |
NASDAQ-100 ticker discovery |
market.py |
Market clock and session state |
rebalance.py |
Rebalance loop |
orders.py |
Order placement and OrderResult |
trade_log.py |
Append-only trades.jsonl |
remote.py |
Optional remote HTTP client |
reporting.py |
Daily record and check-in |
scheduler.py |
bot_loop and decorators |
requirements.txt |
Runtime dependencies |
requirements-dev.txt |
pytest (development) |
.env |
Local secrets (not committed) |
trading_state.json |
Runtime operational state |
trades.jsonl |
Runtime trade history (not committed) |
See AGENTS.md for a concise map for contributors and coding agents.
All trades are written to trades.jsonl (one JSON object per line). Example queries:
# Pretty-print all trades
cat trades.jsonl | jq .
# Filled buys only
jq 'select(.status == "filled" and .side == "buy")' trades.jsonl
# Count by symbol
jq -r .symbol trades.jsonl | sort | uniq -cIn Python:
import json
trades = [json.loads(line) for line in open("trades.jsonl")]The Analytics tab combines trades.jsonl (filtered by period) with trading_state.json and optional Alpaca refresh.
| Column | Meaning |
|---|---|
| Price | Current share price from Alpaca positions after refresh, otherwise from trading_state.json. |
| Trading P/L | (sell $ − buy $) + (net rebalance qty × current price) for filled rebalance trades (rebalance_buy, rebalance_sell, rebalance) in the period. Excludes initial buys (rebalance_initial) and orphan liquidations (liquidate). — when there were no rebalance fills for that symbol. Summary line shows the portfolio total. |
| Unreal. P/L | Alpaca market_value − cost_basis after Refresh from Alpaca. |
| Swing % | How far the position is from the bot’s equal-weight target (rebalance band), not daily stock return. |
Summary Avg Net $ is the per-ticker rebalance target: equity ÷ (ticker_count + ticker_count×margin÷2) (same as the live rebalance loop).
Trade activity (footer chart) ranks symbols by filled trade count in the period, not failed or limit-placed orders.
Use period All for a full trading P/L picture; shorter windows only include rebalance fills logged in that range.
Click a column header to sort (click again to reverse).
Historical simulation via Alpaca bars (5Min, IEX, split-adjusted), cached in SQLite. Steps every 5 minutes during regular US hours (live bot ticks every 1 minute).
python -m backtest fetch --start 2025-01-01 --end 2025-12-31
python -m backtest status
python -m backtest run --start 2025-01-01 --end 2025-12-31 --cash 100000 --margins 0.03,0.05,0.10Run comparison (CLI or TUI tab 7) runs three strategy families on the same range and cached bars:
| Strategy | Description |
|---|---|
| Equal-wt B&H | Invest cash / N in each symbol at the first RTH bar, hold |
| Cap-wt B&H (static NDX wt) | Invest cash × weight using current SlickCharts index weights (data/backtest_weights.json) — not historical point-in-time weights |
| Rebalancer | Full rebalance_tick loop, once per margin in BACKTEST_MARGINS or the margins field |
Outputs: backtest_comparison.csv, backtest_equity.csv / backtest_trades.jsonl for the primary margin (detail tables in the TUI). Fetch/run Python logs go to backtest.log (BACKTEST_LOG_FILE), not alpaca_bot.log, so live bot logs stay clean when using the TUI. See .env.example for BACKTEST_* settings. First fetch needs network and market-data access on your Alpaca account; run uses the cache only.
pip install -r requirements-dev.txt
pytestCI runs pytest on Python 3.9, 3.11, and 3.12 (see .github/workflows/test.yml).
- Alpaca — trading, clock, calendar, positions, market data
- SlickCharts — NASDAQ-100 constituent list (scraped)
- bmd-studios.com — optional remote logging/recording (off by default)
Alpaca API calls use LimiterSession (200/min). Scraping is separate and not rate-limited by that session.
The bot is modular with tests and optional remote logging. Possible follow-ups:
- Integration tests against Alpaca paper API (mocked HTTP)
- Richer rebalance unit tests (margin band math)
- Non-blocking scheduler if loop latency becomes an issue
