Commit Graph
82 Commits
Author SHA1 Message Date
Brandon LiandClaude Opus 5.5 dc210ffcb7 Vest: no leg opens past 8 PM ET until a sync has read the new day
The engine's Vest day now turns with the client's numbers, at the first
sync after the reset. A leg opened between 8 PM ET and that sync would be
planned and counted on the day that ended, so both the pick and the open
wait for it. The claim log's "stage N next" counts payouts the way the
engine picks its stage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 16:10:16 -05:00
Brandon LiandClaude Opus 5.5 e02838409b Vest: a claim names the completed cycle it ends, a day's second and one after the reset too
Two of the cycles a claim ends never got a completion entry. A Vest
day's second cycle is one: the auto-claim at the next tick beats the 30 s
report, and the payout books into that day's fund row, so the latest fund
date never moves for the catch-up. A cycle claimed right after the 20:00
ET reset is the other: the reset puts it back on stage 1, and all of its
P&L sits in the previous claim's day row, so endedCycleCompletion reads
none of it.

Before a claim goes out, claimedCycle (trading-logic) checks whether the
cycle stands complete: cycleCompletion 'today' or 'before' on the
client's numbers. It keys the cycle the way the final day would
(completionPlace on the payout list), so the master dedupes the two.
afterClaim keeps the result in account_meta. The reporter names each kept
cycle for the catch-up's week. It lists them last in the slice, newest
first, under the master's per-slice cap, and adds no counter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 16:08:17 -05:00
Brandon Li 9ecb584dbc Merge branch 'vest-fix-intent' into vest-fixes 2026-10-27 14:51:36 -05:00
Brandon LiandClaude Opus 5.5 ef15200e5e Vest: report on Vest's day, and close a leg whose open came back unknown
The reporter and the dashboard take each firm's own trade date
(tradeDateFor), as the engine does: between 5 PM CT and Vest's 8 PM ET
reset the Central date counted the open Vest day as a prior day and as
today's P&L both, and could report a pass the master keeps for good. The
completion check's settle window now follows the client's own roll.

An open whose outcome came back unknown is marked the bot's once a read
shows the position it made, so the market-close guard closes it before a
long closure; an unmarked leg is no longer logged as a hand trade.

A symbol the close gate holds back stays on the candidate board for the
dashboard and the report, and fireLeg holds each account's leg to the
close at its own size, not just the batch lead's. check-vest-client.mjs
drives the guard and the board with every gateway call answered locally.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 14:41:20 -05:00
Brandon LiandClaude Opus 5.5 378530595e DXtrade: an entry never closes a hand trade, and a flatten never opens one
DXtrade dropped the intent the engine now passes, so on the live
Tradeify247 accounts an entry could still close a hand position as the
"entry" and the leg's flatten then OPEN a position with no exits. The
open/close decision moves out of vest-helpers into platform-client's
orderEffect, which both clients now follow to pick OPEN or CLOSE.

A flatten's flat read is "nothing to flatten" only when the client's own
record agrees. One empty read just after a fill the client saw may be the
gateway lagging, so it stays a plain refusal and keeps the FLATTEN FAILED
alarm. A check now fails if an engine order drops its intent or a client
stops going by orderEffect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 14:13:07 -05:00
Brandon LiandClaude Opus 5.5 ec3e6ea388 Vest: no leg opens into a market close, and accounts plan on Vest's day
Market close: a Vest entry now needs its symbol's market to stay open
for RESET_CLEAR_HOLDS holds of the leg (clearsClose), the clearance the
8 PM ET reset already gets: at the target hold, and at the leg's sized
hold when a leverage cap stretches it. That covers the weekly close and
the daily maintenance hour as Vest's market-hours periods list them.
Before a closure that runs past an 8 PM ET reset (the weekend, a
holiday: longClosure), the bot's own legs still open in the last 5
minutes are closed at market, loudly; a position the bot didn't open is
named in the log and left alone.

Vest's day: Vest accounts are planned, and their legs and entries
counted, on Vest's day (20:00 ET) instead of the Central date
(tradeDateFor, PlatformClient.tradeDate). Between 5 PM CT and the reset
the Central date counted the open Vest day as a prior day and as today's
realized P&L both ($3,000 to go, $800 made: $2,050 left, not $1,900) and
started the leg count over. Other platforms keep the Central date.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 14:12:27 -05:00
Brandon LiandClaude Opus 5.5 e90f21ed91 Vest: an entry never closes a hand trade, and a flatten never opens one
sendOrder takes an optional intent. On Vest an 'open' is refused when the
account already holds the symbol, where it used to close that position as
the "entry"; the leg's exits then found nothing and its flatten opened a
fresh position with no TP or stop. A 'close' only closes or reduces, and on
a flat account is refused with NothingToCloseError, which flattenAndFail
reports as nothing to flatten instead of FLATTEN FAILED. Entries, the
extra-day round trip and flattens pass their intent; the manual order route
and the other platforms keep today's behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 14:03:14 -05:00
Brandon LiandClaude Opus 5.5 beddbdcf6c Vest: stops reserve 0.02% of the position for slippage, not 0.1%
A platform may now state its own stop slippage share
(PlatformClient.stopSlippageFraction); the daily-loss reserve is the
larger of 10% of the limit and that share of the position. Vest's 9
measured stop-outs filled 0.9 points past the trigger on average and
2.25 at worst, against the ~30 points 0.1% held back. On a $1,400 day
with ~$495k of NDX the stop goes from ~$880 to ~$1,230, and the leg
sizes up for the same hold. Sizing, the placed stop, the dashboard's
next stop and the reset guard all use it; other platforms keep 0.1%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 11:34:27 -05:00
Brandon LiandClaude Opus 5.5 f0c1569d96 Vest: a split day's next leg gives back the day's gain plus the limit
On a split day the next Vest leg's daily-loss room now adds the day's gain
still in the account to the bot's limit (floorRoom's `gain`), never past
Vest's own floor, and the leg sizes up for the same target hold (Dingo's
next eval leg: $455k -> $842k at 1.5 h, win 28% -> 56%). Other platforms
are unchanged: dllRoom still never widens on a green day.

