Audits › YizhiSong/FriesTrader

Open-source trading bot · audit

YizhiSong/FriesTrader

Robinhood Agentic Trading agent — a fully automated AI trading bot placing real orders through Robinhood's Agentic Trading MCP, under mechanical, auditable risk rules the model cannot override. Able to run unattended on Claude Pro, no metered API spend. Not financial advice.

Repository facts

Repository
YizhiSong/FriesTrader
Stars
162
Forks
65
Open issues
0
Language
Python
License
MIT
Created
2026-07-17
Last push
2026-09-11
README names exchanges
robinhood
README mentions an LLM
yes
Backtesting mentioned
yes
Paper trading / dry run mentioned
yes

Source: GitHub API, 2026-09-17. Exchange and LLM mentions come from a keyword scan of the README, not from running the bot.

Risk-control audit

Audited 2026-09-17 · 458413627b69

21 of 21 controls checked: 9 present, 5 partial, 4 absent, 2 not applicable, 1 not verified.

Main risk
The mechanical risk rules are enforced by prompt instructions the same LLM session is trusted to follow, not by a code-level gate between the deterministic scripts' output and the actual place_equity_order/place_option_order calls -- so the README's central safety claim ('not the model's judgment') is not fully backed by the code in this repo, and this compounds with the admitted absence of any lockfile against two schedulers running the same phase concurrently.
Summary
FriesTrader is a template for an LLM-driven US-equities trading bot that trades a Robinhood account through Robinhood's own Agentic Trading MCP server, using two short scheduled Claude Code sessions a day (screen-and-thesis, then re-verify-and-execute) instead of a persistent daemon. Its size, stop-loss, take-profit, and account-wide loss-limit math is delegated to small deterministic Python scripts that the agent is instructed to read verbatim rather than recompute, and dry_run is the enforced default for a minimum number of cycles before live trading can even be considered. The main gap is that this separation between 'the model decides' and 'the rules enforce' is written into the task instructions, not built into the code as a hard interceptor in front of the actual order-placing tool calls, and the repo explicitly warns that running two schedulers at once can duplicate real orders with no lockfile to stop it.
ControlVerdictEvidenceNote
Size and exposure
Maximum order sizePresentrisk_rules.json position_sizing.max_position_pct_of_account=0.20 (% of account total_value, not a raw $ figure); enforced in scripts/position_sizing.py -- new-entry branch checks conviction_pct > max_position_pct, held/top-up branch caps via ceiling_room = max_position_pct*total_value - current_position_value. https://raw.githubusercontent.com/YizhiSong/FriesTrader/main/scripts/position_sizing.pyUnit is correctly % of account equity, not raw currency or base units, so the CloddsBot #126-style unit mismatch is avoided.
Execution paths that skip the capPresentREADME.md 'How it works' + PHASE_B_TASK.md Step 7: every buy (new entry or top-up) is routed through the same three scripts -- entry_gate.py -> rank_candidates.py -> position_sizing.py. Full repo tree (git/trees/main, 15 files) has no second order-sizing path.Sells (stop_loss/take_profit/conviction_trim/exit_existing) are explicitly exempt from this gate by design, since selling never increases exposure -- consistent with the spec, not a gap.
Total exposure and leveragePresentrisk_rules.json position_sizing.max_concurrent_positions=4, min_cash_buffer_pct=0.10, combined with the 20% per-position cap; enforced as open_slots = max_concurrent_positions - live_positions in PHASE_B_TASK.md Step 7 and as the cash-buffer check inside scripts/position_sizing.py.Caps both position count and cash buffer, on top of the per-order cap -- covers the 'many small orders sum to one big position' risk.
Brakes and stops
Global circuit breakerPresentrisk_rules.json loss_limits: daily 5% / weekly 10% of net_deposits_usd; scripts/pnl_pct.py computes the breach; PHASE_B_TASK.md (commit 458413627, diff on pnl_pct call) explicitly fails safe: 'if the script fails to run or any of the calls above can't be determined cleanly, fail safe: treat as breached'.Halts new entries/top-ups account-wide on breach, but does not force-close existing positions -- stop-loss/take-profit remain the only exit mechanism and are explicitly exempt from the halt (by design).
Orphaned stop-lossPartialscripts/stop_loss.py + PHASE_B_TASK.md Step 5: the stop is recomputed fresh every cycle from get_equity_positions/get_equity_quotes/get_equity_historicals, never held only in a running process's memory, so a crashed session can't 'lose' it between cycles.But it is not a resting stop order at the broker either -- it is only evaluated once per scheduled cycle (~8:35am Central weekdays, README), so a position has zero protection against a large intraday move between checks.
Orderly shutdownNot applicableREADME.md 'How it works': there is no long-running daemon to send a stop signal to -- each Phase A/B run is a bounded, scheduled Claude Code session. Step 6 (PHASE_B_TASK.md) always resolves sells with a confirmed order state before any buy is considered, and a failed tool call is logged and the candidate skipped rather than left ambiguous.Order type (market vs. limit) for place_equity_order was not found in the ~56% of PHASE_B_TASK.md read in this pass (file truncated at 20000/35765 chars) -- whether an unmonitored limit order could rest in the book is not_verificado within this pass, not ruled out.
Connection and operation
Websocket and data reconnectionPartialNo websocket exists in this architecture at all -- it is synchronous MCP tool calls inside two scheduled batch sessions (README 'How it works'), so continuous-connection reconnection does not apply. PHASE_B_TASK.md Step 6's only documented failure rule is generic: 'if a tool call fails, log the failure and skip that candidate.'No explicit HTTP 429 / rate-limit backoff logic found; cadence.news_search_budget_per_cycle=30 self-limits call volume but is not a retry mechanism. Remainder of PHASE_B_TASK.md (chars 20000-35765) not read in this pass -- may contain more detail, marked no_verificado in spirit.
Duplicate-process lockAbsentREADME.md 'Running it', verbatim: 'make sure only one scheduler is ever active for a given phase -- two schedulers firing the same phase in the same cycle risks duplicate risk_check/order log entries, or duplicate real orders once execution.mode is live.' https://github.com/YizhiSong/FriesTrader#running-itConfirmed by the author's own explicit warning, and by the full file tree containing no lockfile/PID/semaphore mechanism -- this is a stated, unmitigated gap left to the human's own scheduling discipline.
Heartbeat and alertsAbsentFull repo file tree (git/trees/main) has no Telegram/email/webhook code or config. The only documented failure-reporting channel is the Claude Code session's own end-of-run text summary (README Phase A/B prompt templates: 'report the exact conflict/error in your final summary').Requires the human to actively open the routine's run history to notice a failure -- no push notification, no external heartbeat/health endpoint.
Accounting and reconciliation
Position reconciliation with the exchangePresentPHASE_B_TASK.md Step 4 ('Pull get_equity_positions -- this snapshot... is also what Step 5's stop-loss/take-profit checks use') and Step 6 ('Re-pull fresh account state... the earlier pull is now stale for any sell that actually executed above').No independent local position ledger is kept -- every cycle re-derives truth from the broker, both at cycle start and again after this cycle's own sells.
Fees and funding in the accountingPresentInstrument is US equities via Robinhood only (no perpetuals/funding). PHASE_B_TASK.md's loss-limit section and scripts/pnl_pct.py source P&L from the broker's own get_realized_pnl/get_portfolio calls rather than recomputing it from raw fills, so any fee the broker applies is already reflected in those figures.Robinhood's exact current equity commission schedule was not independently re-verified against robinhood.com in this pass; relies on broker-reported P&L being fee-inclusive, which was not directly tested.
Order rejection handlingPresentPHASE_B_TASK.md Step 6: review_equity_order is always called first as a preview; a blocking alert is logged verbatim and treated as rejected without placing; after placement, get_equity_orders is polled up to twice (~15s apart) for a terminal state, whatever state comes back is logged, and a failed tool call is logged and the candidate skipped.No unbounded retry loop found that could multiply exposure on repeated rejection.
Auditable logPresenttrade_log.jsonl is append-only (README 'Why this is safer than it sounds') with a distinct stage per event type (risk_check, stop_loss, take_profit, conviction_trim, order, loss_limit_check, cycle_summary); every order entry carries proposal_date, order_id, order_state, fill_price, fill_quantity (PHASE_B_TASK.md Step 6). trade_log_template.jsonl documents the exact line shapes.Strong, structured, timestamped audit trail.
Security
API key permissionsNot verifiedREADME First-time setup #1: Agentic Trading 'requires a separate, dedicated account -- distinct from your regular investing account, and restricted to only the funds you put in it' (a segregation control). No API key, OAuth scope, or credential file exists anywhere in this repo to inspect.The actual credential/scope granted to Robinhood's MCP server is configured entirely outside this repository (Robinhood's own account settings + the user's Claude Code MCP connector config) -- cannot be verified or refuted from code alone.
Secrets handlingPresentFull repository tree (git/trees/main, 15 entries: README.md, LICENSE, PHASE_A_TASK.md, PHASE_B_TASK.md, risk_rules.json, trade_log_template.jsonl, scripts/*.py x7) contains zero API keys, tokens, or .env files; all 7 scripts take only CLI arguments, no credential handling.risk_rules.json's account_number field is an account identifier, not a secret. Auth lives entirely in the user's own Claude Code MCP connector config, never committed here.
Authenticated remote controlNot applicableNo Telegram/Discord/web control surface exists anywhere in the file tree. The only two 'inputs' to the system are the two scheduled routine invocations (PHASE_A_TASK.md/PHASE_B_TASK.md, README 'Running it') and the human hand-editing risk_rules.json via git.No inbound command channel exists for a third party to hijack -- this checklist item's risk doesn't apply to this bot's design.
AI and learning
The model cannot override the brakesPartialREADME states 'the actual safety mechanism is mechanical, auditable risk rules, not the model's judgment,' and every sizing/stop/gate value is computed by a standalone stdlib-only script the agent is told to 'use... directly rather than recomputing.' But PHASE_B_TASK.md's only enforcement of this is the written instruction 'Do not add, remove, or loosen any gate condition on your own judgment' -- the same agent session that reads a script's blocked/skip_buy JSON also holds the place_equity_order/place_option_order tool call (Step 6/8), and nothing in the repo code intercepts that call to verify it matches the script's output before it reaches Robinhood.Phase A is better protected: its prompt explicitly excludes place_equity_order/place_option_order/review_*/cancel_* 'at the connector level if your MCP setup allows it' -- but that exclusion is optional and depends on the user's own MCP setup, not guaranteed by this repo. This is the single biggest gap between the README's safety claim and what the code actually enforces.
Model output validationPartialscripts/rank_candidates.py hard-fails (exit 1) on any candidate whose conviction isn't exactly 'high'/'medium'/'low' -- a real code-level whitelist. A hallucinated/nonexistent symbol would very likely fail the mandatory fresh get_equity_quotes/get_equity_positions calls (Step 4-5), which per Step 6's generic rule gets logged and skipped rather than acted on.No explicit validation found for price-range plausibility or malformed numeric fields (e.g. a thesis_price of 0 or negative) before those values feed entry_gate.py's arithmetic.
Learns from its mistakesAbsentrisk_rules.json thresholds are static and manually maintained (README First-time setup #2: 'review every other threshold -- the defaults here are illustrative, not a recommendation'). No code or spec text found (within the portion of PHASE_A_TASK.md/PHASE_B_TASK.md read) describing automatic parameter adjustment, prompt/strategy rewriting, or a backtest-gated self-correction loop based on trade_log.jsonl outcomes.The bot's only 'memory' is the human reading trade_log.jsonl and manually editing risk_rules.json (README setup step 6).
Model poisoningPartialREADME 'How it works' step 3: Phase A runs 'a news search' per candidate and feeds the results directly into the model's direction/conviction rating written to pending_proposals.jsonl. No isolation, sandboxing, or instruction-stripping of that third-party text is described anywhere read.Blast radius is bounded: conviction only selects a fixed conviction-tier percentage of the account (per risk_rules.json position_sizing, not an amount the model can freely choose), and Phase A itself is barred from placing orders (see decision_ia_filtrada) -- so a poisoned article could at most bias which stock gets bought and at which fixed tier size, not an immediate live order or an arbitrary size.
Behavior without model quota or responseAbsentNo watchdog or alert was found for the case where a scheduled Phase A or Phase B Claude Code session simply fails to fire at all (e.g. a Claude Pro usage cap hit). Step 0's idempotency logic (PHASE_B_TASK.md) only covers what happens the NEXT time Phase B does run -- it does not protect open positions on a day the session never executes.No daily token/cost cap field exists in risk_rules.json -- consistent with the README's framing of an unmetered flat-rate Claude Pro subscription, but that framing does not address the risk of a fully-skipped run leaving stop-loss/take-profit unchecked for a day with no alert.

Audit A012 — YizhiSong/FriesTrader (plain-language summary)

**What it is.** A public GitHub template (MIT license, 162 stars, 65 forks) for a fully automated stock-trading bot. It does not run as a constantly-running program: instead, two short Claude AI sessions fire on a schedule each weekday — one in the afternoon to screen stocks and write a thesis, one 5 minutes after the market opens to re-check, size, and place trades. It only trades US stocks, on a single Robinhood brokerage account, through Robinhood's own official "Agentic Trading" connector.

**What's genuinely good.** All the position-sizing, stop-loss, take-profit, and account-wide loss-limit math is done by seven small, dependency-free Python scripts, not by the AI doing arithmetic in its head — same inputs always give the same numbers. New copies start in a safe "dry run" mode (no real money) for a minimum number of cycles before real trading is even possible, and every decision is written to a permanent log file. The bot never keeps its own private memory of positions — it re-checks the real account at the broker every single cycle. There is no remote-control channel (no Telegram/Discord bot) for an outsider to hijack, and no password or API key of any kind lives in the code — that all stays outside the repository, in the user's own Robinhood/Claude Code setup.

**What's missing or weak.** The single biggest gap: the "the rules control the AI, not the other way around" promise is written as an instruction to the AI, not built as a hard code barrier in front of the button that actually places a real order. The same AI session that reads the risk script's "don't buy" answer is the one that would also be pressing the "buy" button — nothing in the code stops it from pressing anyway if it decided to. Second: the project's own README admits that running two copies of the schedule at the same time can create duplicate real orders, and there's no lock to prevent that — it's left entirely to the human. Third: the stop-loss is recalculated fresh every cycle (safe against crashes) but is only checked once a day at market open — a stock can fall hard in between checks with no protection. Fourth: if a scheduled session simply fails to run at all (AI quota, outage), there is no alert to the human and no automatic fallback protection for open positions that day.

**Audited:** GitHub `YizhiSong/FriesTrader`, branch `main`, commit `458413627b6934e24b0d8533e56400b18e238c0f` (2026-09-11), read on 2026-09-17. One large spec file (`PHASE_B_TASK.md`, ~36KB) was only partially read (first ~20,000 characters of 35,765); the unread tail is marked `no_verificado` in the full JSON, not assumed either way.

Badge for the README

risk controls audit: 9/21 present

<a href="https://saasfactoryagents.com/bots/yizhisong-friestrader/"><img src="https://saasfactoryagents.com/bots/yizhisong-friestrader/badge.svg" alt="risk controls audit: 9/21 present"></a>

Fixed a control? Request a free re-audit at the new commit.

Exchanges

Running FriesTrader with real money?

The watchdog runs apart from the bot with a read-only key and alerts you when drawdown, position size or heartbeat cross your limits. Founding price 19 USD/month, early access open.