A systematic trading bot for Alpaca, built and run against a real ~$150 live account. This repo is as much a record of the mistakes and the evidence that corrected them as it is working code — see What I've Learned before the code itself.
Not financial advice. This is a personal project for learning and experimentation with a small amount of real money. Nothing here is a recommendation to trade, and past replay/backtest performance does not predict future results. See Disclaimer.
- What This Is
- What I've Learned
- Current Strategy & Results
- How It Works
- What It Uses
- Setup
- Profiles
- Run / Validate / Monitor
- Deployment
- Risk Controls
- Crypto (BTC/USD)
- Small Equity Accounts
- Docs
- Disclaimer
A trend-following bot that pulls bars from Alpaca, evaluates a rules-based signal (moving averages, ADX, ATR, volume, and a higher-timeframe regime filter), sizes and places orders through Alpaca's API, and tracks everything — fills, P&L, risk-limit hits, rejection reasons — in SQLite so it can be audited after the fact instead of trusted blindly.
It currently:
- pulls bars from Alpaca for stocks (IEX feed) or crypto (CryptoHistoricalDataClient)
- generates trend-following signals on a configurable timeframe
- applies layered risk checks before entering trades (daily drawdown, consecutive losses, cooldown, hard stop)
- manages exits via trailing stop, hard stop, breakeven stop, profit lock, time stop, and trend reversal
- records runs, orders, events, and closed trades in SQLite
- writes daily and monitoring reports to
reports/
The interesting part of this project isn't the code, it's what running it live with real (small) money exposed:
- A strategy that looks reasonable on paper can be structurally unable to make money at a given account size. The original live setup traded BTC/USD on 5-minute bars with $150 of capital. Over 120 days it made exactly 2 trades — the entry filter stack required ~14 conditions to align at once. Position sizes were capped around $45, and Alpaca's crypto round-trip friction (~0.6%, taker fee + spread) was often larger than the trade's entire expected profit. It wasn't unlucky — it was unwinnable by construction.
- "Trade more, smaller" made it worse, not better — I tested it instead of assuming. A loosened, higher-frequency BTC variant made 184 trades over a year and lost $95, of which ~$99 was pure fee friction. On a small crypto account, activity itself is the cost. This only became obvious by replaying it against real historical data rather than reasoning about it in the abstract.
- The venue mattered more than the signal. The same trend logic, run on equities (near-zero commission) instead of crypto (~0.6% round trip), turned from a loser into a net-positive strategy in replay — without changing the core idea, just the friction it had to overcome.
- Backtests can lie quietly through infrastructure bugs, not just overfitting.
While diagnosing this, I found the replay harness never reset its
consecutive-loss counter on day rollover the way the live code does — so
any backtest that hit its loss-streak limit silently stopped trading for
the rest of the test period. Every prior research report in this repo was
undercounting trades because of it. Fixed in
bot/trade_controls.py; full writeup indocs/BUILD_LOG.md. - Cron intervals need to match the strategy's timeframe, not just "run often." Moving from 5-minute to hourly bars and leaving the deploy cron at every 5 minutes would have meant ~12x redundant invocations per hour — harmless (guarded by cooldown checks) but a real signal that deploy config and strategy config can silently drift apart if nobody checks.
Full investigation with numbers: docs/strategy_revamp_2026-07.md.
Change-by-change history: docs/BUILD_LOG.md.
The default live/paper deployment is an hourly QQQ trend-following strategy sized for a small account: long-only, fractional, ~90% notional per position, a daily-EMA regime filter, and wide trailing exits that hold winning trends for days rather than minutes. A defensive BTC/USD profile is also available — it only trades confirmed multi-hour uptrends and is designed to sit out downtrends entirely rather than force trades.
Replay results, $150 starting capital, real historical bars, realistic
slippage assumptions (see the strategy doc for methodology — these are
backtest results, not live account performance):
| Profile | Period | Net P&L | Profit Factor | Max Drawdown | Trades |
|---|---|---|---|---|---|
| QQQ hourly trend (live default) | 2023-08 → 2026-07 (~3 yrs) | +$55.3 (+37%) | 1.76 | 6.7% | 74 (~2/mo) |
| BTC hourly, strict uptrend gate | 2025-26 (bear year, BTC −46%) | $0.00 | — | 0% | 0 (stayed flat) |
| BTC hourly, strict uptrend gate | 2024-25 (bull year) | +$3.1 | 1.14 | 10.6% | 12 |
| (retired) BTC 5m scalp, live config | 2026-03 → 2026-07 (real bars) | −$0.85 | 0.0 | 0.6% | 2 |
Alpaca bars → indicators (SMA/EMA/ADX/ATR/volume) → regime filter (higher timeframe)
→ signal (LONG/HOLD) → risk gate (drawdown, loss streak, cooldown, staleness)
→ position sizing → order submission → SQLite (runs, orders, closed trades, events)
→ reports (daily / monitor / research / optimize)
- Signal:
bot/strategy_ma.py— SMA trend cross confirmed by ADX, ATR-bounded volatility, a trend EMA, and a higher-timeframe regime filter (e.g. daily EMA for the equity profile). - Risk:
bot/risk.py,bot/trade_controls.py— daily drawdown/loss caps, consecutive-loss halt, entry cooldown, stale-data rejection, position-notional caps. - Execution:
bot/broker_alpaca.py,bot/main.py— order submission, fill reconciliation, broker position sync. - Persistence:
bot/store.py— SQLite schema for runs, orders, position state, closed trades, and events, so every decision (including ones that resulted in no trade) is auditable later. - Research:
bot/research.py,bot/optimize_strategy.py— replay/backtest harness and walk-forward parameter search, used to generate the results above.
- Python 3.11
- Alpaca API (
alpaca-py) - SQLite
- Docker / Docker Compose
- GitHub Actions (CI + EC2 deploy)
- Copy
.env.exampleto.env - Add your Alpaca keys (paper or live)
- Choose a profile config from
config/or adjust settings directly in.env
Key env values:
| Variable | Default | Description |
|---|---|---|
ALPACA_API_KEY |
— | Alpaca API key |
ALPACA_SECRET_KEY |
— | Alpaca secret key |
ALPACA_PAPER |
true |
Set false for live trading |
SYMBOL |
SPY |
Any equity or BTC/USD for crypto |
IS_CRYPTO |
false |
Set true to enable crypto mode (24/7 market, fractional qty, GTC orders) |
TIMEFRAME_MINUTES |
5 |
Bar timeframe in minutes |
POSITION_SIZING_MODE |
fixed |
fixed, notional_cap, or atr_risk |
ALLOW_FRACTIONAL_EQUITIES |
false |
Allow fractional equity quantities for small live stock accounts |
MIN_ORDER_NOTIONAL |
1.0 |
Minimum dollar exposure for fractional stock or crypto entries |
HARD_STOP_ATR_MULT |
0 |
Hard stop distance in ATR units from entry (0 = disabled) |
ENABLE_BREAKEVEN_STOP |
false |
Move stop to breakeven after N ATR of profit |
ENABLE_PROFIT_LOCK |
false |
Lock in partial profit after N ATR move |
MAX_DAILY_DRAWDOWN_PCT |
0.01 |
Halt entries after this % daily loss |
MAX_DAILY_LOSS |
0 |
Dollar daily loss cap (0 = disabled) |
ALLOW_OVERNIGHT_HOLDING |
false |
Keep positions overnight (set true for crypto) |
FLATTEN_BEFORE_CLOSE_MINUTES |
5 |
Force flat this many minutes before 4 PM ET (set 0 for crypto) |
These are the raw code-level defaults (used only if you run bot.main
directly without a profile). Every shipped profile in config/ overrides
the relevant ones — see Profiles.
Pre-built configs live in config/:
| File | Symbol | Use |
|---|---|---|
config/paper_spy.env |
QQQ | Paper trading equities (hourly trend, multi-day holds) |
config/live_spy.env |
QQQ | Small-account live equities — the recommended live profile |
config/paper_btc.env |
BTC/USD | Paper trading Bitcoin (defensive uptrend-only) |
config/live_btc.env |
BTC/USD | Live Bitcoin — defensive, dormant outside confirmed uptrends |
See docs/strategy_revamp_2026-07.md for the replay evidence behind these profiles.
Load a profile by setting BOT_PROFILE / BOT_MARKET, by sourcing the file before running, or with the profile runner:
python -m bot.profile_runner paper trade spy
python -m bot.profile_runner live trade spyBefore relying on a schedule, verify both the paper account and market-data feed (this never places an order):
python -m bot.profile_runner paper connectivity btcThe BTC paper profile is an exploration profile: it permits up to 8 entries per day with a one-bar cooldown and slightly looser ADX/volume gates, while reducing target notional to 25% and ATR risk sizing to 1%. Live BTC settings are unchanged. The EC2 paper schedule refreshes its monitor hourly and its historical research report daily.
# Paper trade (equity config, Alpaca paper account)
docker compose run --rm paper
# Live trade (equity config, Alpaca live account)
docker compose run --rm trade
# BTC variants
docker compose run --rm paper-btc
docker compose run --rm trade-btc
# Generate monitor report
docker compose run --rm monitor
# Generate paper BTC monitor report
docker compose run --rm paper-monitor
# Validate the full build and runtime setup
docker compose run --rm validateRun the built-in runtime validation:
docker compose run --rm validateThis checks:
- runtime directories
- database setup
- signal generation
- risk evaluation
- report generation
Generate the latest monitor report:
docker compose run --rm monitorOr use the profile-specific monitors:
docker compose run --rm paper-monitor
docker compose run --rm live-monitorThe monitor report includes recent rejection counts, near-miss entry bars, and the latest strategy metrics (regime_side, momentum_pct, pullback_depth_atr, bar_range_atr, volume_ratio, and signal_strength) so quiet live periods can be diagnosed from recorded bot state.
Run the research / replay report:
docker compose run --rm researchRun the walk-forward optimizer:
docker compose run --rm optimizeBTC equivalent:
python -m bot.profile_runner live optimize btcThe optimizer now logs progress while it runs. For a quick smoke test, cap the search first:
$env:OPT_MAX_CANDIDATES="10"
docker compose run --rm optimizeMain outputs:
reports/monitor_latest.mdreports/monitor_latest.jsonreports/daily_YYYY-MM-DD.mdreports/research_latest.mdreports/research_latest.jsonreports/optimize_latest.mdreports/optimize_latest.json
The optimizer compares candidates against the loaded live baseline over the same bars, for either market. A candidate is marked accepted only if it clears the full replay, walk-forward, baseline-improvement, and 2x-slippage checks in the report.
The bot deploys to a plain EC2 instance via GitHub Actions: the workflow at
.github/workflows/deploy-ec2.yml rsyncs the repo over SSH, builds the
Docker image on the instance, and installs a cron job — no container
registry or AWS IAM setup involved. Full setup steps, required secrets, and
verification commands are in docs/github_actions_ec2.md.
It triggers automatically on pushes to master, or manually from the
Actions tab with a chosen profile (live/paper) and market
(spy/btc, defaults to spy).
- Restrict EC2 security groups to only allow SSH from your IP
- Generate a dedicated SSH key pair for deployment
- Store sensitive credentials only in GitHub secrets and the
.envfile on EC2 - Regularly rotate API keys and SSH keys
- Never commit
.env,.pem, or any real API key —.gitignoreblocks the common patterns, but double-check before pushing
bot/- trading logic, broker integration, storage, reportingconfig/- per-profile env files (live_spy,paper_spy,live_btc,paper_btc)data/- SQLite databaselogs/- runtime logs and CSV snapshotsreports/- generated reportsdocs/- strategy audits, build log, and deployment guides
The bot has multiple independent safety layers:
| Control | Config Key | Default |
|---|---|---|
| Hard stop loss | HARD_STOP_ATR_MULT |
disabled |
| Trailing stop | TRAIL_ATR_MULTIPLIER |
1.5x ATR |
| Breakeven stop | ENABLE_BREAKEVEN_STOP |
false |
| Profit lock | ENABLE_PROFIT_LOCK |
false |
| Time-based stop | MAX_BARS_IN_TRADE |
12 bars |
| Regime-invalidation exit | EXIT_ON_REGIME_INVALIDATION |
true |
| Daily drawdown halt | MAX_DAILY_DRAWDOWN_PCT |
1% |
| Dollar loss cap | MAX_DAILY_LOSS |
disabled |
| Consecutive loss halt | MAX_CONSECUTIVE_LOSSES |
3 |
| Max trades per day | MAX_TRADES_PER_DAY |
5 |
| Entry cooldown | COOLDOWN_BARS |
2 bars |
| Stale data check | ENABLE_STALE_BAR_CHECK |
false |
Set IS_CRYPTO=true (or use config/paper_btc.env / config/live_btc.env) to enable crypto mode:
- Uses
CryptoHistoricalDataClientfor bar data (no IEX feed requirement) - Bypasses NYSE market-hours check — trades 24/7
- Orders use
TimeInForce.GTCinstead ofDAY - Position sizing returns fractional quantities (e.g.
0.0005 BTC) - Trades hourly bars like the equity profile; cron runs once an hour around the clock (see
docs/github_actions_ec2.md)
Important for small accounts: Alpaca crypto costs ~0.25% taker fee + spread per
side (~0.6% per round trip). Replay evidence in docs/strategy_revamp_2026-07.md
shows that at ~$150 of equity this friction exceeds any repeatable intraday
edge — every high-frequency BTC variant tested lost money after fees. The
shipped BTC profiles are therefore deliberately defensive: hourly bars, a
strict 4h uptrend regime gate, near-full-notional single positions, and wide
trailing exits. Expect them to be dormant in downtrends. Prefer the equity
profile for growth.
For a roughly $150 account, one whole share of most ETFs is too large a
chunk of the account to size or diversify sensibly. config/live_spy.env
(despite the filename, it trades QQQ) already sets this up:
ALLOW_FRACTIONAL_EQUITIES=trueALLOW_SHORTS=falsePOSITION_SIZING_MODE=notional_capwithTARGET_POSITION_NOTIONAL_PCT=0.90— near-full-notional single positions, since a $150 account can't usefully diversify anywayMAX_POSITION_NOTIONAL_PCTabove the target as a hard ceiling
docs/strategy_revamp_2026-07.md— the investigation and evidence behind the current strategydocs/BUILD_LOG.md— running log of strategy and infrastructure changesdocs/github_actions_ec2.md— EC2 deployment setupOPERATIONS.md— day-to-day commands (run, monitor, research, optimize)docs/strategy_audit_current.md,docs/live_account_path_100usd.md— earlier, now-superseded audits, kept as historical record
- Keep real API keys only in your local
.env— never commit them. - Without a profile, the raw
bot.maindefault is an intraday equity system: it flattens inherited overnight positions on the next session and exits before market close. Both shipped profiles override this —config/live_spy.envandconfig/live_btc.envboth setALLOW_OVERNIGHT_HOLDING=trueandFLATTEN_BEFORE_CLOSE_MINUTES=0, since the current strategy is a multi-day trend hold, not an intraday one. - The
spy-market runners write toruntime/paperandruntime/live;btc-market runners write toruntime/paper_btcandruntime/live_btc. - Use the optimizer to rank parameter sets on walk-forward windows before going live.
- Run
scripts/validate.ps1(Windows) orscripts/validate.sh(Unix) for a full local validation pass.
This is a personal, educational project. It is not investment advice, and nothing in this repository — including the replay results above — is a recommendation to buy, sell, or hold any security or asset. Backtest and replay performance do not guarantee future results; they use simplified fill/slippage assumptions and a limited historical window. Trading involves risk of loss, including total loss of capital. The author is not a registered investment advisor. Use at your own risk, and never trade with money you cannot afford to lose.