The gain only widens a leg all but sure to close before Vest's 8 PM ET
reset (resetClearStop, held to the shortest leg the day may carve), and
none of a hand claim's profit counts. The last-stand margin is measured
against the limit, so a green day can't drop a leg's stop.

Around the reset: no new Vest position opens in the last 7 minutes, and
the guard re-fits every 15 s on fresh positions instead of one pass. An
account's positions share the next-day room (stops first, least at risk
first; locked-in profit counts), a hand-moved stop is fitted again, and a
close or profit-side stop that doesn't take is retried or closed.

The dashboard's next stop now reserves for the leg's notional, as fireLeg
does, so DXtrade labels read smaller (display only).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 07:21:52 -05:00
Brandon LiandClaude Opus 5.5 6f6660fdcc Auto-claim without the scheduler, and no claim twice
A payout locks in profit and opens nothing, so stopping the trading no
longer stops it: a claim watcher runs the same claims once a minute while
the scheduler is off (its tick still claims while it runs), and the
dashboard's Auto-claim payouts box turns them off.

From two independent reviews of the claim path:
- A claim holds every further claim on the account until Vest's
  transactions show its WITHDRAW. A sync already out when the claim lands
  (by sync generation, not the clock) no longer writes the pre-claim
  balance back, and a fresh sync follows the claim.
- A withdraw that fails in transit (timeout, reset, 5xx) may have gone in:
  it is recorded as of unknown outcome and holds claims the same way,
  until the transactions show it, or an hour of them shows it didn't.
- claimProfit checks Vest's own account: funded (or instant), and no more
  than the balance above its starting capital.
- A claiming account is marked in flight; runClaims sends the amount it
  re-checked under the mutex.
- The watcher starts before the rest of the init can fail; settings are
  validated before any is written.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 20:45:35 -05:00
