37 Commits
Author SHA1 Message Date
Brandon LiandClaude Opus 5 36c5b69550 Add session handling, waitFor gating, and the Tradeify purchase flow
Step vocabulary gains `waitFor`: block until a selector exists, then continue.
Nothing is clicked or typed — it is a gate for conditions something outside the
run has to satisfy. Unlike every other step it carries no signed-out guard,
because the things worth gating on often sit on the login page, where that guard
would abort the run at exactly the wrong moment. Honours the dashboard's Stop
button, since a two-minute gate that ignored it would be worse than no gate.

Session handling. A run that lands on the login page must not continue: once
redirected, every selector resolves against a login form, so a click aimed at
"Add Account" hits whatever that form renders in the same place. Runs now detect
the redirect and stop before sending any input, with a distinct SignedOutError
rather than a generic failure.

Two ways out of that state, in order: a firm's `authSteps` run and the failed
step is retried, or — when none are defined — the run pauses for
signedOutWaitSeconds so a human can sign in, then resumes. Auth steps are
verified rather than trusted: they can all "succeed" while the site still
rejects the sign-in, so the session is re-checked before the retry, and the run
stops with "auth steps ran but the session is still signed out" if it did not
take.

That check polls for up to 20s instead of reading once. Submitting a login form
starts a network round trip and then a redirect, so the tab still shows the
login URL for a second or two afterwards; checking immediately failed a sign-in
that was merely in flight, killing run #15 nine seconds after it had actually
worked. Third instance of the same mistake in this system — reading page state
immediately after an action that triggers async navigation.

The runner reports its version in the heartbeat and the dashboard blocks the
buttons when it is behind. A running Python process does not reload when the
source changes, so a stale runner fails on step types it predates; that cost a
debugging round when a navigate step reached a runner that had never heard of
one.

lib/automations.ts carries the Tradeify buy-accounts flow: navigate to the
dashboard, open Add Account, pick the account type and size, enter the account
name, and work through the challenge widget before submitting. Selectors are
authored by hand against the live page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 16:33:25 -05:00
Brandon LiandClaude Opus 4.6 f6b2ee27fd Use cycle net target (not profit target) for consistency cap
The consistency cap was using profitTarget × consistency, which is
correct only when starting equity = 0 (fresh stage). After a loss within
a cycle, the cap should still respect the cycle's intended net total.

Now computes:
  cycleStartEquity = equityProfit − tradingProfit (constant per cycle)
  cycleNetTarget   = profitTarget − cycleStartEquity
  maxConsistencyDay = cycleNetTarget × consistency

This way prior losses don't expand the daily cap. For PAAPEX stage 2
with -$3000 day 1 and $7100 target: today's cap = $1421.95 (50% of the
cycle's $2843.90 net target), preserving consistency at exactly $7100.

Stage 1 behavior unchanged (cycleStartEquity = 0 → cycleNetTarget = profitTarget).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-04 18:21:14 -05:00
Brandon LiandClaude Opus 4.6 3da96fe73a Retry requestReport on 5xx/network errors with backoff
Tradovate's report server occasionally returns 502, causing accounts
like PAAPEX5776400000019 to end up with empty data when both Cash
History and Fills fail. Now retries up to 3 times with exponential
backoff (500ms, 1s, 2s) on transient errors before falling through to
the existing 5-min outer retry.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-03 17:01:48 -05:00
Brandon LiandClaude Opus 4.6 e6a83eed2f Use max(maxDay, profitTarget × consistency) for Day 2+ cap
Replaces min() with max(). Both bounds need to be respected on different
sides:
  - Below profitTarget × consistency: cap allows growth up to that ceiling
  - Above profitTarget × consistency: consistency is already broken at target,
    cap must equal maxDay (going higher creates a new maxDay requiring
    even more total to satisfy consistency)

