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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
98016b3eb3
commit
c0899620d4
+19
-7
@@ -9,7 +9,7 @@
|
||||
|
||||
import { getClients } from './clients';
|
||||
import { getCachedFirm, getTradableFirms } from './firm-catalog';
|
||||
import { getAccountConfig } from './account-type-resolver';
|
||||
import { ensureRulesEvidence, getAccountConfig } from './account-type-resolver';
|
||||
import {
|
||||
combineCmeSessions, computeDayPlan, groupLegFraction, pickLegAmount, resolveEffectiveConfig, currentTradeDateCT,
|
||||
obfuscationStopDollars, takeProfitPlan, onTick, percentFee, dllRoom, dllStopDollars, dllViable, stopTicks,
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
isNoTradeTime, isPastSessionClose,
|
||||
BIG_DAY_THRESHOLD, LEG_COMPLETION_EPSILON, MAX_LEGS_PER_DAY,
|
||||
FALLBACK_COMMISSION_PER_CONTRACT, MAX_COMMISSION_PER_CONTRACT, DLL_MIN_STOP,
|
||||
CME_EVENING_OPEN_MIN, foldReason,
|
||||
CME_EVENING_OPEN_MIN, foldReason, withinFirmCaps,
|
||||
fundRowsKey, markFinishedCycle, unbookedPayoutDrop,
|
||||
type DayPlan, type FinishedCycleMark,
|
||||
} from './trading-logic';
|
||||
@@ -666,6 +666,11 @@ function finishedCycleDrop(
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function evaluateAccountForTrade(client: any, acc: { id: number; name: string; active: boolean }, tradeDate: string): AccountEvaluation | null {
|
||||
// Which rule set the account trades on hangs on its rules date: bring it
|
||||
// down to the broker's evidence before reading the rules, so an account
|
||||
// whose history landed since the last sync never trades a leg on the
|
||||
// date this machine first saw it.
|
||||
ensureRulesEvidence(client, acc.id);
|
||||
const cfg = getAccountConfig(acc.id).accountType;
|
||||
if (!cfg) return null;
|
||||
|
||||
@@ -1037,7 +1042,9 @@ async function runTradeLocked(
|
||||
// (largest group first), never mixed across prefixes — a group's remainder
|
||||
// fires as a smaller batch on a later tick instead of being padded from
|
||||
// another prefix. E.g. 4×FTDFY, 5×MFFUEV, 7×MFFUSF at limit 5 fires as
|
||||
// batches of 5, 5, 4, 2. A group that can't trade anything right now
|
||||
// batches of 5, 5, 4, 2. A firm's own cap on accounts at once (the
|
||||
// master's Firms page) trims its share first: 6 Topstep accounts capped
|
||||
// at 3 fire as 3, 3. A group that can't trade anything right now
|
||||
// (nothing listed fits, no contract resolves) yields to the next.
|
||||
const groups = new Map<string, Candidate[]>();
|
||||
for (const item of allEligible) {
|
||||
@@ -1051,7 +1058,8 @@ async function runTradeLocked(
|
||||
let batchPrefix = '';
|
||||
let resolvedSymbol = '';
|
||||
for (const [prefix, members] of ordered) {
|
||||
const candidates = members.slice(0, maxConcurrent);
|
||||
// The lane is flat (see above), so no firm holds anything yet.
|
||||
const candidates = withinFirmCaps(members).slice(0, maxConcurrent);
|
||||
const lead = candidates.find((it) => !it.isExtraDay && !it.drain);
|
||||
const firms = [...new Map(candidates.map((it) => [it.firm.id, it])).values()];
|
||||
|
||||
@@ -1566,7 +1574,8 @@ async function fireLeg(
|
||||
// ── copy trade ───────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Copy the current trade direction to up to maxConcurrent accounts.
|
||||
* Copy the current trade direction to up to maxConcurrent accounts (and no
|
||||
* more of a firm's than its cap on accounts at once).
|
||||
* Finds accounts with open positions, determines direction, then fires
|
||||
* orders for eligible accounts that haven't traded yet — within the lane of
|
||||
* the first open position found, never across lanes.
|
||||
@@ -1617,12 +1626,15 @@ async function copyTradeLocked(lease: number, lane: string): Promise<unknown[]>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let positionedClient: any = null;
|
||||
const positionedPrefixes = new Set<string>();
|
||||
// Open positions by firm, against each firm's cap on accounts at once.
|
||||
const heldByFirm = new Map<string, number>();
|
||||
|
||||
for (const { client } of ready) {
|
||||
for (const { firm, client } of ready) {
|
||||
for (const acc of client.accountList) {
|
||||
const pos = client.positions[acc.id];
|
||||
if (!pos) continue;
|
||||
positionedCount++;
|
||||
heldByFirm.set(firm.id, (heldByFirm.get(firm.id) ?? 0) + 1);
|
||||
const prefix = getAccountConfig(acc.id).accountType?.prefix;
|
||||
if (prefix) positionedPrefixes.add(prefix);
|
||||
if (!resolvedAction) {
|
||||
@@ -1718,7 +1730,7 @@ async function copyTradeLocked(lease: number, lane: string): Promise<unknown[]>
|
||||
// accounts before falling back to mismatched prefixes.
|
||||
const matchesPositioned = (item: LegItem) => positionedPrefixes.has(item.prefix);
|
||||
const ordered = [...eligible.filter(matchesPositioned), ...eligible.filter((i) => !matchesPositioned(i))];
|
||||
const batch = ordered.slice(0, slotsAvailable);
|
||||
const batch = withinFirmCaps(ordered, heldByFirm).slice(0, slotsAvailable);
|
||||
if (batch.length === 0) {
|
||||
console.log('[copy-trade] no eligible accounts to copy to');
|
||||
return [];
|
||||
|
||||
Reference in New Issue
Block a user