71 Commits
Author SHA1 Message Date
Brandon LiandClaude Opus 5 4288d8c298 Auto-start both processes at logon and self-update from master
Instances were started by hand and updated by hand, so they drifted behind
master silently. Now: PM2 supervises the dashboard and the clicker, a logon
task brings them up, and a 5-minute task pulls, rebuilds and restarts when
master moves.

Restarting on every push is only safe because the scheduler now survives it.
It was pure in-memory state (_global.__autoTrader), so any restart silently
stopped automated trading with the dashboard simply showing it as off. It now
mirrors running/action/symbol/stopAfterAll to the settings table, and
resumeSchedulerIfPersisted() picks it back up from the getClients() bootstrap.
No sync-wait was needed there: tick() already skips while a client reports
!syncComplete and while any account holds a position.

A failed build is never deployed — the build runs before anything restarts, so
a broken push leaves the previous build serving.

start-all and update-check both warm the app with a request afterwards. That is
load-bearing: getClients() is lazily bootstrapped, so until something makes an
HTTP request the Tradovate clients, the reporter and the resumed schedule never
start. That was already true of manual restarts.

Logic lives in Node so a macOS or Linux port only needs an equivalent of
install-autostart.ps1. Python deps are hash-guarded, so the common path is one
hash and one import with no network, and failure is non-fatal since only the
clicker needs them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 17:36:25 -05:00
Brandon LiandClaude Opus 5 d378712e79 Default master_dashboard_url to https://master.juicerroom.com
Fresh installs point at the master dashboard without manual configuration.
Seeded with INSERT OR IGNORE, so existing databases are untouched.

Note that reporter.ts requires both master_dashboard_url and instance_name
to be non-empty, so this alone does not start reporting — instance_name is
still seeded blank and set per machine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 16:48:38 -05:00
Brandon LiandClaude Opus 5 3bef7dea9e Seed only NQ, GC and CL as enabled instruments
The other 17 symbols still seed, so they remain listed and can be switched
on from the Instruments page — they just start disabled.

Affects fresh installs only: the seed block runs only when the instruments
table is empty, so existing databases keep their current selection.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 16:16:06 -05:00
Brandon LiandClaude Opus 5 fc08a41c4b Fix seven type errors that broke next build
`npm run build` failed on a clean checkout, so nothing on master could be
built for production. `npm run dev` does not hard-fail on type errors, which
is why it went unnoticed.

- state route returned client.perContractFees, which has never existed on
  TradovateClient on any branch; nothing consumed it
- mapFirmConfig omitted bannedSymbols. Type gap only: the trade path calls
  isSymbolBanned() against the DB directly, so bans were always enforced
- initClient's sync callback was sync where the constructor wants
  () => Promise<void>
- accessInfo and ws are assigned during async connect/auth, never in the
  constructor, so they take definite-assignment assertions
- the socket payload's inline entityType union had drifted five members
  behind the indirect-callback union above it, making the 'position' and
  'cashBalance' branches unreachable to the compiler. Both now share a
  TradovateEntityType alias. Type-only: those handlers ran fine at runtime

Behaviour is unchanged throughout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 16:16:06 -05:00
Brandon LiandClaude Opus 5 3cc7ddcc5c Add scrollToLoad and an absent variant of waitFor
scrollToLoad walks a progressively-loading list to the bottom before the steps
that act on its items run. Stopping is two-part: no new matches appeared AND the
container was already pinned to the bottom — counting alone stops early on a slow
fetch. Hitting the scroll cap is reported rather than passed off as done, so a
later step never works quietly on a partial list.

The scrolling element is usually not the window. Lists like this live in a div
with its own overflow, and scrolling the document does nothing at all, so the
step walks up from a matched item to the ancestor that actually scrolls —
overflow allows it and there is more content than fits — with containerSelector
to name one outright when the guess is wrong. Verified against a page whose
document also scrolls, which is the case that tells the two apart: it found the
inner div and pulled 12 items up to 60 in 7 scrolls.