For TDFYSL50724548525 ($3000/40%, maxDay $36.96): target = $1200
For a day-1 $1500 win on same config: target = $1500 (must grow to $3750)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-28 03:44:35 -05:00
Brandon LiandClaude Opus 4.6 7b27de6d61 Speed up copyTrade symbol resolution + add entry/progress logs
The symbol resolution loop iterated every firm × every enabled symbol
calling findFrontMonthContract (Tradovate + Yahoo APIs), causing the
endpoint to hang for 30+ seconds with no logs. Now only checks the
client that actually holds the position.

Added logs at function entry and after each major resolution step so
hangs are visible in stdout.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 00:41:07 -05:00
Brandon LiandClaude Opus 4.6 d6344061b6 Add per-account skip logging to copyTrade
Surfaces exactly which gate caught each ineligible account during
copy-to-max so we can debug which filter is excluding accounts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 00:33:21 -05:00
Brandon LiandClaude Opus 4.6 95f9e550d1 Stage pill colors scale from light gray to dark blue
Stage 1 = bg-slate-100 (matches Flat status pill), each subsequent stage
gets a deeper blue. Shared helper in lib/stage-colors.ts used by both
the main dashboard and the account detail page.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-26 03:10:20 -05:00
Brandon LiandClaude Opus 4.6 536aad27ef Show stage pill next to profit target on dashboards
State API now returns stage = 1 + number of withdrawals. Both the main
dashboard and the account detail page show a small Stage N pill next to
the profit target value.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-26 03:02:02 -05:00
Brandon LiandClaude Opus 4.6 009f7471c2 Count only days that hit minDayPnL toward trading day requirement
The state API was returning client.daysTraded (any non-zero day) as the
displayed daysTraded. Firms count only days that hit minDayPnL toward
the min trading day requirement.

When cfg.min_day_pnl > 0, filter dailyPnL by that threshold and use the
filtered count. Otherwise fall back to client.daysTraded.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-26 02:57:14 -05:00
Brandon LiandClaude Opus 4.6 c589adec91 Cap consistency at profitTarget × consistency
Caps consistencyCap at the largest day the consistency rule would have
allowed (profitTarget × consistency). Prevents previous overshoots or
losses from expanding future targets beyond what consistency permits.

Day 1: min(remaining × consistency, profitTarget × consistency)
Day 2+: min(maxDay, profitTarget × consistency)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-26 02:54:03 -05:00
Brandon LiandClaude Opus 4.6 8204cd138b Show stage-aware + consistency-adjusted profit target on dashboards
State API now returns effectiveProfitTarget = max(stage profit target,
maxDay/consistency). When a big day forces the consistency rule, this
reflects the actual amount needed to complete the stage — not just the
base profit target.

Main dashboard and account detail page now display this instead of the
raw cfg.profitTarget.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 02:20:11 -05:00
Brandon LiandClaude Opus 4.6 9928f1cabd Add consistency shortfall check to computeDailyTarget
When a big day has been made, total trading profit must be >= maxDay/consistency
for the consistency ratio to be satisfied. Previously the function only
checked equity vs profitTarget and would coast on min-day even when
consistency was still violated.

Now the remaining = max(equityShortfall, consistencyShortfall).

Per-stage: dailyPnL is already filtered to post-withdrawal entries by
filterActivePnL, so tradingProfit, qualifyingDays, and maxDay naturally
scope to the current stage.

Example PAAPEX5466170000038 (consistency 50%, maxDay $3307.60, total $3306.05):
- equityShortfall = 0 (equity way above $6600 target due to $150k deposit)
- consistencyShortfall = $6615.20 - $3306.05 = $3309.15
- Target now aims for $3309.15 spread across remaining days, not $350 min-day

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 13:53:18 -05:00
Brandon LiandClaude Opus 4.6 670ee9fbeb Require equityProfit; return null when balance invalid
computeDailyTarget now requires equityProfit (amount - accountSize) and
returns null when it's undefined/null/NaN. 0 is still a valid value.

