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>
This commit is contained in:
Brandon Li
2026-10-19 20:00:33 -05:00
co-authored by Claude Opus 5.5
parent ed56e306d1
commit 6dd2528fe2
8 changed files with 126 additions and 15 deletions
+1 -1
View File
@@ -166,7 +166,7 @@ export async function GET() {
const best = rankNextLeg({ const best = rankNextLeg({
client, accountId: acc.id, cfg, firm: f, leg: plan.remainingToday, dllRoom, client, accountId: acc.id, cfg, firm: f, leg: plan.remainingToday, dllRoom,
obfuscationDollars: obfuscationStopDollars(cfg, dailyPnL, plan, combined.tradeDate, plan.remainingToday), obfuscationDollars: obfuscationStopDollars(cfg, dailyPnL, plan, combined.tradeDate, plan.remainingToday),
cash, cash, tradeDate: combined.tradeDate,
})[0]; })[0];
if (best) { if (best) {
holdEstimate = { holdEstimate = {
+7 -6
View File
@@ -14,7 +14,6 @@ import {
combineCmeSessions, computeDayPlan, groupLegFraction, pickLegAmount, resolveEffectiveConfig, currentTradeDateCT, combineCmeSessions, computeDayPlan, groupLegFraction, pickLegAmount, resolveEffectiveConfig, currentTradeDateCT,
obfuscationStopDollars, takeProfitPlan, onTick, percentFee, dllRoom, dllStopDollars, dllViable, stopTicks, obfuscationStopDollars, takeProfitPlan, onTick, percentFee, dllRoom, dllStopDollars, dllViable, stopTicks,
expectedSwapToWin, planLegSize, maxLossRoom, breachDistance, expectedSwapToWin, planLegSize, maxLossRoom, breachDistance,
DEFAULT_TARGET_HOLD_HOURS,
isNoTradeTime, isPastSessionClose, isNoTradeTime, isPastSessionClose,
BIG_DAY_THRESHOLD, LEG_COMPLETION_EPSILON, MAX_LEGS_PER_DAY, BIG_DAY_THRESHOLD, LEG_COMPLETION_EPSILON, MAX_LEGS_PER_DAY,
FALLBACK_COMMISSION_PER_CONTRACT, MAX_COMMISSION_PER_CONTRACT, DLL_MIN_STOP, FALLBACK_COMMISSION_PER_CONTRACT, MAX_COMMISSION_PER_CONTRACT, DLL_MIN_STOP,
@@ -27,6 +26,7 @@ import type { LaneTick, TickReason } from './master-protocol';
import { POINT_VALUES, housekeepingRoot, HOUSEKEEPING_ROOT, MAX_HOLD_HOURS, futuresContracts } from './instrument-specs'; import { POINT_VALUES, housekeepingRoot, HOUSEKEEPING_ROOT, MAX_HOLD_HOURS, futuresContracts } from './instrument-specs';
import { firmInstruments, drawFuturesInstrument, spreadBlock, type LegShape } from './instruments'; import { firmInstruments, drawFuturesInstrument, spreadBlock, type LegShape } from './instruments';
import { recordEntry } from './trade-mix'; import { recordEntry } from './trade-mix';
import { sizingHoldHours } from './person-size';
import { errorMessage, isClientReady, isOrderNotPlaced, type PlatformClient, type PlatformContract, type PickContext } from './platform-client'; import { errorMessage, isClientReady, isOrderNotPlaced, type PlatformClient, type PlatformContract, type PickContext } from './platform-client';
// ── process-wide state ─────────────────────────────────────────────────────── // ── process-wide state ───────────────────────────────────────────────────────
@@ -1329,7 +1329,7 @@ function sizeLeg(item: SizingItem, contract: LegContract, legAmount: number, tra
leg: legAmount, leg: legAmount,
contract, contract,
commissionPct: pct, commissionPct: pct,
targetHoldHours: item.firm.targetHoldHours || DEFAULT_TARGET_HOLD_HOURS, targetHoldHours: sizingHoldHours(item.firm, tradeDate),
financingPctPerDay: item.firm.financingPctPerDay ?? 0, financingPctPerDay: item.firm.financingPctPerDay ?? 0,
dailyLossLimit: cfg.dailyLossLimit ?? 0, dailyLossLimit: cfg.dailyLossLimit ?? 0,
dllRoom: item.dllRoom, dllRoom: item.dllRoom,
@@ -1352,7 +1352,7 @@ function pickContextFor(items: SizingItem[], tradeDate: string): PickContext | u
if (!lead) return undefined; if (!lead) return undefined;
const legAmount = legAmountFor(lead, tradeDate); const legAmount = legAmountFor(lead, tradeDate);
return { return {
targetHoldHours: lead.firm.targetHoldHours || DEFAULT_TARGET_HOLD_HOURS, targetHoldHours: sizingHoldHours(lead.firm, tradeDate),
evaluate: (contract) => { evaluate: (contract) => {
const { sizing, cost } = sizeLeg(lead, contract, legAmount, tradeDate); const { sizing, cost } = sizeLeg(lead, contract, legAmount, tradeDate);
return sizing && cost !== null ? { notional: sizing.notional, holdHours: sizing.predictedHoldHours, cost } : null; return sizing && cost !== null ? { notional: sizing.notional, holdHours: sizing.predictedHoldHours, cost } : null;
@@ -1366,9 +1366,10 @@ function pickContextFor(items: SizingItem[], tradeDate: string): PickContext | u
* *
* Size: futures trade one contract per $1,000 of leg; a platform that prices * Size: futures trade one contract per $1,000 of leg; a platform that prices
* positions (stepNotional) sizes by notional instead — whatever makes the * positions (stepNotional) sizes by notional instead — whatever makes the
* leg's expected hold the firm's target, from the instrument's recent * leg's expected hold the firm's target as this person sizes it today
* volatility — under the account type's notional cap and the instrument's * (sizingHoldHours), from the instrument's recent volatility — under the
* margin rate (the firm's leverage). * account type's notional cap and the instrument's margin rate (the firm's
* leverage).
* *
* Fees: a percent-of-notional firm's fee is computed off the fill; otherwise * Fees: a percent-of-notional firm's fee is computed off the fill; otherwise
* it is read off the realized-P&L change a second after it. A firm that * it is read off the realized-P&L change a second after it. A firm that
+3 -2
View File
@@ -32,7 +32,8 @@
*/ */
import axios from './http-client'; import axios from './http-client';
import { currentTradeDateCT, FIRM_RESET_UTC_HOUR, DEFAULT_TARGET_HOLD_HOURS, rankForTarget } from './trading-logic'; import { currentTradeDateCT, FIRM_RESET_UTC_HOUR, rankForTarget } from './trading-logic';
import { sizingHoldHours } from './person-size';
import { import {
saveDailyPnL, loadDailyPnL, loadAccountMeta, saveAccountMeta, loadFundTransactions, saveFundTransactions, saveDailyPnL, loadDailyPnL, loadAccountMeta, saveAccountMeta, loadFundTransactions, saveFundTransactions,
saveEndedCycleStart, saveEndedCycleStart,
@@ -1222,7 +1223,7 @@ export class DXTradeClient implements PlatformClient {
const ref = this.referenceAccount(); const ref = this.referenceAccount();
if (!ref) return null; if (!ref) return null;
const firm = getCachedFirm(this.cfg.firmId); const firm = getCachedFirm(this.cfg.firmId);
const targetHold = firm?.targetHoldHours || DEFAULT_TARGET_HOLD_HOURS; const targetHold = sizingHoldHours(firm);
const symbols = [...new Set(candidates.map((c) => c.symbol))]; const symbols = [...new Set(candidates.map((c) => c.symbol))];
const [insts, dets, quotes, vols] = await Promise.all([ const [insts, dets, quotes, vols] = await Promise.all([
this.instruments(symbols), this.instruments(symbols),
+6 -3
View File
@@ -11,9 +11,10 @@ import type { PlatformContract } from './platform-client';
import type { SymbolStats } from './master-protocol'; import type { SymbolStats } from './master-protocol';
import { import {
combineCmeSessions, computeDayPlan, resolveEffectiveConfig, obfuscationStopDollars, dllRoom as computeDllRoom, combineCmeSessions, computeDayPlan, resolveEffectiveConfig, obfuscationStopDollars, dllRoom as computeDllRoom,
planLegSize, rankForTarget, maxLossRoom, lastStand, dllStopDollars, DEFAULT_TARGET_HOLD_HOURS, planLegSize, rankForTarget, maxLossRoom, lastStand, dllStopDollars,
FALLBACK_COMMISSION_PER_CONTRACT, FALLBACK_COMMISSION_PER_CONTRACT,
} from './trading-logic'; } from './trading-logic';
import { sizingHoldHours } from './person-size';
export type CandidateLeg = SymbolStats['symbols'][number] & { export type CandidateLeg = SymbolStats['symbols'][number] & {
swapReserve: number; swapReserve: number;
@@ -39,10 +40,12 @@ export function rankNextLeg(o: {
dllRoom: number | null; dllRoom: number | null;
obfuscationDollars: number; obfuscationDollars: number;
cash: { amount: number; availableFunds?: number }; cash: { amount: number; availableFunds?: number };
/** The trade date the hold is drawn for (sizingHoldHours); default today. */
tradeDate?: string;
}): CandidateLeg[] { }): CandidateLeg[] {
const candidates: PlatformContract[] = o.client.lastCandidates ?? []; const candidates: PlatformContract[] = o.client.lastCandidates ?? [];
if (!(o.leg > 0) || candidates.length === 0) return []; if (!(o.leg > 0) || candidates.length === 0) return [];
const target = o.firm.targetHoldHours || DEFAULT_TARGET_HOLD_HOURS; const target = sizingHoldHours(o.firm, o.tradeDate);
const floorRoom = maxLossRoom({ const floorRoom = maxLossRoom({
balance: o.cash.amount, accountSize: o.cfg.accountSize, maxLoss: o.cfg.maxLoss ?? 0, balance: o.cash.amount, accountSize: o.cfg.accountSize, maxLoss: o.cfg.maxLoss ?? 0,
autoLiqThreshold: o.client.autoLiqThresholds?.[o.accountId], autoLiqThreshold: o.client.autoLiqThresholds?.[o.accountId],
@@ -145,7 +148,7 @@ export function accountNextLeg(
lastStand: p.room !== null && lastStand({ dllRoom: p.room, dailyLossLimit: p.dll, maxLossRoom: floorRoom }), lastStand: p.room !== null && lastStand({ dllRoom: p.room, dailyLossLimit: p.dll, maxLossRoom: floorRoom }),
candidates: rankNextLeg({ candidates: rankNextLeg({
client, accountId: acc.id, cfg: p.cfg, firm, leg: p.plan.remainingToday, dllRoom: p.room, client, accountId: acc.id, cfg: p.cfg, firm, leg: p.plan.remainingToday, dllRoom: p.room,
obfuscationDollars: p.obfuscation, cash: p.cash, obfuscationDollars: p.obfuscation, cash: p.cash, tradeDate,
}), }),
}; };
} }
+24
View File
@@ -0,0 +1,24 @@
/**
* The hold this machine sizes DXtrade legs for. Every person's machine would
* otherwise size a like account in a like state to the same position as
* everyone else's; each person's own draw (personHoldHours) puts their sizes
* within ±PERSON_SIZE_SPREAD of the plan instead, the same for all of their
* accounts and new each trade date.
*/
import { getSetting } from './db';
import { currentTradeDateCT, personHoldHours, DEFAULT_TARGET_HOLD_HOURS } from './trading-logic';
/** Who this machine trades for: the master's uuid for the person, else the
* instance name it reports under. */
function personKey(): string {
return getSetting('instance_id') || getSetting('instance_name') || '';
}
/** The hold a DXtrade firm's legs are sized for on this machine today. */
export function sizingHoldHours(
firm: { targetHoldHours?: number } | null | undefined,
tradeDate: string = currentTradeDateCT(),
): number {
return personHoldHours(firm?.targetHoldHours || DEFAULT_TARGET_HOLD_HOURS, personKey(), tradeDate);
}
+3 -2
View File
@@ -20,9 +20,10 @@ import { getClients } from './clients';
import { getAccountTypeById, getKeptRuleIdsForFirm, getTradableFirms } from './firm-catalog'; import { getAccountTypeById, getKeptRuleIdsForFirm, getTradableFirms } from './firm-catalog';
import { getAccountConfig } from './account-type-resolver'; import { getAccountConfig } from './account-type-resolver';
import { getSchedulerStatus } from './auto-trade'; import { getSchedulerStatus } from './auto-trade';
import { botStopsActive, combineCmeSessions, computeDayPlan, currentTradeDateCT, cycleCompletion, endedCycleCompletion, LEG_COMPLETION_EPSILON, projectPerfectPath, resolveEffectiveConfig, DEFAULT_TARGET_HOLD_HOURS } from './trading-logic'; import { botStopsActive, combineCmeSessions, computeDayPlan, currentTradeDateCT, cycleCompletion, endedCycleCompletion, LEG_COMPLETION_EPSILON, projectPerfectPath, resolveEffectiveConfig } from './trading-logic';
import type { AccountTypeStats, FirmStats, InstrumentStats, SymbolStats } from './master-protocol'; import type { AccountTypeStats, FirmStats, InstrumentStats, SymbolStats } from './master-protocol';
import { accountNextLeg, accountLegShape } from './leg-estimate'; import { accountNextLeg, accountLegShape } from './leg-estimate';
import { sizingHoldHours } from './person-size';
import { instrumentStatsFor, isFuturesFirm } from './instruments'; import { instrumentStatsFor, isFuturesFirm } from './instruments';
import { accountStatusFields, brokerFacts, emptyFirmStats, firmStatusFields, historyFresh, keptRuleIdsField } from './platform-client'; import { accountStatusFields, brokerFacts, emptyFirmStats, firmStatusFields, historyFresh, keptRuleIdsField } from './platform-client';
@@ -494,7 +495,7 @@ function symbolStatsFor(
symbolStats: { symbolStats: {
account: acc.name, account: acc.name,
leg: next.leg, leg: next.leg,
targetHoldHours: f.targetHoldHours || DEFAULT_TARGET_HOLD_HOURS, targetHoldHours: Math.round(sizingHoldHours(f, tradeDate) * 10) / 10,
lastStand: next.lastStand, lastStand: next.lastStand,
at: new Date().toISOString(), at: new Date().toISOString(),
symbols: next.candidates.map((c) => ({ symbols: next.candidates.map((c) => ({
+38
View File
@@ -1440,6 +1440,44 @@ export function botStopsActive(cfg: { firstDayStop: boolean; minDayStop: boolean
/** The hold a leg is sized for when the firm doesn't set one. */ /** The hold a leg is sized for when the firm doesn't set one. */
export const DEFAULT_TARGET_HOLD_HOURS = 12; export const DEFAULT_TARGET_HOLD_HOURS = 12;
/** How far one person's DXtrade sizes move off the plan, each way. */
export const PERSON_SIZE_SPREAD = 0.15;
/**
* One person's size factor for a trade date, in [1 − spread, 1 + spread].
* Seeded (FNV-1a) by the person, so all of one person's accounts size alike
* while another person's land elsewhere, and drawn afresh each trade date.
* FNV alone barely moves its top bits when only the key's last character
* changes (the next day's date), so the hash is mixed (murmur3's finalizer)
* before it is scaled.
*/
export function personSizeFactor(personKey: string, tradeDate: string): number {
const key = `size|${personKey}|${tradeDate}`;
let hash = 0x811c9dc5;
for (let i = 0; i < key.length; i++) {
hash ^= key.charCodeAt(i);
hash = Math.imul(hash, 0x01000193) >>> 0;
}
hash ^= hash >>> 16;
hash = Math.imul(hash, 0x85ebca6b);
hash ^= hash >>> 13;
hash = Math.imul(hash, 0xc2b2ae35);
hash = (hash ^ (hash >>> 16)) >>> 0;
return 1 - PERSON_SIZE_SPREAD + (hash / 0x100000000) * 2 * PERSON_SIZE_SPREAD;
}
/**
* The hold a person's DXtrade leg is sized for: the firm's target, moved so
* the size comes out personSizeFactor times the plan. Size goes as 1/√hold
* (sizeNotional), so hold / factor² does it, and every rule that ranks or
* checks against the hold sees the same number the size was made for.
*/
export function personHoldHours(targetHoldHours: number, personKey: string, tradeDate: string): number {
const hold = targetHoldHours > 0 ? targetHoldHours : DEFAULT_TARGET_HOLD_HOURS;
const factor = personSizeFactor(personKey, tradeDate);
return hold / (factor * factor);
}
/** Share of the broker's margin headroom a new position may use: the rest /** Share of the broker's margin headroom a new position may use: the rest
* absorbs the price moving between sizing and the fill, and the entry fee. */ * absorbs the price moving between sizing and the fill, and the entry fee. */
export const MARGIN_HEADROOM = 0.97; export const MARGIN_HEADROOM = 0.97;
+44 -1
View File
@@ -13,7 +13,7 @@ import {
botStopsActive, sizeNotional, isNoTradeTime, isPastSessionClose, nextTradingDate, effectiveCombine, DEFAULT_TARGET_HOLD_HOURS, botStopsActive, sizeNotional, isNoTradeTime, isPastSessionClose, nextTradingDate, effectiveCombine, DEFAULT_TARGET_HOLD_HOURS,
projectPerfectPath, cycleCompletion, DLL_RESERVE_FRACTION, DLL_MIN_STOP, notionalCap, expectedSwapToWin, MARGIN_HEADROOM, projectPerfectPath, cycleCompletion, DLL_RESERVE_FRACTION, DLL_MIN_STOP, notionalCap, expectedSwapToWin, MARGIN_HEADROOM,
dllReserve, DLL_SLIPPAGE_FRACTION, maxLossRoom, lastStand, breachDistance, LAST_STAND_FRACTION, planLegSize, rankForTarget, dllReserve, DLL_SLIPPAGE_FRACTION, maxLossRoom, lastStand, breachDistance, LAST_STAND_FRACTION, planLegSize, rankForTarget,
currentTradeDateCT, foldReason, withinFirmCaps, currentTradeDateCT, foldReason, withinFirmCaps, personSizeFactor, personHoldHours, PERSON_SIZE_SPREAD,
} from '../lib/trading-logic.ts'; } from '../lib/trading-logic.ts';
// Deterministic PRNG so a failure reproduces. // Deterministic PRNG so a failure reproduces.
@@ -475,6 +475,49 @@ test('planLegSize: the stop, a last stand, the margin cap and the cost, as fireL
assert.equal(planLegSize({ ...base, maxLossRoom: null, contract: {} }).sizing, null); assert.equal(planLegSize({ ...base, maxLossRoom: null, contract: {} }).sizing, null);
}); });
test('personSizeFactor: one draw per person and trade date, within the spread', () => {
const people = Array.from({ length: 300 }, (_, i) => `0b6f${i.toString(16).padStart(4, '0')}-5c1e-4a7e-9d2b-${i}`);
const draws = people.map((p) => personSizeFactor(p, '2026-10-20'));
for (const f of draws) assert.ok(f >= 1 - PERSON_SIZE_SPREAD && f <= 1 + PERSON_SIZE_SPREAD, `factor ${f}`);
// Spread over the whole range, not bunched at 1.
assert.ok(Math.min(...draws) < 0.88 && Math.max(...draws) > 1.12, `${Math.min(...draws)}–${Math.max(...draws)}`);
// Repeatable, so every sizing of the day agrees.
assert.equal(personSizeFactor(people[0], '2026-10-20'), draws[0]);
// Two people land within half a percent of each other about as often as
// uniform draws do (2 × 0.005 / 0.3 ≈ 3%), not bunched together.
const close = draws.filter((f, i) => i > 0 && Math.abs(f - draws[i - 1]) < 0.005).length;
assert.ok(close < draws.length * 0.08, `${close} near-identical neighbours`);
// A new trade date, a new draw: a month of one person's days covers the
// range too, not a slow drift (the next date differs in its last digit).
const days = Array.from({ length: 30 }, (_, i) => personSizeFactor(people[0], `2026-11-${String(i + 1).padStart(2, '0')}`));
assert.ok(Math.max(...days) - Math.min(...days) > PERSON_SIZE_SPREAD, `days span ${Math.min(...days)}–${Math.max(...days)}`);
const steps = days.slice(1).map((f, i) => Math.abs(f - days[i]));
assert.ok(steps.reduce((a, b) => a + b, 0) / steps.length > 0.05, 'day to day the draw should jump, not creep');
});
test('personHoldHours: the size comes out the person\'s factor times the plan, at the hold it was sized for', () => {
const eth = { stepNotional: 26.86, volatilityPerHour: 0.022 / Math.sqrt(24), spreadFraction: 0.00004, marginRate: 0.2 };
const base = {
leg: 12239, contract: eth, commissionPct: 0.04, targetHoldHours: 12, financingPctPerDay: 0.033,
dailyLossLimit: 3000, dllRoom: 2757.39, maxLossRoom: 5757.39, obfuscationDollars: Infinity, availableFunds: 99757.39,
};
const plan = planLegSize(base).sizing;
for (const person of ['me-uuid', 'nik-uuid', 'ahmad-uuid', 'ziad-uuid']) {
const factor = personSizeFactor(person, '2026-10-20');
const hold = personHoldHours(12, person, '2026-10-20');
const sized = planLegSize({ ...base, targetHoldHours: hold }).sizing;
// Fees and the daily-loss stop move a little with the size, so the
// ratio sits a touch nearer 1 than the factor.
const ratio = sized.notional / plan.notional;
assert.ok(Math.abs(ratio - factor) < 0.02, `${person}: ×${ratio.toFixed(4)} vs factor ${factor.toFixed(4)}`);
assert.ok((ratio - 1) * (factor - 1) > 0, `${person}: moved the wrong way`);
// The pick ranks against this hold, and the size reaches it.
assert.ok(Math.abs(sized.predictedHoldHours - hold) < 0.1, `${person}: ${sized.predictedHoldHours}h vs ${hold}h`);
}
// No firm hold: the default, moved the same way.
assert.equal(personHoldHours(0, 'me-uuid', '2026-10-20'), personHoldHours(DEFAULT_TARGET_HOLD_HOURS, 'me-uuid', '2026-10-20'));
});
test('planLegSize: the broker\'s order size limits bind the sizing, so the pick sees the real hold', () => { test('planLegSize: the broker\'s order size limits bind the sizing, so the pick sees the real hold', () => {
const eth = { stepNotional: 26.86, volatilityPerHour: 0.022 / Math.sqrt(24), spreadFraction: 0.00004, marginRate: 0.2 }; const eth = { stepNotional: 26.86, volatilityPerHour: 0.022 / Math.sqrt(24), spreadFraction: 0.00004, marginRate: 0.2 };
const base = { const base = {