waitFor gains `absent`, for waiting on something to go rather than arrive — a
modal closing after a reset. It only accepts a genuine "selector matched
nothing"; an unreachable extension looks the same from a distance and would
otherwise satisfy the gate for the wrong reason, sending the next iteration into
a page that still has the modal open.

The locate queue carries a free-form options blob now, so a new kind of request
stops meaning a new column each time.

Also fixes a missing comma in the reset flow that broke the build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 14:31:36 -05:00
Brandon LiandClaude Opus 5 3ac9fe060f Raise the browser the extension reported, and add a diagnostics script
focus.py raised the first browser in its list that happened to be running. On a
machine with both Chrome and Edge installed that is a coin flip, and losing it is
silent: coordinates measured from a tab in one browser, the click delivered into
a window of the other. It presents as selectors failing for no reason. A VPS with
both installed hit exactly this.

The extension now reports which browser is hosting it, and that travels with the
measurement, so the clicker raises the browser the coordinates actually came
from. Asking for a browser that is not running now fails honestly instead of
quietly raising a different one, and the verification step rejects the wrong
browser coming forward. Chromium, Opera and Vivaldi are recognised alongside
Chrome, Edge and Brave, on both platforms.

diagnose.py answers the question a remote desktop makes hard: whether the mouse
is really moving or the viewer simply is not drawing it. It moves the cursor and
reads the position back from the OS, so the answer does not depend on anything
being rendered, and it reports DPI mode, screen size, whether this is an RDP
session, and whether the browser can be raised at all. Nothing is clicked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 14:13:18 -05:00
Brandon Li 731fafda0e Scope the reset button to failed accounts
`button.reset_btn` matched the reset control on any account row; qualifying it
with `.status-failed` keeps the repeat block from resetting healthy accounts.
2026-08-30 14:13:18 -05:00
Brandon LiandClaude Opus 5 3ff5728af9 Survive transient dashboard errors instead of failing the run
Next's dev server intermittently answers a 500 while recompiling a route: it
reads a build manifest mid-write and cannot parse it. A single one of those
during the locate poll was fatal, so a blip in the pipeline killed a run partway
through an auth flow on Windows.

5xx responses and dropped connections are now a distinct TransientError, retried
until the step's own timeout. A 4xx still fails immediately — those are verdicts
about the request, not blips. If the errors persist all the way to the timeout,
the message says so rather than blaming a missing extension.

Error bodies are also summarised. A dev-server 500 replies with a full HTML page,
and printing it raw buried the one line that said what went wrong under kilobytes
of script tags.

This makes the client tolerant of the fault, which is not the same as fixing it:
the real answer on an automation host is to run a production build rather than
`next dev`, so those manifests are written once instead of continuously.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 14:00:23 -05:00
Brandon LiandClaude Opus 5 37adbf67fc Document Windows support and the AutoBuyer components
focus.py now verifies on Windows rather than assuming activation worked. Windows
declines to raise a window for a process that doesn't own the foreground — it
flashes the taskbar and the call returns as if it succeeded — so the runner reads
the foreground window's process back and reports a failure instead of clicking
into a background window. Same gap that was fixed on macOS earlier.

The runner also declares itself DPI-aware at startup. Without it Windows reports
a virtualised screen size and rescales the coordinates it accepts, while the
browser keeps reporting CSS pixels; on a display at 125% or 150% the two disagree
and clicks drift further off the further they are from the top-left.

Browser windows are matched on the owning process rather than the window title, so
an editor with chrome.js open is no longer mistaken for the browser. Linux now says
window management is unsupported there, rather than reporting no browser found —
pygetwindow has no X11 backend, and "no browser window" reads like Chrome is shut.

The main README gained a section on the AutoBuyer: what the three pieces are, how
to load the extension, and that neither the extension nor the runner reloads
itself when the source changes. That last point has been the cause of most of the
confusing failures so far, so it is called out in Updating too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 13:36:47 -05:00
Brandon LiandClaude Opus 5 22db6eae8d Add repeat blocks, per-run inputs, skipIfNotFound, and orphaned-run recovery
Repeat. A `repeat` block runs its steps several times, with the count either
fixed in the config or taken from an input the user sets on the dashboard. The
block is unrolled in resolveSteps before the runner sees it, so the runner needs
no loop, the run's total step count stays honest, and every iteration appears in
the log as its own line — a failure on the third purchase reads as "(3/5)"
rather than as an indistinguishable repeat of the first.