- Removed totalProfit parameter (was only used as fallback)
- Callers handle null by skipping the account (eligibility) or throwing
  (execution paths)
- State API sets dailyTarget to null when no valid balance, avoids
  incorrect targetHit computation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 20:00:52 -05:00
Brandon LiandClaude Opus 4.6 b7430df16e Rewrite computeDailyTarget with stage-first structure
Replaces the tangled first_day/consistency/min-day branches with a
cleaner flow:
1. If profit target met, coast on min-day (or nothing)
2. Compute cappedByFuture (reserve future min-days)
3. Compute consistencyCap:
   - Day 1 of cycle: remaining × consistency
   - Day 2+: current maxDay
   - 0/100% consistency: no cap
4. Combine and floor at minDayPnL when mandatory days remain

Fixes a bug where Stage 2+ Day 1 used the full profitTarget × consistency
instead of remaining × consistency, allowing day 1 to exceed 50% of
cycle-local profit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 19:47:26 -05:00
Brandon LiandClaude Opus 4.6 fa2aad38cb Use actual first-trade timestamp for same-day withdrawal filtering
Instead of the 9 AM CT heuristic, compare the withdrawal timestamp
against the day's earliest trade timestamp. If trades happened AFTER
the withdrawal, those trades count toward the new cycle.

Falls back to the 9 AM heuristic when first-trade timestamp is
unavailable (e.g., cache-only fallback path).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 20:30:41 -05:00
Brandon LiandClaude Opus 4.6 91a448750c Cap consistency branch at maxDay; restore consistency cap in min-day
When consistency is between 0 and 1 (exclusive) and profitTarget not met,
the daily target should cap at maxDay (can't exceed the current max without
breaking the consistency ratio). The previous 'needed' calc could return
less than maxDay, stalling progress toward profitTarget.

Also restored Math.min(baseAmount, cappedByFuture) in the min-day
reservation so consistency is enforced when both mandates are active.

Traces:
- 0 days, $11111 target, 50%: first_day $5555.50, cappedByFuture $10511
  -> consistencyCap = min($5555.50, $10511) = $5555.50 ✓
- 3 days, $5946.44 equity, maxDay $2415.20, 50%: baseAmount $2415.20,
  cappedByFuture $5014.56 -> min = $2415.20 ✓

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 20:22:33 -05:00
Brandon LiandClaude Opus 4.6 c0d60dfe8d Simplify Mode A to compare equityProfit against profitTarget
Previously Mode A computed effective target = profitTarget - (priorProfit
+ totalWithdrawals), which produced inflated targets after withdrawals.

Since computeDailyTarget now uses equityProfit (amount - accountSize)
directly, Mode A just needs to return the base profitTarget unchanged.
The equity-based comparison naturally handles 'get back to same target
above accountSize' semantics.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 20:16:51 -05:00
Brandon LiandClaude Opus 4.6 9c2b2278eb Fall back to base minTradingDays when stage doesn't specify one
When a withdrawal stage has minTradingDays unset (0 or missing), use
the account's base minTradingDays instead of overriding to 0. This
was causing min-day reservation to skip and return consistency-based
targets instead of the min-day floor.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 20:14:52 -05:00
Brandon LiandClaude Opus 4.6 cd0f1e6fac Use equity-based profit for profitTarget comparison
Instead of relying on dailyPnL sum (which can miss reports or be out
of sync with account balance), use (amount - accountSize) as the actual
profit when comparing against profitTarget. Consistency calc still uses
dailyPnL totalProfit for realTarget.

Also unconditionally floor the min-day reservation at minDayPnL — if
equity + (days × minDay) >= target, we coast on min-day.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 20:08:16 -05:00
Brandon LiandClaude Opus 4.6 1dbaebb7d8 Prioritize profitTarget over consistency in min-day reservation
The min-day block was capping the target at baseAmount (consistency-derived)
via Math.min(baseAmount, cappedByFuture), which could produce a target too
low to hit profitTarget with min-day reservations for remaining days.

