From 5d2f4d607a6b736fe3c9280f7c23d16633d40c22 Mon Sep 17 00:00:00 2001 From: Brandon Li Date: Tue, 8 Sep 2026 22:03:23 -0500 Subject: [PATCH] Seed leg splits per prefix group so like accounts split targets alike MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Like accounts in a batch previously carved legs with independent Math.random, so near-identical day targets could split very differently. Leg fractions are now drawn from groupLegFraction(prefix, tradeDate, legIndex) — FNV-1a seeded like dailyBump — so accounts of the same account type on the same leg of the same day split identically when their remaining targets match, and proportionally close otherwise. Co-Authored-By: Claude Fable 5 --- lib/auto-trade.ts | 14 ++++++++++---- lib/trading-logic.ts | 16 ++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/lib/auto-trade.ts b/lib/auto-trade.ts index f6882ec..69c9b06 100644 --- a/lib/auto-trade.ts +++ b/lib/auto-trade.ts @@ -12,7 +12,7 @@ import { getClients } from './clients'; import { getTradableFirms } from './firm-catalog'; import { getAccountConfig } from './account-type-resolver'; import { - combineCmeSessions, computeDayPlan, pickLegAmount, resolveEffectiveConfig, currentTradeDateCT, + combineCmeSessions, computeDayPlan, groupLegFraction, pickLegAmount, resolveEffectiveConfig, currentTradeDateCT, LEG_COMPLETION_EPSILON, MAX_LEGS_PER_DAY, POINT_VALUES, type DayPlan, } from './trading-logic'; @@ -354,8 +354,12 @@ export async function runTrade(action: 'Buy' | 'Sell' | 'Auto', symbol: string) } // Trade one leg toward today's remaining target. Split-allowed big days carve a - // random ≥$800 piece; everything else trades the full remaining amount in one go. - const legAmount = item.plan.splitAllowed ? pickLegAmount(item.plan.remainingToday) : item.plan.remainingToday; + // ≥$800 piece; everything else trades the full remaining amount in one go. The + // carve fraction is seeded per (prefix, date, leg number) so like accounts in a + // batch split their targets the same way, or close to it when remainders differ. + const legAmount = item.plan.splitAllowed + ? pickLegAmount(item.plan.remainingToday, () => groupLegFraction(item.prefix, tradeDate, legsToday(acc.id, tradeDate))) + : item.plan.remainingToday; recordLeg(acc.id, tradeDate); const rawContracts = Math.max(1, Math.ceil(legAmount / 1000)); @@ -563,7 +567,9 @@ export async function copyTrade() { const { client, acc, contract } = item; const cfg = getAccountConfig(acc.id).accountType!; - const legAmount = item.plan.splitAllowed ? pickLegAmount(item.plan.remainingToday) : item.plan.remainingToday; + const legAmount = item.plan.splitAllowed + ? pickLegAmount(item.plan.remainingToday, () => groupLegFraction(item.prefix, tradeDate, legsToday(acc.id, tradeDate))) + : item.plan.remainingToday; recordLeg(acc.id, tradeDate); const rawContracts = Math.max(1, Math.ceil(legAmount / 1000)); diff --git a/lib/trading-logic.ts b/lib/trading-logic.ts index 2ba3c96..32d7d60 100644 --- a/lib/trading-logic.ts +++ b/lib/trading-logic.ts @@ -92,6 +92,22 @@ export function currentTradeDateCT(now: Date = new Date()): string { return new Date(date).toISOString().slice(0, 10); } +/** + * Deterministic fraction in [0, 1) for a (prefix group, trade date, leg index) triple. + * Like accounts (same account-type prefix) carving the same leg of the same day draw the + * same fraction, so pickLegAmount splits their targets identically when their remaining + * amounts match and proportionally close when they differ (bumps, epsilon leftovers). + */ +export function groupLegFraction(prefix: string, tradeDate: string, legIndex: number): number { + const key = `${prefix}|${tradeDate}|${legIndex}`; + let hash = 0x811c9dc5; + for (let i = 0; i < key.length; i++) { + hash ^= key.charCodeAt(i); + hash = Math.imul(hash, 0x01000193) >>> 0; + } + return hash / 0x100000000; +} + /** * Pick the next leg to trade toward today's remaining target. * remaining ≤ BIG_DAY_THRESHOLD → trade it all in one final leg; otherwise carve a random