Brandon LiandClaude Opus 5.5 71606ffc9e Claim now: a button on the account page claims a payout account's profit
For a payout account on a firm that takes claims by API (Vest), the
account page shows what it could claim right now — its whole profit above
the account size — or why not (not flat, history loading, below its
start). Pressing it asks first, then claims to the login's main account
under the lane's mutex, with the same checks and bookkeeping as the
bot's own claim at a stage's target: the account trades its next stage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 19:44:10 -05:00
Brandon LiandClaude Opus 5.5 29eaf64a6b Vest: a day that lost to the bot's limit stops, even with Vest room left
The daily-loss room on Vest was min(the stage's limit, balance − Vest's
floor), which ignored the day's losses against the bot's own limit. With
the type's $1,229 under Vest's 3% ($1,500), five 50k evals that had
lost $1,111.65 today still saw $388 of room and traded again. The room
is now min(limit + the day's realized loss, balance − floor).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 18:55:54 -05:00
Brandon LiandClaude Opus 5.5 32081f3dcf Vest: claim the profit at the target, then trade again the same day
On Vest, a claim lowers the daily-loss floor by the amount claimed. Winning
$1,500 and claiming it leaves $3,000 of room the same day, the full
drawdown. The bot now uses that.

- Payout claims: a scheduler step (runClaims, under the lane mutex, before
  the flat gate) claims every payoutable account that reached its stage's
  target, via a platform that takes payouts by API. The claim is all
  profit above the starting balance. It then clears the account's legs and
  entry outcome for the day so it can trade its next stage. Failures retry
  after 5 minutes. The auto_claim setting 'off' disables it.
- VestClient.claimProfit: POST /v3/capital/withdraw to the login's main
  account (account_type 1 on /v3/accounts). The payout is booked at once:
  today's fund row and the cycle cut, with today's P&L counted from the
  post-claim balance. The history read later rebooks it from Vest's
  WITHDRAW row, and the books check counts a claim until that row shows.
- Same-day stages: an account type can pick its stage by the payouts since
  the firm's last daily reset (stagesSameDay, sameDayClaims). A stage can
  carry its own daily loss limit.
- That limit replaces the type's everywhere room, stops and last stands are
  worked out: the batch, the manual leg, copy trade, the dashboard and the
  report.
- Room from the platform's own floor: dailyLossFloor (Vest) makes the room
  min(stage limit, balance − Vest's floor). A same-day claim that lowers
  the floor therefore frees room, and the room is never more than Vest
  allows. VestClient.dayStartBalance is gone.
- Before the 8 PM ET reset: from 7:55 PM ET, each open Vest position on an
  account with a daily limit is fitted to the room the reset will leave
  (resetSafeStop). Its stop comes in via PUT/POST stop-loss, or the
  position closes when there is no room.
- Checks for same-day stages, the stage limit, floorRoom, the post-claim
  plan, the next reset across both time changes, and resetSafeStop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 15:56:00 -05:00
Brandon LiandClaude Opus 5.5 335af486d4 One side per machine: no lane opens against a position another lane holds
Each lane chose its batch's side on its own coin flip. So one person's
machine could be long NQ in the futures lane while its Vest accounts sat
short NDX, hedging that person against themselves across firms.

- Every new entry now takes the side of whatever the machine holds in any
  lane (machineSide in trading-logic). That includes another firm's
  positions and hand trades on any of the machine's accounts.
- With positions on both sides already, nothing is added: the batch reports
  positions-open with the reason, and a manual leg is refused.
- A side chosen with nothing held is reserved while its batch places it, so
  a lane deciding meanwhile follows it.
- It works on the side, not the instrument: NQ against an NDX perp is the
  same bet, and the indices and crypto are close enough that one side for
  everything is the only rule that never hedges.
- Unchanged: one direction per batch within a lane, and the DXtrade and Vest
  clients' own guards against opening opposite another of their accounts.
  The fleet still doesn't coordinate sides across people.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 15:27:18 -05:00
Brandon LiandClaude Opus 5.5 96fdd9ca23 Vest platform: a client for Vest Markets, traded on a session the extension relays
Vest (next.vestmarkets.com) is a perps prop firm: index, metal and energy
perps on CME hours, plus stocks, FX and crypto. It joins as a fourth
platform, 'vest', on the DXtrade engine paths. Those are now keyed by
isNotionalPlatform() instead of === 'dxtrade': the firm's own symbols,
percent fees, notional sizing from volatility, and a scheduler lane of its
own.

- Login is relayed, not stored. Vest signs people in with Google through
  Privy and renews the session with a device key the browser won't export.
  Extension 0.17.0's vest-relay.js reads the token the Vest app renews for
  itself (~15 min). It renews nothing, so the tab is never logged out. The
  background script posts that token to POST /api/vest/session, which holds
  it in memory only. With no Vest tab open on a machine that trades Vest,
  the extension opens one.
- The firm's credential is the Vest login's wallet address. The dashboard's
  form offers the relayed wallet with one click.
- VestClient (lib/vest-class.ts), mapped from the web app's traffic and code
  since Vest publishes no API:
  - Reads the capital accounts, balances (free amount plus position
    collateral), positions and open orders with the user token.
  - Mints per-account tokens for orders and transactions.
  - Names accounts for their rules (V50K-E-D3-T10-34, V50K-F-D3-34,
    V25K-I-7) so account-type prefixes can match.
  - Takes Vest's own max-drawdown and daily-loss floors. dayStartBalance =
    floor + the type's limit, so the engine's room is never more than
    Vest's.
  - Uses each symbol's taker fee and margin rate from exchangeInfo, and
    hourly volatility over in-session hours only.
  - Places take-profit and stop as position legs, which Vest cancels as an
    OCO pair.
- Daily P&L and fund rows come from /v3/transactions by Vest's 20:00 ET
  day. A books check holds an account, naming the transaction types, when
  the balance and the transactions disagree.
- scripts/check-vest.mjs (in npm run check): Vest's day across DST, account
  names, parsing, volatility gaps, market hours, the session store.

Not yet run against Vest with a live session: the order and exit bodies
come from the app's code, and the transaction classification from its enum.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 14:19:31 -05:00
Brandon LiandClaude Opus 5.5 5ec5abde6e Remove broker session rest
Reverts 062ec07 and 5eb0ea7: every firm's broker session stays logged in
around the clock again, renewing its token on the fixed timer as before.
The tree is back to 6dd2528's. A session_rest row left in settings is
unused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 23:39:53 -05:00
Brandon LiandClaude Opus 5.5 5eb0ea7760 Broker sessions rest while nothing needs them
A firm's client used to stay logged in around the clock, renewing on a fixed
timer overnight and at weekends. Now, once a firm has nothing open, nothing
pending and nothing left to trade before its next session for a random
15–45 minutes, its client disconnects and the dashboard shows it Resting.
It logs back in a random 20–50 minutes before the next session opens, at a
check-in every 3–7 Central daytime hours, or at once when trading starts, a
manual leg, copy trade, manual order or AutoBuyer run needs it. Token
renewals fall at random points (Tradovate 15–45 min before expiry, TopstepX
every 45–150 min). Settings → Trading → Rest idle broker sessions
(session_rest) turns it off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 13:28:47 -05:00
Brandon LiandClaude Opus 5.5 6dd2528fe2 Tradeify247: each person sizes a little apart from everyone else
Every machine sized a like account in a like state to the same position as
every other person's, so two people's positions could match to the unit.
Each person now draws a size factor in [0.85, 1.15] per trade date, seeded
by the master's uuid for them; all of one person's accounts share it.

The factor moves the hold a DXtrade leg is sized for (hold / factor², since
size goes as 1/√hold), so the instrument pick, the dashboard's next leg and
the Firms page's reach test all see the hold the size was made for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 20:00:33 -05:00
Brandon LiandClaude Opus 5.5 c0899620d4 Old rules for older accounts, a rules date per account, and a firm cap
When the master changes an account type with "New accounts only", it now
serves the type's old rules as kept rule sets (firm.keptAccountRules), each
for accounts dated inside [openedAt, closedAt). This build applies them:

- getAccountConfig, the one rules lookup trading reads, puts a kept set's
  rule fields over the live type when the account's rules date falls in
  its window. Id, label, prefix, size and the projection fields stay the
  live type's, so every report still names the live type.
- account_assignments gains rules_date and rules_date_manual. A new row is
  dated when the machine first sees the account, on the master's clock
  when this one runs behind and never before the catalog's last change.
  Auto dates come down to the broker's evidence: the end of the first fund
  day in Chicago, or a DXtrade registration instant. Evidence is applied
  before an account's first trade. Rows from before the migration get its
  time; unpinning keeps the row and its date. Backups carry both columns.
- An account on a kept set stays on its type while its prefix and cached
  size still match, so a tie-break flip can't move it off its old rules.
- Kept entries parse through the same normalizer as live types (now
  normalizeAccountType in lib/kept-rules.ts); a malformed one rejects the
  sync like a malformed type. The cached catalog records the parser that
  stored it, and a cache from an older parser is fetched in full rather
  than trusted with ?since= (this also brings in maxConcurrentAccounts).
- Reports add keptRuleIds per firm and live counts per kept set per type.
- The dashboard row and the account page show the rules date next to the
  account name, with an "old rules" chip saying what differs. Clicking
  the date edits it inline (PUT /api/accounts/:id/rules-date), previewing
  which rules the date lands on.

Also: the master's per-firm cap on accounts traded at once
(maxConcurrentAccounts), applied on top of the machine's own max
concurrent when a batch is drawn and in copy trading.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 22:21:55 -05:00
Brandon LiandClaude Opus 5.5 c0261f42ae Hide the next stop on a booked day; label min day P&L as such
On 2026-10-08 a Tradeify min day ($150) stopped out at −$306 on its 2×
stop of $300. The dashboard then read "stop $912": the next-stop label is
recomputed from the remaining target, which the loss grew to $456, for a
leg that never fires — the day is booked and the make-back rides tomorrow,
stopless.

- obfuscationDayBooked() holds the scheduler's booked-day check; the state
  route asks it too and shows no next stop, last stand or hold on a
  booked day.
- The account page's "Daily Loss Limit" showed minDayPnL. It now shows the
  type's real daily loss limit, with Min Day P&L on its own row; the
  objective that passed on total profit ≥ minDayPnL is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 16:06:35 -05:00
Brandon LiandClaude Opus 5.5 c3b58c8d8b Hold a payout that isn't booked yet, instead of re-earning it
On 2026-10-06 two Topstep Express accounts sat finished, waiting for their
payouts. The hub moved their balances the moment the payouts landed, but
reconcileFunds books a withdrawal only in the next sync. 28 seconds later
the plan read the drop as lost profit, reopened the cycle as stage 1 and
fired a full 3xNQ leg with no stop, on what should have been a drain day.
Both were liquidated against the $0 floor a payout leaves.

- TopstepX: unbookedBalanceChange holds an account while its balance is
  $200+ off its books (today's realized read fresh off the trades, so the
  5 PM roll doesn't trip it); the sync books it as before.
- Every platform: a finished cycle marks its books in account_meta; a later
  drop of $200+ with no fund row booked since holds the account until the
  payout books and moves it to its next stage. Relative to the mark, so
  history missing from before the cache cancels out; a wipe clears it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 12:13:48 -05:00
Brandon LiandClaude Opus 5.5 8747dd6b08 Exit prices on the tick grid, and a refused take-profit flattens
4217.6 − 87 × 0.1 is 4208.900000000001 in floating point. TopstepX refused
that take-profit ("Invalid price. Price is outside allowed range") on two
GC shorts on 2026-10-02, and with no stop on the leg nothing else was
working: both rode far past their target with no exit. ES and NQ never hit
it because 0.25 is exact in binary; GC and CL ticks aren't.

- onTick puts the measured-fee take-profit and every stop on the grid —
  the take-profit away from the entry, the stop toward it — so an averaged
  multi-contract fill between ticks is handled the same way.
- A leg's lone take-profit (no stop, or the fallback after a refused
  bracket) that the broker refuses now flattens the position and benches
  the account for the day, like a refused bracket. An unknown outcome still
  places nothing more.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 13:49:59 -05:00
Brandon LiandClaude Opus 5.5 58e7414849 Futures legs respect a firm's per-instrument contract limit
A firm can cap how many contracts of an instrument its accounts may hold
(the catalog's instrumentLimits, set on the master's Firms page). A leg
trades one contract per $1,000, capped by the account type's max contracts
and that limit. The Auto draw sizes each instrument's hold with the capped
count, so a limit that slows a leg past 12 hours takes it out of the draw.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 13:45:44 -05:00
Brandon LiandClaude Opus 5.5 ed62788480 Drain and extra-day legs always trade MNQ
They traded the micro of whatever the firm listed: MNQ with NQ, but MGC
for a firm listing only GC and CL. Now every futures firm's tiny legs go on
MNQ, the one micro the bot has always drained on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 18:54:41 -05:00
Brandon LiandClaude Opus 5.5 5696f2b036 Report each firm's trading mix: open positions and today's entries by instrument
The master knew how many accounts were in a trade, not in what. Each firm's
report now carries its open positions by instrument (accounts long and
short, contracts held) and today's entries the same way.

A position's contract is named by its instrument: DXtrade's carries the
symbol; a futures one is looked up among the contracts this machine has
entered (recorded at the fill and kept), then the resolver's cache and
TopstepX's own. Entries are counted as they fill, per trade date, in a
setting, so a restart doesn't lose the day.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 18:50:56 -05:00
Brandon LiandClaude Opus 5.5 ab3132182a Report each tick's outcome and the updater's state to the master
The master saw a scheduler's last run time and nothing of what it did. Each
tick now records, per lane, its outcome, a line on what happened (what
fired, or the spread, contract or hold cap that stopped it) and why the
lane's accounts sat it out, counted by reason with a few names each. It
rides the scheduler status as lastTicks.

Each report also carries the auto-updater's state from the files it keeps
and from git: its last check, a failed update waiting to retry, how far the
checkout is behind origin, and tracked files edited by hand, which stop the
fast-forward pull.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 18:31:21 -05:00
Brandon LiandClaude Opus 5.5 ff97a59b49 Session open: a random 0–180 minute delay, kept across restarts
The day's delay was 1–20 minutes. It is now 0–180, so the evening open
lands anywhere from 5:05 to 8:05 PM CT. The day's roll is saved as a
setting: with a three-hour window, a restart during the wait (an update, a
crash) would otherwise roll again and could start the session early.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 17:30:01 -05:00
Brandon LiandClaude Opus 5.5 db1d12327a Live futures spreads off one client, and the master's max entry spread
One quote feed per machine: a TopstepX client's market hub, else a
Tradovate client's market-data socket, subscribed to every listed root's
front month and the drain micro. A supervisor keeps it on a working client,
dropping one that fails or goes quiet while the market trades.

The Auto draw weighs each instrument by its live spread (the typical tick
when no fresh quote is at hand). No futures entry pays more than the
master's max entry spread ($15 until set) to cross the book: the draw
leaves such an instrument out, an explicit symbol sits the tick out, and
fireLeg and the extra-day trade check it last before sending. The Settings
page and the master's stats show the live spreads and the feed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:06:44 -05:00
Brandon LiandClaude Opus 5.5 934620e8e9 Manual leg: take the lane's lock before reading the account
It checked the account, then awaited the instrument draw and the contract
lookup, and only then locked the lane. A batch that fired in that gap left
it trading on numbers from before: a second leg, or on DXtrade the other
side of the position the batch had just opened. Now it locks first, as the
batch and copy trade do, and checks the lease still holds before firing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 13:59:23 -05:00
Brandon LiandClaude Opus 5.5 ff9b107452 Instruments: no fee lookup — commissions barely differ between instruments
Entry cost is one tick of spread; fees can be added later if they matter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 19:51:56 -05:00
Brandon LiandClaude Opus 5.5 02544fbdf5 Instruments: each firm's list from the master, and a session-aware futures draw
- lib/instrument-specs.ts (pure, tested): the CME specs — point value,
  tick size, one-tick spread, Yahoo ticker — replacing POINT_VALUES; the
  draw's maths; the 12-hour hold cap; the micro for drain and extra-day
  legs; reading a firm's list (the master's, or the old rules against an
  older master: this machine's enabled set less bans, the symbol map).
- lib/instruments.ts: the one module the trading paths ask. Measures every
  future's daily move from Yahoo's hourly candles once per trade date, and
  shows entry cost (a tick of spread plus Tradovate's fee parameters or
  TopstepX's last measured fill) without ever choosing by it.
- Futures Auto draws one root per batch from the batch firm's list: movement
  per dollar of spread early in the session with a 10% floor each, sliding
  to the fastest movers by the last entry; nothing expected to hold 12h or
  more; micros only if nothing else is listed. Every draw logs its odds.
- The batch is chosen before its instrument, and a group that can't trade
  anything yields to the next. Copy trade matches the held contract against
  the listed roots and won't copy an instrument it can't identify. Manual
  legs, manual orders and drain/extra-day legs follow the lists.
- DXtrade firms rank their own list whatever root was drawn.
- Capabilities, the dashboard's trade bar and contract resolves follow the
  lists; the trade bar and scheduler default to Auto. The master link
  reconnects when master_dashboard_url changes, and the catalog drops
  ?since after a switch so two masters' versions can't collide.
- Reports carry each futures firm's instrumentStats for the master's Firms
  page; GET /api/instruments shows them (?simulate=N runs the draw, no
  orders); the Settings page shows the lists read-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 19:47:32 -05:00
Brandon LiandClaude Opus 5.5 a2cd3a94c9 Legs: cheapest symbol that reaches the target hold, last stands, a reserve that grows with size
- A batch sizes its leg on every mapped candidate, each at the leverage
  the broker states for it, and trades the cheapest (spread, commissions,
  expected swap) that reaches the target hold — else the quickest. The
  dashboard ranks the same way from a half-hourly candidate survey.
- Last stand: when a daily-loss stop-out would leave the account within
  15% of that loss above its max-drawdown floor (the account type's static
  max loss, or the broker's liquidation floor), the leg carries no
  daily-loss stop and is sized against the firm's own breach.
- The daily-loss reserve is 10% of the limit or 0.1% of the position,
  whichever is more, since a stop's slippage grows with the position.
- The account type's max notional is gone: the broker's margin rates cap
  each symbol. planLegSize holds the one sizing rule fireLeg, the pick and
  the dashboard share.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 18:26:12 -05:00
Brandon LiandClaude Opus 5.5 a2d9df5ba4 DXtrade: count closed-trade P&L, week-long volatility, broker leverage caps, swap in the take-profit
- Transfers: a close books its SETTLEMENT and COMMISSION under one
  transaction code, and keying by the code alone let the commission
  overwrite the P&L. Keys now carry the type, so the day-start balance,
  daily-loss room, realized P&L, daily history and books check all see
  closed-trade profit and loss.
- Volatility from a week of hourly closes (was 48 h), still measured once
  per trade date, so one quiet weekend can't triple a hold estimate.
- Each symbol's margin rate from the account's instrument details caps
  the position at available funds / rate (Tradeify: 5:1 crypto, 2:1
  stocks); the account type's max notional stays as an optional ceiling.
- Firms with overnight financing add the swap a winning leg is expected
  to pay (the winners' mean hold, (A²+2AB)/3σ²) to its take-profit, and
  the sizing and hold estimate count it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 18:11:42 -05:00
Brandon LiandClaude Opus 5.5 675b4c89f8 Manual leg: may join a lane on the same instrument and side, never the other
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 17:46:16 -05:00
Brandon LiandClaude Opus 5.5 b5ed423145 Manual one-leg trigger: POST /api/accounts/[id]/fire-leg
Fires one leg on one account through the scheduler's own fire path (sizing,
take-profit, daily-loss stop, exit bracket), overriding only the one-trade-
per-day rule for unsplit accounts. Flat, history, daily-loss room, trading
hours, a flat lane and the lane mutex still apply. The daily-loss check is
factored out so both paths share it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 17:43:51 -05:00
Brandon LiandClaude Opus 5.5 d01f03b293 DXtrade sizing: volatility only (12h default hold); fees and every-day only for DXtrade
The symbol map's fixed notional per contract is gone: every DXtrade leg is
sized from the instrument's recent volatility for the firm's target hold
(blank = 12 h), and a candidate without a volatility isn't picked. Percent
commission and every-day trading are ignored on non-DXtrade firms, which keep
measured per-contract fees and the CME calendar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 17:29:32 -05:00
Brandon LiandClaude Opus 5.5 28f465508d DXtrade platform for Tradeify247: percent fees, hard daily-loss stops, volatility sizing, every-day lanes
- DXtrade client (lib/dxtrade-class.ts): REST session login with lockout-safe
  backoff, 60s sync of accounts/metrics/positions/orders/transfers, history and
  fund transactions from cash transactions, day-start balance at the 22:00 UTC
  reset, CLOSE-by-positionCode exits (native OCO group, else a stop-first
  emulated pair), a cross-account hedge guard, stray-order reaper.
- Instrument selector: the firm's symbol map lists candidate symbols per CME
  root; the cheapest tradable one per unit of recent hourly volatility wins
  (spread + 2x commission [+ financing] / sigma).
- Fees: a firm's commission % of notional prices every fee off the fill; the
  take-profit is solved in closed form so the leg nets its target.
- Hard daily loss limit per account type: every leg carries a stop keeping the
  firm's day inside the limit less a 10% reserve and both fees (rounded in);
  a green day never widens it; no room or a refused bracket flattens.
- Notional sizing where the platform prices positions: target-hold volatility
  sizing or the symbol map's fixed notional, under a per-type notional cap.
- Every-day firms trade weekends/holidays; the scheduler runs lanes (futures,
  one per DXtrade firm) with their own hours, flat gate, mutex and batches.
- The catalog sync skips platforms this build doesn't know instead of treating
  them as Tradovate. One shared fireLeg replaces the duplicated batch/copy code.
- npm run check: fee/stop/sizing/hours/selector checks; scripts/dxtrade-smoke.mjs
  confirms the API against a real login before trading.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 16:04:15 -05:00
Brandon LiandClaude Opus 5.5 809f337055 Unknown entries: judge only on a fresh good sync; all live orders count as working
Seventh review round (57835c0):

- An entry of unknown outcome is judged — filled after all, or never
  filled — only on a good sync that started after its settle window and
  with no order still working. A partial pass (position read failed or
  predated the fill, trades read after it) can no longer release it as
  "filled and flat" into a second entry.
- Tradovate counts every order not Filled/Canceled/Rejected/Expired/
  Completed as working (PendingCancel, Unknown included).
- TopstepX: an account whose order ended with its outcome unknown gets its
  open orders checked (Order/searchOpen) on each sync until none show, and
  counts as holding a working order until then.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 22:13:47 -05:00
Brandon LiandClaude Opus 5.5 57835c01f0 Unknown entries settle only on a sync started after them; filled-after-all and working orders seen
Sixth review round (8e66631):

- An entry of unknown outcome is judged against a good sync that STARTED
  at least 90s after it was declared unknown (lastSyncStartedAt: TopstepX's
  pass start, Tradovate's request time), not one that merely ended then —
  a sequential pass can read the account before a late fill.
- Once the broker shows its fill (a position, or realized P&L moved), the
  entry becomes an ordinary filled one instead of holding the account for
  the rest of the day.
- It never settles while the broker still shows a working order on the
  account: Tradovate now reads working orders from the sync snapshot, and
  its fill-timeout cancel checks Tradovate's answer instead of assuming it.
- A second wipe (after a restart) keeps the pending wipe's pre-wipe fund
  signature instead of overwriting it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 21:38:57 -05:00
Brandon LiandClaude Opus 5.5 8e66631b0e Entries of unknown outcome hold the account until settled; wipes keep their guards
Fifth review round (1eb0736):

- An entry now has three outcomes. Filled (confirmed): one-and-done is done
  for the day. Never placed: leg taken back, free to retry. Unknown (an ack
  or fill that never came back): the account — any mode — is held until a
  good sync at least 90s later shows it flat with realized P&L unchanged,
  then the leg is taken back and it may retry. Stop-after-all counts it in
  play meanwhile. This closes both the day-long bench of 52ed9c7 and the
  stacked re-entries of 1eb0736.
- Tradovate cancels an acked order whose fill didn't come within 30s (as
  TopstepX already does), so a retry can't sit beside a still-working one.
- A wipe keeps the rebuild's guards: wipeAccountData leaves the Cash History
  baseline, roll base, wipe hold, pre-wipe fund signature and the
  resolver's inferred size in account_meta; the routes forget history
  before wiping (so the stored rows are still there to read), and a wipe
  hold restored after a restart keeps its pre-wipe yardstick.
- Wipe routes take the set of typed accounts before wiping, so an account
  the resolver un-assigns mid-rebuild still counts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 21:12:51 -05:00
Brandon LiandClaude Opus 5.5 1eb0736fbe One-and-done counts confirmed fills; holds need a persistent refusal; wipes keep their yardstick
Fourth review round (52ed9c7):

- The one-and-done guard counts only entries the broker confirmed filled,
  so an entry that timed out or was rejected unfilled no longer benches the
  account for the day.
- TopstepX: an account is promoted only by a sync that actually checked its
  funds; one with no account type stays out (not traded) without costing a
  sync per pass, and once typed waits for that check. Wipe routes don't
  count untyped accounts as failures.
- Tradovate: an unchecked balance-hold release needs Cash History refused
  for a full hour straight (a 4xx other than 401/403/408/429, or an answer
  without data); any other outcome resets it, so one stray reply during a
  5xx outage can't release a payout.
- A wipe keeps the pre-wipe baseline and fund rows as its yardstick: the
  rebuild must explain today's balance against them, so a payout the
  rebuilt report hasn't booked yet stays held.
- The roll base of an unrolled record is kept per trade date in
  account_meta, so a restart later the same trade date counts from the same
  point instead of from a value that already includes the day's fills.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 20:46:35 -05:00
Brandon LiandClaude Opus 5.5 52ed9c75e4 Roll window instead of a roll-time base; transient outages keep payout holds
Third review round (088886e):

- Tradovate: a record still on the ended trade date waits only inside
  17:00-17:45 CT (every account rolled 17:17-17:21 on 2026-09-24); nothing
  trades on it there, so no pre-roll fill can be lost or double-counted by
  the broker's roll. Past the window an unrolled record (an idle account)
  counts from its value when first read, so nothing waits indefinitely —
  across restarts, stale bases or accounts the broker rolls only on a fill.
- One-and-done accounts also stop once this process fired a leg today.
- A balance hold is released unchecked only when Cash History is refused
  outright (4xx / errorText, e.g. a passed account's 404s); a 5xx, timeout
  or queue outage keeps the payout held until it recovers.
- A Cash History is judged against the baseline as it stood when requested,
  so a fold observed while it was in flight is not taken for a payout.
- TopstepX: a cache fallback for an account not loaded before waits for a
  funds check; accounts with no account type (not ours) aren't left
  pending; a fetch awaits a sync already running and syncs again if its
  accounts still wait, so wipe routes report the real outcome.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 19:37:14 -05:00
Brandon LiandClaude Opus 5.5 088886ed73 Pre-roll fills stay visible; holds survive restarts and judge fund amounts
Second review round (e496404, 9b2d677):

- Tradovate: past the 5 PM roll an old-dated cash record counts only what
  it gained since the roll (its realizedPnL as last seen while current is
  kept as the roll base), so tonight's fills booked onto it before
  Tradovate rolls the account stay visible — no re-trading a one-and-done
  account. A record this process never saw before the roll leaves the
  account waiting for the broker's roll (ROLL_PENDING, counted in play for
  stop-after-all). The 60s snapshot carries the roll base and can't undo a
  newer record the socket delivered.
- A wipe's hold is kept in account_meta, so a restart onto a failing Cash
  History still holds the account.
- A Cash History whose pre-request balance the last vouched-for baseline
  can't explain, and which booked nothing, holds the account instead of
  rebasing onto an unbooked payout.
- Holds compare fund transactions by date and amount (a second payout on a
  date that already has one), and an in-flight hold is judged against the
  rows from before its report.
- The saved baseline tracks the day's realized P&L and folds, so a restart
  judges against current figures; held refetches back off to an hour, and
  a balance hold no Cash History can check is released after an hour.
- TopstepX: a fetched account is promoted only by a sync whose
  reconcileFunds actually checked the books (not one skipped for an open
  position), and a direct fetch awaits that sync, so wipe routes report
  the real outcome.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 18:45:45 -05:00
Brandon LiandClaude Opus 5.5 9b2d677662 History holds: only a booked fund transaction lifts them; pace, persist, and cover wipes
Follow-up to 77d8c2e from its adversarial review:

- Stop-after-all no longer shuts down before an open while histories are
  still loading (a regression in 77d8c2e): a pending account counts as in
  play everywhere but the weekday 2:55-5:00 PM CT stretch after the close.
- Tradovate balance holds lift only when a Cash History books a fund
  transaction the account didn't have when the hold began; one that never
  does is released after 3 clean reports over 15 minutes, with a warning.
  The baseline comes from the balance when the report was requested, so a
  payout landing mid-report is held for the next one. Held accounts
  refetch every 5 minutes, not every sync.
- The Cash History baseline is kept in account_meta, so a restart onto a
  failing report server still catches a payout from the DB cache.
- A wipe holds the account until a Cash History rebuilds its fund
  transactions (the Fills report and the cache can't).
- A report still queued after its polls, or answered without data, is a
  failure, not an empty history.
- TopstepX: a newly fetched account (new, wiped, failed) waits for a sync
  that reconciles its funds, and the fetch starts that sync right away.
- Accounts a fetch is already working through aren't fetched again by the
  targeted pass; the TopstepX REST fill search spans ±15 minutes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 17:55:32 -05:00
Brandon LiandClaude Opus 5.5 e49640498a Tradovate: yesterday's realized P&L no longer reads as today's after the roll
Tradovate rolls each account's cash record to the new trade date on its own
schedule (18 minutes after the 17:00 CT open on 2026-09-24, or not until the
account trades), and until then the latest record still carries the ended
day's realizedPnL. Plans sized legs off it, one-and-done accounts read as
already traded, the reporter scored every account partial/loss, and an entry
across the record roll took a whole day's P&L as its "commission", parking
the take-profit out of reach.

- Cash records keep their trade date; realized P&L from an earlier trade
  date reads as 0 (judged when read, so a record stored before the roll
  counts as ended after it). Archived or older records never overwrite a
  newer one, whichever order the roll pair arrives in.
- The entry-commission delta falls back to $2.50/contract when it exceeds
  any real fee ($10/contract).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 17:22:43 -05:00
Brandon LiandClaude Opus 5.5 77d8c2ecdb Trade only on loaded history; catch early TopstepX fills; fix SI/M2K point values
- Both clients track which accounts this process has loaded history for
  (historyReady). An account restarted, rebuilt, wiped or newly listed is
  ineligible until its fetch lands, instead of trading as a fresh stage-1
  first day; a failed fetch with no cache no longer reads as empty history.
  Waiting accounts get a targeted fetch on the next sync.
- Tradovate holds an account whose start-of-day balance moves without a fill
  (a payout between hourly Cash History runs) until a Cash History books it,
  so a payout-pending account isn't re-traded on its ended cycle.
- Wipe routes forget history through the client and count accounts whose
  history didn't come back as failures.
- TopstepX sendOrder picks up a fill that arrived before the place response,
  and its REST backstop uses the documented Trade/search and Order/search.
- POINT_VALUES: SI 5000 (was 50), M2K 5 (was 10).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 17:04:09 -05:00
Brandon LiandClaude Opus 5.5 b932452e98 Self-healing broker clients, per-firm scheduler isolation, eager boot
Why the autotrader sometimes broke after a weekend until `pm2 restart`:
`next start` only logs uncaught errors, so nothing ever crashed, and a few
paths could leave a client dead for the life of the process.

Tradovate: a closed socket scheduled exactly one re-login, with no catch —
one failure during weekend maintenance (or a rate-limit penalty returned as
a 200 without a token) and the client never came back. connectAndAuth is now
a single-flight loop that never throws: jittered backoff capped at 5 min,
penalty tickets waited out, captcha demands backed off. A 15s watchdog
abandons silent, never-authorized or stuck-closing sockets and stale syncs;
renewal falls back to a fresh login; orders fail fast (OrderNotPlacedError)
on a dead socket and time out instead of hanging; a bad sync answer no
longer wipes accounts and positions.

TopstepX: hub start/stop/invoke and every REST call are bounded; a wedged
hub is discarded and rebuilt; a stuck sync is superseded after 3 min;
re-login no longer tears down the sync loop; a watchdog re-logs when syncs
go stale; a no-fill timeout cancels the order.

Scheduler: one unready client used to hold every firm forever. It now holds
the tick for a 10-minute grace, then trades the other firms without it.
Per-firm try/catch in batch collection, one batch at a time, accounts marked
in flight, legs un-counted only when the entry never reached the broker, and
the scheduler starts before the first batch fires.

Boot and supervision: instrumentation.ts boots the process at server start
(it used to wait for the first dashboard request); a supervisor rebuilds a
client that has been unhealthy for 20 min (not a TopstepX one holding a
position); broker HTTP calls default to a 60s timeout; GET /api/health shows
each client's liveness and the scheduler's last decision. Shared state the
eager boot would otherwise split across bundles lives on globalThis. Logs no
longer carry access tokens or whole AxiosErrors (which include the encoded
Tradovate password).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 20:18:33 -05:00
Brandon LiandClaude Fable 5 fc65b4340d Reconstruct TopstepX MLL floors from the EOD balance curve
TopstepX never reports the Maximum Loss Limit, but it's deterministic:
floor = min(0, eodBalanceHigh − trailingDrawdown), locking at the $0
starting balance. The EOD curve rebuilds from daily P&L + fund events,
with off-book fee drift spread across days proportionally to reported
fees (validated against a platform-reported MLL to ~$0.10). Stored in
autoLiqThresholds — negative, or +$0.01 once locked — so dead detection,
dashboards, and the ruin model's startThreshold all use the real room.
Guards move from '> 0' to '!== 0'; 0 still means no floor. canTrade=false
accounts are skipped. daily_pnl gains a fees column for the weighting.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-13 13:57:06 -05:00
Brandon LiandClaude Fable 5 50a5eb4713 Add TopstepX platform support, drain withdrawal stages, and perfect-path calendar projection
- TopstepXClient (ProjectX Gateway): API-key auth, SignalR user hub,
  Trade/search daily P&L, emulated OCO exits, books-invariant
  withdrawal detection for $0-start accounts
- Platform-aware client pool, credentials (api_key column), and
  credential form; $0-size account types match by prefix alone
- Drain stages: min-day ($5-$15 randomized) MNQ legs for the
  configured days, then wait for the 50% withdrawal; stage pills
  show 'Stage N – Drain'
- Account calendar projects the perfect path: future days book their
  planned targets, simulated 50% withdrawals, payout marker

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-12 16:25:52 -05:00
Brandon LiandClaude Fable 5 c58e981b1e Place OCO stop brackets for the catalog's obfuscation options
When an account type opts in, legs are placed as an OCO pair via
order/placeoco - the usual limit plus a stop: the first cycle day's stop
keeps the day's net floor at -$1,000, min-day trades stop at 2x their
target, and the day after any red day rides stopless to make it back. A
stop-out books the day: eligibility refuses the account once realized
P&L falls short of the legs fired (or is simply negative on an opted-in
account, which survives restarts). If placeoco is rejected the leg falls
back to the classic lone limit with a loud log rather than leave a
position without an exit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-11 18:28:21 -05:00