Counts are clamped server-side against the automation's declared min/max, and
expansion is capped at 400 steps and three levels of nesting. Each iteration can
be a purchase, so the number is not taken on trust from the client, and the
confirmation dialog names it before anything runs.

skipIfNotFound on a click or type step tolerates an element that is not on the
page — a cookie banner, a modal that only sometimes appears. Only absence is
tolerated. That distinction needed a new NotFoundError: previously a missing
element, an unreachable dashboard, a missing tab and a covered button all
surfaced as the same DashboardError, and skipping that whole class would mean a
step quietly passing while the extension was down.

Orphaned runs are now reaped. Only one run executes at a time, so a run left in
'running' when its runner went away blocked every future run — restarting the
daemon mid-run deadlocked the queue, which is exactly what happened. The
heartbeat decides: a runner that is gone, or up and reporting idle, is not
driving that run whatever the status column says. Gated on the busy flag rather
than elapsed time alone, since a run sitting in a waitFor gate or a sign-in wait
can legitimately go minutes without progress.

Lucid Trading is scaffolded with no automations yet. One match pattern covers
both its hosts — `*.` matches the apex as well as subdomains, confirmed against
a live tab. Its signed-out pattern is `//lucidtrading.com/` rather than
`lucidtrading.com/dashboard`: the leading slashes anchor it to the start of the
host, and without them the substring also matches dash.lucidtrading.com, which
would abort every step while properly signed in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 13:22:45 -05:00
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 5 b748f95372 Add automation framework, typing, and runner for the autobuyer
Turns the autobuyer from a page scraper into something that acts. A dashboard
button queues a run; a desktop process executes it against the real browser.

lib/automations.ts — automations are declarative step lists nested inside the
firm whose site they drive. Steps are click / type / wait / navigate, and they
inherit the firm's tab pattern and URL, so one firm's automation can't act on
another's tab. Adding a button means adding an entry here; the page renders
buttons from the API and the runner receives steps from the server, so neither
needs editing. Runs key on firm:automation — every firm will plausibly have its
own "buy-accounts", and a bare id would resolve to the wrong one.

clicker/runner.py — the daemon behind the buttons. Claims a queued run, works
through the steps, reports each one back for the page's live log. Only one run
executes at a time: two processes driving one physical mouse would interleave
clicks. Heartbeats on its own thread, because a step can block for tens of
seconds and folding the beat into the main loop would show the runner as offline
in the middle of the run it was executing.

clicker/actions.py — one implementation of the safety checks, shared by the CLI
and the runner. Refuses to act when the element is covered by an overlay, when
coordinates fall off-screen, when the browser can't be confirmed frontmost, or
(for type) when the target isn't an editable field.

Typing: uneven human cadence, and the field is read back afterwards and compared
against what was typed — a field that never took focus fails silently and looks
identical to success otherwise. Non-ASCII is rejected because pyautogui skips
those characters without complaint, and newlines because Enter may submit the
form. Typos are deliberately not simulated: a mistyped digit in a trading form
is a real loss, and the correction is the part that can go wrong.

Extension: opens the firm's page when no tab matches, navigates to a specific
page for a navigate step (skipped when already there, so page state survives),
and retries the locate while a freshly loaded React app mounts — `complete` only
means the document loaded.

Staleness reporting, after it cost three debugging rounds: Chrome doesn't reload
an unpacked extension and Python doesn't reload a running process, so both now
report their version. A stale runner gets a red banner naming both versions and
the automation buttons are disabled, rather than failing mid-run on a step type
it predates.

Scale detection is now conservative: a raw OS/browser width ratio is only
trusted when it lands on a real scaling factor. On this multi-monitor desktop
the previous logic would have silently halved every coordinate.