Now uses cappedByFuture directly so the target always stays on track to
hit profitTarget when there are remaining mandatory days. Consistency can
still be enforced via the firm's consistency setting (1.0 = no cap).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 20:01:09 -05:00
Brandon LiandClaude Opus 4.6 255cf2dd83 Stop consistency calc once profitTarget is met
When totalProfit >= profitTarget, set baseAmount = 0 so the min-day
reservation block takes over. This returns effectiveMinDay ($150) for
remaining mandatory days instead of climbing toward maxDay/consistency.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 19:49:44 -05:00
Brandon LiandClaude Opus 4.6 c63be0d3e2 Target profitTarget in addition to consistency realTarget
computeDailyTarget was only solving for consistency (realTarget =
maxDay / consistency). When realTarget < profitTarget, the function
would stop trading before hitting the actual profit goal.

Now uses Math.max(realTarget, profitTarget) - totalProfit as the
remaining needed, capped at maxDay for the daily target.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 19:40:27 -05:00
Brandon LiandClaude Opus 4.6 c3ec477c50 Treat Manual Adjustment cash changes as fund transactions
Some firms record withdrawals as "Manual Adjustment" instead of
"Fund Transaction" in the Cash History. Include both types so the
withdrawal-aware cycle filtering still works.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 16:59:22 -05:00
Brandon LiandClaude Opus 4.6 99e473c687 Block runTrade when positions are open on restart
The POST handler calls runTrade() directly, bypassing the scheduler
tick's position gate. If you stop and restart with open positions,
other accounts could get new trades while existing ones haven't closed.
Now runTrade itself checks for open positions before proceeding.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 03:55:41 -05:00
Brandon LiandClaude Opus 4.6 58628e0ce1 Fix daily target dropping below minDayPnL after min days met
Math.max(baseAmount, effectiveMinDay) ensures the floor is always
enforced when minDayPnL > 0, not just inside the min-day reservation block.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 17:18:39 -05:00
Brandon LiandClaude Opus 4.6 7afc5b9437 Simplify contract resolver: use Yahoo's shortName directly
Instead of generating candidates from Tradovate and matching by price,
now parses Yahoo's continuous contract shortName (e.g. "Gold Jun 26")
to determine the exact month/year, constructs the Tradovate name
(e.g. "GCM6"), and looks it up directly. Falls back to Tradovate
suggest if Yahoo is unavailable.

This eliminates all price comparison, volume comparison, and the
rollcontract API calls entirely — Yahoo already knows the active month.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 18:24:41 -05:00
Brandon LiandClaude Opus 4.6 24cfa7efcc Fix contract resolver picking wrong month for adjacent contracts
Adjacent futures months (e.g. 6EK vs 6EM) have nearly identical prices,
so the first-match-within-tolerance approach picked the front month
(6EK/May) instead of the actual active contract (6EM/June). Now picks
the candidate with the closest price to the continuous contract instead
of breaking on the first match within 0.1% tolerance.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 18:22:11 -05:00
Brandon LiandClaude Opus 4.6 4025ed2f41 Fix contract resolver: use Yahoo continuous contract price matching
Replace volume-based contract selection with Yahoo Finance continuous
contract price matching ({PRODUCT}=F). The old approach compared volumes
across front/roll candidates, which failed when serial months had
deceptive volume (6EJ26 > 6EM26) or Yahoo was rate-limited (all 0s).

Now fetches the continuous contract price and matches it against
candidates within 0.1% tolerance. Falls back to roll1 if Yahoo fails.
Also adds User-Agent header to avoid 429 rate limiting.

Verified: GC=F price matches GCM26, 6E=F price matches 6EM26.

