There was no way to tell "running, nothing to pull" from "not running". The
no-change path logs nothing by design, and PM2 reports a cron-restart process
as `stopped` with ↺ 0 even while firing on schedule — I verified that against
PM2 7.0.4: a one-minute cron fired four times without the counter moving once.
scripts/.last-check is now rewritten on every run with the outcome. It is a
single overwritten line, so it needs no trimming.
The outcome is set by each exit path and written once in `finally`, rather than
calling record at each return — the first version of this did the latter and
already missed the build-failure path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The clicker had no Linux support at all: pygetwindow has no X11 backend, so
_other_activate returned "window management is unsupported" and every step
failed before it could click. focus.py now has a third backend that raises the
browser with xdotool and reads the focused window's WM_CLASS to verify it came
forward - the same activate-then-confirm shape as the macOS and Windows paths.
setup-linux.sh mirrors setup-windows.bat with apt instead of winget. Three
things are specific to this platform rather than incidental:
- Node comes from NodeSource. Pi OS ships one too old for Next 16, and the LTS
line is also what better-sqlite3 publishes prebuilt arm64 binaries for.
- Python dependencies go in a virtualenv. Pi OS Bookworm enforces PEP 668, so
pip into the system interpreter fails with externally-managed-environment.
- Autostart uses an XDG ~/.config/autostart entry, the direct analogue of the
Windows Startup folder: no sudo, and it runs inside the graphical session,
which the clicker needs for DISPLAY.
Wayland is called out in four places - the session guard, diagnose.py, the
installer and both READMEs - because Pi OS on a Pi 5 defaults to it and the
clicker simply cannot work there. Wayland does not let one client synthesise
input into another, so this is a switch-to-X11 situation, not a bug to fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Seven sections of PowerShell - install Git, install Node, install
windows-build-tools, clone, npm install, build, run - all of which
setup-windows.bat now does, and better. The clone URL in step 4 was stale
anyway, pointing at a repo that no longer exists.
Kept the parts the script cannot do: adding firms through the UI, loading the
extension, and the AutoBuyer notes on foreground lock and display scaling. The
clicker sections now say what setup already handles and how to do it by hand.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One line per account on every sync, which on a multi-firm instance buries
anything worth reading in `pm2 logs autofirmer`. The threshold is still
recorded on the client and still drives the dead-account checks; it just is
not narrated any more.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Capped at 256 KB, trimmed back to the newest 500 lines. Done once per run
before anything is written: the no-change path logs nothing, so that is the
only point at which the file can have grown since the last run. Failures are
swallowed - housekeeping must never be the reason an update does not land.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A run died with "'charmap' codec can't encode character '▶'" at step 1.
Python picks the console code page for stdout, and under PM2 - where stdout is
a pipe rather than a console - that is cp1252 on Windows. cp1252 handles the em
dashes in these files but not the run markers, so the first one raised
UnicodeEncodeError from inside run_steps and the runner reported it as a step
failure rather than an output problem.
Reconfigure stdout and stderr to UTF-8 at the top of each entry point, with
errors="replace" as a backstop for streams that cannot be reconfigured. Fixing
the encoding once beats stripping the glyphs from ~30 call sites, and covers
manual runs in a plain console too.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Register-ScheduledTask failed with Access denied (0x80070005). Writing to the
root task folder needs elevation, so the claim that this install needed no
admin was simply wrong. Rather than demand UAC, use two mechanisms that need
no privileges at all:
- a Startup-folder entry (AutoFirmer.cmd) runs start-all.bat at logon
- PM2's own --cron-restart with --no-autorestart drives the 5-minute update
check, so PM2 owns the schedule it was already going to resurrect anyway
Both still run in the logged-in interactive session, which is the requirement
that ruled out a Windows service in the first place: the clicker sends real
input and needs a desktop.
pm2 save now runs after the updater is registered, so `pm2 resurrect` brings
back all three processes rather than two.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`pm2 delete autofirmer` on a machine that has never registered it writes
"[PM2][ERROR] Process or Namespace autofirmer not found" to stderr. PM2 ships a
PowerShell shim, and under $ErrorActionPreference = 'Stop' any native stderr
becomes a terminating NativeCommandError - so the routine "remove it if it is
there" line killed the install before a single task was registered. The 2>$null
at the call site was useless: the error is raised inside pm2.ps1.
Route every pm2 and npm call through helpers that drop to 'Continue' and judge
by $LASTEXITCODE. npm had the same latent problem, since its warnings also go
to stderr.
Also start Next directly instead of via `npm start`. On Windows npm is a .cmd
shim, so PM2 was supervising the shim while the real server ran as its child -
restarts and stops would have missed the process that actually matters.
The file is now pure ASCII. PowerShell 5.1 reads a non-BOM UTF-8 script as
ANSI, so the box-drawing characters in the comments were a latent hazard.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
playwright was declared but referenced nowhere in the source — no import, no
require, no script. Its postinstall downloads several hundred MB of browsers
on every install, which setup-windows.bat had been working around with
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD. Removing the dependency removes the need for
the workaround, so that goes too.
Nothing else depends on it; the remaining lockfile mentions are Next declaring
@playwright/test as an optional peer, which installs nothing.
Instances pick this up through the normal update path: package-lock.json
changed, so update-check runs npm install and playwright disappears from
node_modules. Any browsers already downloaded on a machine live outside
node_modules and are not removed by this — see README.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
winget installed Python successfully and the very next line reported it
missing. The re-probe only checked `py -3` and `python` on PATH, and a
just-installed interpreter is not on the PATH this session inherited at start
— the same failure Git and Node already had directory probes for. Python
never got them.
Probe the standard install homes before giving up:
%LOCALAPPDATA%\Programs\Python\Python3* (per-user, winget's default)
%ProgramFiles%\Python3* (all-users)
...\Python\Launcher\py.exe, %SystemRoot%\py.exe
Globbed rather than version-pinned so a 3.13 install is found too, and the
loop body uses %%~D: a quoted for /d pattern carries its quotes through, which
would otherwise mangle every path built from "C:\Program Files\Python312".
This also covers Python installed without "Add python.exe to PATH" ticked,
which is the more common way to end up here without winget involved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
A bare `git clone` follows whatever the remote advertises as its default
branch. Pushing --all to a fresh Gitea repo left that default pointing at
autobuyer, so setup cloned a branch predating the build fixes and failed at
`next build` on an error that had already been fixed on master.
The Gitea default is corrected, but the script no longer depends on it:
clone passes --branch master, and the update path fetches, checks out master
and pulls it by name. That also recovers a checkout already stranded on the
wrong branch, rather than needing the folder deleted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Each prerequisite now follows probe -> offer install -> probe again. The
second probe matters: a freshly installed tool is never visible to `where`
in the session that installed it, since the process inherited its PATH at
start. A *_TRIED guard stops the loop at one attempt.
Node installs from the LTS package on purpose - better-sqlite3 publishes
prebuilt binaries for LTS, so this sidesteps the node-gyp compile that the
existing Node >= 23 warning covers.
Python detection runs the interpreter instead of calling `where python`.
Windows ships a stub python.exe under WindowsApps that only opens the
Microsoft Store; `where` finds it but it cannot execute anything. Asking for
sys.version_info distinguishes the two, and the py launcher is preferred
because the stub does not shadow it.
Dependency install now upgrades pip first and verifies pyautogui actually
imports, rather than trusting pip's exit code alone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Git for Windows only adds itself to PATH when "Git from the command line"
is selected during install, and an already-open Command Prompt keeps its
old PATH regardless — so a correct install still failed the check.
Probe the standard install locations before giving up, and when that also
fails, say which directories were searched and call out the just-installed
case explicitly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.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>
Standalone batch file: clones (or pulls), installs, builds, seeds the
reporter settings, and writes a start-autofirmer.bat. Prompts for the master
dashboard URL and instance name, defaulting to %COMPUTERNAME%.
Two install hazards it handles:
- better-sqlite3 has no prebuilt binary above Node 22, so install silently
falls back to node-gyp and dies without Visual Studio Build Tools. The
script warns first and names the fix if install fails anyway
- playwright is declared but referenced nowhere in the source, so its
postinstall browser download is skipped
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
`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>
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>
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>
`button.reset_btn` matched the reset control on any account row; qualifying it
with `.status-failed` keeps the repeat block from resetting healthy accounts.
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>
The file said to work on `main`, but this repo's mainline is `master` and no
`main` exists — which is how a feature branch got created instead.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The DPI awareness call added for Windows went into both entry points, but the
import only went into clicker.py, so starting the runner died immediately with
NameError: name 'focus' is not defined.
py_compile does not catch this — a missing import is a runtime error, not a
syntax one — and the tests around it stub the modules rather than starting the
process, so nothing exercised the real startup path. Verified this time by
booting the runner against a dead port, which reaches the claim loop.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
The popup's target URL pattern was doing two jobs. As a fallback for locate
requests it is now dead — every step carries its firm's pattern. As the thing
deciding which tab gets scraped it was still load-bearing: without it, capture
falls back to whichever tab is active, which means scraping a banking or mail
tab and storing it in the dashboard's database.
So the setting goes, but the scoping moves to the manifest rather than
disappearing. host_permissions already lists exactly the hosts this extension is
allowed to read; capture now queries those (minus localhost, which is the
dashboard mirroring its own output back). The two cannot drift apart, and adding
a firm — which means adding its host to the manifest anyway — scopes capture
without a second place to remember.
With more than one firm open, capture prefers the active tab over the first
match, so it follows attention rather than tab order. That case could not arise
while a single pattern matched one site.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
The daemon indicator sat in the firm tab row, which put it next to the firm
tabs it has nothing to do with. It belongs with the page capture switch: both
describe whether the moving parts outside the browser are alive.
The row now carries host, pid and version while the daemon is up, and the start
command while it is not, so a stopped daemon says what to run rather than only
that something is wrong. The dry-run and out-of-date states move here as chips
beside the status dot.
The warning banners stay above the automation buttons — those are actionable
next to the thing they block.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>