Verified end to end against the live browser: navigate, locate, and a real
click (run #12, all three steps). API round-trips, claim-once semantics, run
cancellation, the heartbeat online/offline lifecycle, motion geometry and
timing, focus activation, and typing verification all pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 14:00:33 -05:00
Brandon LiandClaude Opus 5 54221bbc0c Add autobuyer page capture, browser extension, and desktop clicker
Builds the pipeline the autobuyer needs: see the page, find an element,
click it.

extension/ — MV3 Chromium extension. Polls /api/autobuyer/status and,
while on, scrapes the target tab's HTML and posts it back. Also serves
locate requests: focuses the window, scrolls the element into view, and
reports its position. host_permissions is scoped to tradeify plus
localhost so it cannot read other sites — an empty target pattern would
otherwise capture whatever tab happened to be active, including banking
or mail.

app/api/autobuyer/ — status toggle, capture store, and the locate request
queue. CORS is open because the extension's origin changes every time an
unpacked extension is reloaded.

app/autobuyer/page.tsx — ON switch, source view (default) and a rendered
view. The render uses sandbox="allow-scripts" without allow-same-origin:
the page's own JS is needed because sites ship content at opacity:0 and
fade it in, but the frame must not reach the dashboard's same-origin API
routes, which serve firm credentials.

clicker/ — Python CLI. Asks the extension where a selector is, adds the
element rect to the window's screen position and the browser chrome
height to get desktop coordinates, then clicks with a human motion model
(curved path, eased velocity, occasional overshoot, dwell before press).
Raises the browser application first, since macOS consumes a click on an
unfocused window rather than delivering it.

Refuses to click when the element is covered by an overlay, when the
coordinates fall off-screen, or when the browser cannot be confirmed
frontmost.

Verified: API round-trips, capture pruning, locate claim-once semantics,
motion geometry and timing, and focus activation — the last two against
stubs, since pyautogui and pyobjc are not installed here. NOT verified
end to end: Chrome is still running a stale build of the extension, so a
locate request has never completed against a real page and no real click
has been sent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 16:15:18 -05:00
Brandon LiandClaude 24a54c6642 Treat Challenge Payout cash changes as fund transactions
Some firms record payouts as "Challenge Payout" in the Cash History
rather than "Fund Transaction" or "Manual Adjustment". Without this the
payout was summed into that day's P&L (a -2,000 payout turned a +1,416
day into -584) and never reset the cycle, so daysTraded kept counting
and the stage never advanced.

Verified against a captured report: the payout is now recorded as a
fund transaction, excluded from daily P&L, and because its timestamp
precedes the day's first trade the day's trades land in the new cycle.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-21 18:34:25 -05:00
Brandon LiandClaude Opus 4.7 e37c39a7d1 Pick active futures contract by Yahoo volume
Replaces the Yahoo continuous-contract month parse with a volume-based
probe: walk the next 6 month codes via Yahoo's specific tickers
({PROD}{MONTH}{YY}.{EXCHANGE}) and pick the one with the highest recent
volume. Tracks the trader-standard "front month" definition and rolls
correctly even when the continuous (=F) feed lags expiry.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-19 01:01:17 -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 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 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 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
SenofyandClaude Opus 4.6 f498594b16 Add configurable trading hours setting
Dropdown on settings page with two options:
- Full CME (5:00 PM – 3:00 PM CT) with 5 min buffer
- Equity Hours (8:30 AM – 3:00 PM CT) with 5 min buffer
Setting is read live each tick, no restart needed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 19:57:48 -05:00
SenofyandClaude Opus 4.6 11144612c7 Block trading after 3 PM Central instead of 4 PM
Gives a full hour buffer before the CME daily halt at 4 PM.
No-trade window is now 3:00 PM – 5:00 PM Central (Mon–Thu),
and after 3:00 PM on Fridays.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 14:49:49 -05:00
SenofyandClaude Opus 4.6 c99f6843a7 Fix contract resolver: prefer rolled contract on equal volume
Changes > to >= so when both contracts have 0 volume (e.g. off-hours),
the further-out standard month wins. Fixes SI resolving to SIJ (April,
non-standard) instead of SIK (May, standard delivery month).

Also fixes SI/SIL prefix collision by requiring month+digit after symbol.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 03:39:11 -05:00