Also fixes stale 'Random' comment in auto-trade.ts and cleans up
frontVolume/rolledVolume references from settings page.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 18:19:51 -05:00
Brandon LiandClaude Opus 4.6 9a24692c1a Resolve symbol from positioned contract in copy-trade
Instead of relying on the scheduler state (which may be 'Auto'), look up
each enabled symbol's front-month contract and match by contractId to
determine which symbol the positioned accounts are actually trading.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 02:55:18 -05:00
Brandon LiandClaude Opus 4.6 df793bfd70 Fix consistency bug, rename Random to Auto, add stop-after-all, direction pills, copy-trade
Bug fixes:
- Fix computeDailyTarget when consistency is 0% or 100%: treat as no constraint,
  letting min-day reservation or full remaining profit drive the target
- Rename 'Random' to 'Auto' across entire codebase (types, API, UI, scheduler)

Features:
- Add "Stop after all eligible" checkbox: auto-stops scheduler when all
  configured accounts are dead, inactive, already traded, or challenge complete
- Show position direction in status pill: "Long" (green) / "Short" (red)
  instead of generic "In Trade" (blue)
- Add "Copy to Max" button: copies current trade direction to remaining
  eligible accounts up to max_concurrent_accounts limit

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 02:50:31 -05:00
Brandon LiandClaude Opus 4.6 8d9a9a5ea9 Rename Random to Auto in UI and always show + on last stage
- Display "Auto" instead of "Random" for symbol/action selectors
- Last withdrawal stage always shows "+" suffix (even with one stage)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 17:55:20 -05:00
Brandon LiandClaude Opus 4.6 70b1362d3e Add per-stage withdrawal targets with consistency and min trading days
- Add withdrawal stage system: each stage defines profit target, consistency,
  and min trading days for post-withdrawal challenge cycles
- Target Same Equity mode accounts for withdrawn amounts when computing
  effective profit target (profitTarget - remainingProfit)
- Store fund transaction timestamps for time-aware cycle filtering
  (withdrawals before 9 AM CT include that day in new cycle)
- Expose full P&L history (fullDailyPnL) for calendar/equity curve display
  across all cycles, with DB fallback for pre-restart data
- Show stage number (#1, #2, etc.) on calendar cells
- Hide consistency reference line when consistency is 0% or 100%
- Settings UI: "After First W/D" column with same-equity checkbox,
  expandable stage sub-rows with profit/consistency/days inputs
- Default target_same_equity to 1 for new and existing account configs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 05:27:13 -05:00
Brandon Li c54073e4b8 Throttle request report refreshes and add retries 2026-03-21 04:30:36 -05:00
Brandon LiandClaude Sonnet 4.6 4252adfbea Show fund transactions (W/D) on calendar, equity curve, and cash history table
- Persist fund transactions to SQLite (fund_transactions table) so they
  survive beyond Tradovate's 28-day report window
- Calendar: highlight W/D dates in amber with the amount shown below the day
- Equity curve: reduce running equity at withdrawal dates and show a vertical
  dashed amber line labelled W/D
- New Cash History table below calendar listing all trades and W/D events
  sorted newest-first

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 04:16:24 -05:00
Brandon LiandClaude Sonnet 4.6 6d960ceb7b Reset days traded counter from last fund transaction date
Tracks Fund Transaction entries in the Cash History report to find the
most recent account funding/reset date. Only trading days on or after
that date count toward daysTraded and the daily target calculation.
The last fund date is persisted in SQLite so it survives beyond the
28-day Tradovate report window.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 04:10:02 -05:00
Brandon LiandClaude Sonnet 4.6 6b299f5359 Persist daily P&L beyond 28-day Tradovate window
Adds a daily_pnl SQLite table that accumulates trade history indefinitely.
On each hourly fetch, fresh API data is upserted (not replaced) so entries
older than Tradovate's 28-day limit are preserved. Cache is loaded at startup
and used as fallback when both report requests fail.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 03:40:37 -05:00
Brandon Li 131c9bca9f Use resolved active contracts for orders 2026-03-18 00:52:25 -05:00