Guardrails
EdgeProof is designed so that the system is hard to break, not just hard to misconfigure. Guardrails are structural. They apply before any order reaches the broker.
Position sizing
Every position is sized by a dollar budget:
laneFraction = min(isLotto ? TRADING_LOTTO_SIZE_FRACTION : 1,
isStarter ? TRADING_STARTER_SIZE_FRACTION : 1)
laneBudget = positionSizeUsd × laneFraction
qty = floor(laneBudget / (marketPrice × 100))
qty = max(qty, 1) // always at least 1 contract if budget allows
qty = min(qty, TRADING_MAX_CONTRACTS_PER_TRADE) // only when set (off by default)
TRADING_POSITION_SIZE_USD defaults to $500. At $5.00 per contract ($500 notional value), that's 1 contract. At $2.50, that's 2 contracts.
Lotto and starter signals are not sized at the full rate. A lotto gets a quarter of the budget (TRADING_LOTTO_SIZE_FRACTION, default 0.25) and is skipped with a budget-cap error if even 1 contract doesn't fit that lane: a cheap lotto contract must never buy full-budget size. A starter gets a third of the budget (TRADING_STARTER_SIZE_FRACTION, default 0.33), but always gets at least 1 contract as long as the price fits the full (non-lane) budget, so a light entry priced out of its lane still gets filled at minimum size. Both lanes compose with source-weighted sizing below: the source multiplier scales the budget first, then the lane fraction applies. When a signal is both a lotto and a starter, the smaller lane governs sizing.
TRADING_MAX_CONTRACTS_PER_TRADE is off by default (0): the dollar budget is the sizing control, and a $1 contract is the same exposure as a $6 one. Set a positive value to add a liquidity ceiling: very cheap contracts can otherwise mean order sizes larger than the option's book comfortably absorbs.
Per-underlying cap
TRADING_MAX_CONTRACTS_PER_UNDERLYING (default: 2) limits how many contracts EdgeProof will hold across all strikes and expiries for one underlying ticker. If SPY already has 2 contracts, new SPY signals are skipped until a position closes.
Signal staleness
TRADING_MAX_SIGNAL_AGE_MINUTES (default: 60): entry signals older than this are discarded without attempting an order. Options pricing can move significantly in an hour; a stale entry is often worse than no entry. Channel exits use the same window differently: they degrade rather than drop; see "Channel-driven exits" below.
Price ceiling (hard cap)
TRADING_HARD_CAP_PCT (default: 10%): if the current market price is more than 10% above the signal's recommended entry, the order is skipped. This prevents chasing momentum on late signals.
Entry price requirement
Both the entry sanity ratio and the price ceiling above key off the signal's stated entry price: without one, they have nothing to anchor to and silently disable themselves. TRADING_REQUIRE_ENTRY_PRICE (default: true) skips any entry signal with no positive entry price before it reaches those checks, so a watchlist-style message ("keep an eye on SPY 500c") can't execute at market. Set to false to disable this guard.
Risk filters (you choose your appetite)
These are not the author's personal limits baked into the product; they are yours to set. edgeproof configure asks about the common ones; all are configurable via env. Defaults are conservative, so you opt in to riskier signal types rather than having them forced on you.
| Variable | Default | Effect |
|---|---|---|
TRADING_ALLOW_0DTE |
false |
When false, skip options expiring the same trading day |
TRADING_ALLOW_LOTTOS |
false |
When false, skip lotto / long-shot signals (e.g. very low premium) |
TRADING_ALLOW_LOW_CONVICTION |
false |
When false, skip signals the extractor marks low-conviction (and not a starter) |
TRADING_MAX_OPEN_POSITIONS |
10 |
Global cap on concurrent open positions across all underlyings. At the cap, new entries are skipped; add-ons to existing positions are still allowed |
TRADING_ADD_ON_CAP |
3 |
Maximum number of add-on entries allowed on a single existing position |
TRADING_ENTRY_SANITY_RATIO |
3 |
Reject an entry whose live price is more than this multiple above (or below 1/ratio) the signal's stated entry (guards against fat-finger prices and mismatched contracts) |
Exit rules (per position)
Configured via POSITIONS_DEFAULT_* vars. Defaults:
| Rule | Default | Meaning |
|---|---|---|
| Take-profit | +75% | Close when position value is 75% above entry |
| Stop-loss | −50% | Close when position value is 50% below entry |
| Trailing stop | −40% from peak | Close if price falls 40% from the highest mark |
| Trailing activation | +30% | Trailing stop only activates after a +30% gain |
| Max hold | 150h | Force-close after 150 hours regardless of P&L |
These are defaults applied to every position. Individual signals can set tighter rules via the signal text (EdgeProof's LLM extraction respects explicit targets when present).
Learned exit calibration (opt-in)
TRADING_SOURCE_EXIT_CALIBRATION (default: false): once enabled, EdgeProof derives a tighter trailing-stop activation per source (how early the trail arms) from that source's own median realized winner exit, and tightens the trailing-stop width itself by a fixed fraction of the global default. It is tighten-only: the global trailing-stop percent and activation percent above are the ceiling, never the floor, and take-profit/stop-loss are never touched. Calibrated values never tighten below a floor of 10% or half your configured activation, whichever is higher. This bounds the self-reinforcing ratchet, since the median that drives calibration is itself measured from EdgeProof's own prior exits. Calibration shares its gate with source-weighted sizing: TRADING_SOURCE_WEIGHTING_MIN_TRIPS (default 10) closed round trips are required for a source before its trailing settings tighten at all, and a source with no realized winners yet is left at the plain global defaults. As a source's ledger history accumulates, the derived activation adapts with it. Like the DTE-bucketed exit defaults it composes with, calibration is computed once at position-discovery time and frozen for the life of the hold.
Kill switch
edgeproof halt # pause entries, keep managing exits
edgeproof flatten # close everything, then halt
halt stops new trade entries within one monitor cycle. Open positions continue to be managed (exits still fire). No restart required to resume:
edgeproof resume
flatten places market orders to close all open positions, then halts. Use this before going offline for an extended period.
Trading hours
EdgeProof checks that the Alpaca clock says the market is open before placing entries. Exits may fire outside regular hours if the signal warrants it (e.g., pre-market stop-loss triggers via streaming). Use --bypass-trading-hours (supervisor dev mode only) to disable this check in testing.
Slippage control
TRADING_SLIPPAGE_PCT (default: 7%): limit orders are placed at midpoint × (1 + slippage) for buys. If the order doesn't fill, a retry at TRADING_RETRY_SLIPPAGE_PCT (default: 10%) is attempted after TRADING_RETRY_MIN_AGE_MINUTES.
Channel-driven exits
By default EdgeProof ignores what a channel says about closing. It opens positions from entry calls and closes them using your own rules: take profit, stop, trailing stop, time limit. That is the product's default position: the channel picks the trade, you decide when to leave it.
Setting TRADING_FOLLOW_CHANNEL_EXITS=true lets a channel also close or reduce
positions. Some callers' real edge is their exit discipline, and this follows
it. What it costs you is a share of the exit decision, so it is opt-in rather
than on by default.
Resolution is account-level, not origin-level: a symbol-only exit call resolves against any long option position held on that underlying in your account, including one EdgeProof did not itself open. This is still strictly opt-in, still only ever reduces a position (never opens or adds), and still skips rather than guesses whenever more than one position on the underlying could match.
What it acts on
EdgeProof tracks each author's de-risk stage on a position (trimming, then runners, then flat) rather than reacting to each message in isolation:
| Author's stage | What EdgeProof does |
|---|---|
| Trimming ("taking a trim here", "half off") | Sells the stated fraction, else TRADING_VAGUE_TRIM_FRACTION; tightens the trailing stop |
| Runners ("leave a few to run", "risk free now") | Sells down to TRADING_RUNNER_CONTRACTS; floors the stop at breakeven while in profit |
| Flat ("all out", "stopped out") | Closes the remainder |
TRADING_VAGUE_TRIM_FRACTION=0 keeps sizeless TRIMS notify-only; a runners
call still sells down to TRADING_RUNNER_CONTRACTS, and explicit closes still
close. Set TRADING_FOLLOW_CHANNEL_EXITS=false if you want no author-driven
selling at all.
Stages only move forward. A repeated or out-of-order message can never sell twice, and a missed message self-heals: the next one states where the author already is. The one exception is a repeat of a call whose sale was withheld (late or chased, below): that repeat is not a duplicate to ignore. It is the author trying again, and it retries the sale if the repost itself is fresh.
Most callers never state a size at the trimming stage: they write "taking a
trim here", not "selling a third". A vague instruction has no number in it, so
acting on it would mean executing a quantity the model invented, and EdgeProof
never does that. Instead, TRADING_VAGUE_TRIM_FRACTION (e.g. 0.33) lets you
decide the size once, in config: any sizeless trim sells that fraction of
the position, and the alert notes the default was used. Left unset (0),
sizeless trims execute nothing: you get a notification so you can act
manually, and your rules stay in charge. A stated gain ("340c up 50%") is not a
trim and EdgeProof does nothing with it.
Rules that still hold
- Your stop always fires. A channel instruction can close a position earlier than your rules would, never later, and can never stop your take-profit, stop or time limit from firing on whatever remains.
- It can only reduce. This path sells; it can never open or add. The worst case is exiting earlier than you would have, not new exposure.
- Only positions you actually hold. An exit naming a contract that is not in your account is ignored, exactly as an add-on would be: the position is a fact about your account, not a claim in a message.
- Stale calls degrade instead of dropping. Using the same
TRADING_MAX_SIGNAL_AGE_MINUTESwindow as entries: the author's de-risk stage still advances and the trailing stop still tightens, but the sale itself is withheld. An hour-old "all out" can refer to a move that has since reversed, so EdgeProof declines to sell blind into it, while still acting on the one part of the message that's still true: that the author wants less risk on. - A trim sells at least one contract, and never flattens a multi-contract position. Trims round down and never sell more than the stated proportion, but a fraction that rounds to zero (a third of 3 contracts) still sells one: the author reduced, so EdgeProof reduces.
- A single-contract trim closes the position, but only in profit. Default sizing buys one contract of most spreads, so "down to half" often has nothing to divide, and selling the minimum is a full close. Holding the full position there would leave you more exposed than the author who just took size off, so EdgeProof banks it instead. At a loss it does nothing: there is nothing to lock in, the author kept a position rather than flattening, and your stop is the right mechanism.
- A failed exit is not fatal. If the order is rejected the position stays open and your own rules continue to govern it; you get an alert.
Late or chased exits
Two situations withhold the sale on an otherwise-valid exit call while still recording the author's stage and tightening your stop, degrading gracefully instead of either blindly executing or silently dropping the message:
- Late. A call older than
TRADING_MAX_SIGNAL_AGE_MINUTESno longer triggers a sale, but the de-risk stage still ratchets forward and the trailing stop still tightens, so the position is left safer even though nothing was sold on a possibly-reversed move. - Chased. When the author states an exit price ("out at $5"), EdgeProof
checks the live bid before selling.
TRADING_EXIT_CHASE_PCT(default0.10) sets how far the bid can fall below the stated price before that happens, on by default once channel exits are followed. If the bid has already fallen more than that fraction below the author's price, the fill they described no longer exists: selling anyway would lock in a worse price than their own call, so the sale is withheld while state and stops still update.TRADING_EXIT_CHASE_PCT=0disables this check entirely: the sale proceeds with no bid check, same as before this guard existed. If the live quote can't be fetched, the guard does not block the sale: it is protective, not gating. (A quote that returns a zero bid still withholds: selling at market into no bid is exactly what the guard exists to prevent.)
Either way the de-risk stage still moved forward, so EdgeProof remembers that
a sale was withheld at that stage. The next stage the author posts (a trim
followed later by "all out") is evaluated fresh against its own timestamp and
price, and sells normally if that later call isn't itself late or chased. But
a repost of the SAME stage (the author saying "all out" again) is not
treated as a no-op duplicate: a withheld flat executes if the author re-posts
while the signal is fresh; otherwise your own rules close the position. A
repost that is itself late or chased is withheld again, the same as the first
time, and stays eligible for the next repost. TRADING_MAX_SIGNAL_AGE_MINUTES
and TRADING_EXIT_CHASE_PCT gate the retry exactly as they gate a first
attempt.