Batch like accounts together by prefix before any mismatches
runTrade now draws each batch from a single account-type prefix group (largest group first) instead of slicing across prefixes; a group's remainder fires alone on a later tick. copyTrade fills open slots with accounts matching the positioned accounts' prefix before mismatched ones. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
48f3c97ec5
commit
77ae4971f8
+30
-4
@@ -255,6 +255,7 @@ export async function runTrade(action: 'Buy' | 'Sell' | 'Auto', symbol: string)
|
|||||||
daysTraded: number;
|
daysTraded: number;
|
||||||
plan: DayPlan;
|
plan: DayPlan;
|
||||||
isExtraDay: boolean;
|
isExtraDay: boolean;
|
||||||
|
prefix: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const allEligible: EligibleItem[] = [];
|
const allEligible: EligibleItem[] = [];
|
||||||
@@ -281,6 +282,7 @@ export async function runTrade(action: 'Buy' | 'Sell' | 'Auto', symbol: string)
|
|||||||
firmName: firm.name, client, acc, contract,
|
firmName: firm.name, client, acc, contract,
|
||||||
cash: ev.cash, dailyPnL: ev.dailyPnL, daysTraded: ev.daysTraded,
|
cash: ev.cash, dailyPnL: ev.dailyPnL, daysTraded: ev.daysTraded,
|
||||||
plan: ev.plan, isExtraDay: ev.isExtraDay,
|
plan: ev.plan, isExtraDay: ev.isExtraDay,
|
||||||
|
prefix: getAccountConfig(acc.id).accountType?.prefix ?? '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
@@ -292,8 +294,21 @@ export async function runTrade(action: 'Buy' | 'Sell' | 'Auto', symbol: string)
|
|||||||
|
|
||||||
// Take only the first batch — all fired simultaneously, no rolling pool.
|
// Take only the first batch — all fired simultaneously, no rolling pool.
|
||||||
// Remaining accounts wait for the next tick (which only fires once all positions are flat).
|
// Remaining accounts wait for the next tick (which only fires once all positions are flat).
|
||||||
const batch = allEligible.slice(0, maxConcurrent);
|
// Like accounts trade together: a batch is drawn from a single prefix group
|
||||||
console.log(`[auto-trade] ${allEligible.length} eligible account(s) — firing batch of ${batch.length}`);
|
// (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.
|
||||||
|
const groups = new Map<string, EligibleItem[]>();
|
||||||
|
for (const item of allEligible) {
|
||||||
|
const group = groups.get(item.prefix);
|
||||||
|
if (group) group.push(item);
|
||||||
|
else groups.set(item.prefix, [item]);
|
||||||
|
}
|
||||||
|
const [batchPrefix, batchGroup] = [...groups.entries()]
|
||||||
|
.sort((a, b) => b[1].length - a[1].length || a[0].localeCompare(b[0]))[0];
|
||||||
|
const batch = batchGroup.slice(0, maxConcurrent);
|
||||||
|
console.log(`[auto-trade] ${allEligible.length} eligible account(s) across ${groups.size} prefix group(s) — firing batch of ${batch.length} ${batchPrefix || '(no prefix)'}`);
|
||||||
|
|
||||||
// ── Phase 2: fire the batch simultaneously ──
|
// ── Phase 2: fire the batch simultaneously ──
|
||||||
const tradeResults = await Promise.allSettled(batch.map(async (item) => {
|
const tradeResults = await Promise.allSettled(batch.map(async (item) => {
|
||||||
@@ -414,6 +429,7 @@ export async function copyTrade() {
|
|||||||
let positionedCount = 0;
|
let positionedCount = 0;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
let positionedClient: any = null;
|
let positionedClient: any = null;
|
||||||
|
const positionedPrefixes = new Set<string>();
|
||||||
|
|
||||||
for (const firm of firms) {
|
for (const firm of firms) {
|
||||||
const client = clients.get(firm.id);
|
const client = clients.get(firm.id);
|
||||||
@@ -422,6 +438,8 @@ export async function copyTrade() {
|
|||||||
const pos = client.positions[acc.id];
|
const pos = client.positions[acc.id];
|
||||||
if (!pos) continue;
|
if (!pos) continue;
|
||||||
positionedCount++;
|
positionedCount++;
|
||||||
|
const prefix = getAccountConfig(acc.id).accountType?.prefix;
|
||||||
|
if (prefix) positionedPrefixes.add(prefix);
|
||||||
if (!resolvedAction) {
|
if (!resolvedAction) {
|
||||||
resolvedAction = pos.netPos > 0 ? 'Buy' : 'Sell';
|
resolvedAction = pos.netPos > 0 ? 'Buy' : 'Sell';
|
||||||
positionContractId = pos.contractId;
|
positionContractId = pos.contractId;
|
||||||
@@ -482,6 +500,7 @@ export async function copyTrade() {
|
|||||||
contract: { name: string; tickSize: number };
|
contract: { name: string; tickSize: number };
|
||||||
cash: { amount: number; realizedPnL: number };
|
cash: { amount: number; realizedPnL: number };
|
||||||
plan: DayPlan;
|
plan: DayPlan;
|
||||||
|
prefix: string;
|
||||||
};
|
};
|
||||||
const eligible: CopyItem[] = [];
|
const eligible: CopyItem[] = [];
|
||||||
|
|
||||||
@@ -509,11 +528,18 @@ export async function copyTrade() {
|
|||||||
// copyTrade has no extra-day branch — those accounts wait for the scheduler
|
// copyTrade has no extra-day branch — those accounts wait for the scheduler
|
||||||
if (ev.isExtraDay) { console.log(`[copy-trade] ${acc.name}: extra-day account — skip`); continue; }
|
if (ev.isExtraDay) { console.log(`[copy-trade] ${acc.name}: extra-day account — skip`); continue; }
|
||||||
|
|
||||||
eligible.push({ firmName: firm.name, client, acc, contract, cash: ev.cash, plan: ev.plan });
|
eligible.push({
|
||||||
|
firmName: firm.name, client, acc, contract, cash: ev.cash, plan: ev.plan,
|
||||||
|
prefix: getAccountConfig(acc.id).accountType?.prefix ?? '',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const batch = eligible.slice(0, slotsAvailable);
|
// Fill the open slots with accounts whose prefix matches the already-positioned
|
||||||
|
// accounts before falling back to mismatched prefixes.
|
||||||
|
const matchesPositioned = (item: CopyItem) => positionedPrefixes.has(item.prefix);
|
||||||
|
const ordered = [...eligible.filter(matchesPositioned), ...eligible.filter((i) => !matchesPositioned(i))];
|
||||||
|
const batch = ordered.slice(0, slotsAvailable);
|
||||||
if (batch.length === 0) {
|
if (batch.length === 0) {
|
||||||
console.log('[copy-trade] no eligible accounts to copy to');
|
console.log('[copy-trade] no eligible accounts to copy to');
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
Reference in New Issue
Block a user