Merge branch 'vest-fix-display' into vest-fixes
# Conflicts: # package.json # scripts/check-trading-logic.mjs
This commit is contained in:
+18
-4
@@ -49,8 +49,11 @@ const COLD_RETRY_MAX_MS = 60_000;
|
||||
* first sync on a newer parser fetches the whole catalog.
|
||||
* 2: kept rule sets (keptAccountRules) and the firm's maxConcurrentAccounts.
|
||||
* Stamped only on a payload from a master that sends keptAccountRules: one
|
||||
* from an older master can't vouch for kept sets it never sent. */
|
||||
export const CATALOG_PARSER_VERSION = 2;
|
||||
* from an older master can't vouch for kept sets it never sent.
|
||||
* 3: Vest firms, which a build from before the platform skipped. A platform
|
||||
* learned later needs no bump: the cache names the platforms it skipped
|
||||
* (skippedPlatforms), and a build that knows one fetches in full. */
|
||||
export const CATALOG_PARSER_VERSION = 3;
|
||||
|
||||
/** A Date header further off than this is somebody else's clock, not the master's. */
|
||||
const MAX_MASTER_OFFSET_MS = 365 * 24 * 60 * 60 * 1000;
|
||||
@@ -103,6 +106,10 @@ function cache(): CatalogCache {
|
||||
const parser = (stored as { parser?: unknown }).parser;
|
||||
if (typeof parser === 'number') payload.parser = parser;
|
||||
else delete payload.parser;
|
||||
// Likewise the platforms it skipped: their firms aren't in the blob.
|
||||
const skipped = (stored as { skippedPlatforms?: unknown }).skippedPlatforms;
|
||||
if (Array.isArray(skipped)) payload.skippedPlatforms = skipped.filter((p): p is string => typeof p === 'string');
|
||||
else delete payload.skippedPlatforms;
|
||||
} catch {
|
||||
console.error('[catalog] cached payload is unreadable — ignoring it');
|
||||
}
|
||||
@@ -283,6 +290,8 @@ function validatePayload(raw: unknown): FirmCatalogPayload {
|
||||
if (!Array.isArray(body.firms)) throw new Error('missing firms');
|
||||
|
||||
const firms: CatalogFirm[] = [];
|
||||
// Stamped on the payload, so a build that learns one refetches its firms.
|
||||
const skippedPlatforms = new Set<string>();
|
||||
body.firms.forEach((f: unknown, i: number) => {
|
||||
if (typeof f !== 'object' || f === null) throw new Error(`firm ${i} is not an object`);
|
||||
const firm = f as Record<string, unknown>;
|
||||
@@ -297,6 +306,7 @@ function validatePayload(raw: unknown): FirmCatalogPayload {
|
||||
const platform = firm.platform === undefined ? 'tradovate' : firm.platform;
|
||||
if (!isFirmPlatform(platform)) {
|
||||
warnUnknownPlatform(firm.id, firm.name, platform);
|
||||
skippedPlatforms.add(String(platform));
|
||||
return;
|
||||
}
|
||||
// Percent commission and every-day trading are settings of the
|
||||
@@ -349,6 +359,7 @@ function validatePayload(raw: unknown): FirmCatalogPayload {
|
||||
return {
|
||||
version: body.version, updatedAt: body.updatedAt, firms, ...(trading ? { trading } : {}),
|
||||
...(keptAware ? { parser: CATALOG_PARSER_VERSION } : {}),
|
||||
...(skippedPlatforms.size > 0 ? { skippedPlatforms: [...skippedPlatforms] } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -424,10 +435,13 @@ export async function syncCatalog(): Promise<{ ok: boolean; changed: boolean; ve
|
||||
// `since` only against the master the cache came from: two masters'
|
||||
// version numbers can collide, and an 'unchanged' from the wrong one
|
||||
// would keep the other's catalog. Nor for a cache an older parser wrote:
|
||||
// it lacks what that parser dropped, and 'unchanged' would keep it so.
|
||||
// it lacks what that parser dropped, and 'unchanged' would keep it so —
|
||||
// fields, or firms on a platform it didn't know and this build does.
|
||||
const hasCache = getCachedCatalog().length > 0;
|
||||
const source = url.origin;
|
||||
const cacheCurrent = (cache().payload?.parser ?? 0) >= CATALOG_PARSER_VERSION;
|
||||
const cached = cache().payload;
|
||||
const cacheCurrent = (cached?.parser ?? 0) >= CATALOG_PARSER_VERSION
|
||||
&& !(cached?.skippedPlatforms ?? []).some((p) => isFirmPlatform(p));
|
||||
if (hasCache && cacheCurrent && getSetting('catalog_source_url') === source) url.searchParams.set('since', String(version));
|
||||
|
||||
try {
|
||||
|
||||
@@ -514,6 +514,9 @@ export interface FirmCatalogPayload {
|
||||
* payload. An older parser dropped fields it didn't know, so a cache
|
||||
* without it, or below the current one, isn't trusted to hold them. */
|
||||
parser?: number;
|
||||
/** Instance-local, like parser: the platforms of the firms that parser
|
||||
* skipped as unknown to it. A build that knows one fetches in full. */
|
||||
skippedPlatforms?: string[];
|
||||
}
|
||||
|
||||
export interface FirmCatalogUnchanged {
|
||||
|
||||
+3
-2
@@ -221,6 +221,7 @@ function collectFirmStats() {
|
||||
fundTransactions: fundTxns,
|
||||
balance: cash.amount,
|
||||
realizedPnL: cash.realizedPnL,
|
||||
stageInputs: stageInputsFor(client, acc.id, cfg),
|
||||
payout: {
|
||||
withdrawalPct: cfg.withdrawalPct,
|
||||
payoutCap: cfg.payoutCap,
|
||||
@@ -399,11 +400,11 @@ function collectFirmStats() {
|
||||
// A grind-day fill (dayTarget 0) can flag targetHit on a
|
||||
// negative day — a loss is still a loss, so red wins.
|
||||
// A red day on an account the bot puts stops on (the
|
||||
// obfuscation stops, or a hard daily loss limit) is its own
|
||||
// obfuscation stops, or its stage's hard daily loss limit) is its own
|
||||
// personal DLL, booked on purpose — not a genuine loss. (A
|
||||
// firm DLL lockout took the !acc.active branch above and
|
||||
// stays a loss.)
|
||||
if (botStopsActive(cfg) && combined.realizedPnL <= -25) {
|
||||
if (botStopsActive(cfg, effective) && combined.realizedPnL <= -25) {
|
||||
personalDllCount++;
|
||||
slice.personalDll = (slice.personalDll ?? 0) + 1;
|
||||
} else if (combined.realizedPnL < 0) {
|
||||
|
||||
+66
-20
@@ -454,6 +454,11 @@ export function projectPerfectPath(opts: {
|
||||
fundTransactions: { date: string; amount: number }[];
|
||||
balance: number;
|
||||
realizedPnL: number;
|
||||
/** What picks the stage beyond the fund rows (stageInputsFor). A type
|
||||
* whose stages reset each day trades today on today's payouts and
|
||||
* every projected day from the base stage. Absent = every withdrawal
|
||||
* counts. */
|
||||
stageInputs?: StageInputs;
|
||||
/** Trading days to look ahead. */
|
||||
horizonDays?: number;
|
||||
maxWithdrawals?: number;
|
||||
@@ -540,17 +545,36 @@ export function projectPerfectPath(opts: {
|
||||
return room / (room + dayTarget);
|
||||
};
|
||||
|
||||
const effectiveNow = () => resolveEffectiveConfig(
|
||||
// A type whose stages reset each day (Vest) counts only the payouts made
|
||||
// since its day began: today's real ones plus the path's own on that
|
||||
// date, so every projected day starts on the base stage.
|
||||
const sameDay = opts.stageInputs?.stagesSameDay === true;
|
||||
/** The path's own payouts, by the date they are claimed on. */
|
||||
const paidOn = new Map<string, number>();
|
||||
/** Payouts before the stage traded on `day`: that day's own for a
|
||||
* same-day type, else every withdrawal so far. */
|
||||
const paidBefore = (day: string): number => (sameDay
|
||||
? (day === today ? Math.max(0, Math.floor(opts.stageInputs?.sameDayWithdrawals ?? 0)) : 0) + (paidOn.get(day) ?? 0)
|
||||
: funds.filter((f) => f.amount < 0).length);
|
||||
/** A same-day type trades its next stage on the date a payout lands
|
||||
* (runClaims claims, then trades again on the room the claim freed) —
|
||||
* once through its stages a day: the last one repeats, and the path
|
||||
* stops there. */
|
||||
const tradesAgain = (day: string): boolean => sameDay
|
||||
&& (paidOn.get(day) ?? 0) > 0
|
||||
&& paidBefore(day) <= cfg.withdrawalStages.length;
|
||||
const effectiveOn = (day: string) => resolveEffectiveConfig(
|
||||
cfg.profitTarget, cfg.consistency, cfg.minTradingDays, cfg.targetSameEquity,
|
||||
cfg.withdrawalStages, 0, funds, cfg.minDayPnL,
|
||||
sameDay ? { stagesSameDay: true, sameDayWithdrawals: paidBefore(day) } : {},
|
||||
);
|
||||
/** The payout parameters of the stage currently being traded: per-stage
|
||||
/** The payout parameters of the stage being completed: per-stage
|
||||
* overrides when set, else the type defaults. */
|
||||
const stagePayout = (): { pct: number; cap: number } => {
|
||||
const count = funds.filter((f) => f.amount < 0).length;
|
||||
const stagePayout = (stage: number): { pct: number; cap: number } => {
|
||||
const count = stage - 1;
|
||||
if (count === 0 || cfg.withdrawalStages.length === 0) return { pct: basePct, cap: baseCap };
|
||||
const stage = cfg.withdrawalStages[Math.min(count - 1, cfg.withdrawalStages.length - 1)];
|
||||
return { pct: stage.withdrawalPct ?? basePct, cap: stage.payoutCap ?? baseCap };
|
||||
const s = cfg.withdrawalStages[Math.min(count - 1, cfg.withdrawalStages.length - 1)];
|
||||
return { pct: s.withdrawalPct ?? basePct, cap: s.payoutCap ?? baseCap };
|
||||
};
|
||||
const qualifyingDays = (minDay: number, excludeDate: string) => (
|
||||
minDay > 0
|
||||
@@ -562,7 +586,7 @@ export function projectPerfectPath(opts: {
|
||||
// Skipped when "today" is a weekend date (Saturday, or Friday evening
|
||||
// after the roll): there is no session to finish.
|
||||
if (todayHasSession) {
|
||||
const eff = effectiveNow();
|
||||
const eff = effectiveOn(today);
|
||||
const plan = computeDayPlan(
|
||||
accountId, today, eff.profitTarget, eff.consistency, sim, eff.minDayPnL,
|
||||
eff.minTradingDays, balance - cfg.accountSize, opts.realizedPnL, cfg.splitEnabled, eff.drain,
|
||||
@@ -591,7 +615,7 @@ export function projectPerfectPath(opts: {
|
||||
// over and over. Only a non-payoutable eval stops after one completion.
|
||||
const loops = cfg.withdrawalStages.length > 0 || cfg.payoutable === true;
|
||||
const finishCycle = (onDate: string, dayPnl: number, stage: number, drain: boolean): boolean => {
|
||||
const { pct, cap } = stagePayout();
|
||||
const { pct, cap } = stagePayout(stage);
|
||||
const rawGross = round2(Math.max(0, balance - cfg.accountSize) * pct);
|
||||
const gross = cap > 0 ? Math.min(rawGross, round2(cap)) : rawGross;
|
||||
const net = round2(gross * profitSplit);
|
||||
@@ -600,6 +624,9 @@ export function projectPerfectPath(opts: {
|
||||
balance = round2(balance - gross);
|
||||
sim = []; // withdrawal excludes its day — fresh cycle
|
||||
withdrawals++;
|
||||
// A cycle that stood complete before today is claimed today.
|
||||
const claimDay = onDate < today ? today : onDate;
|
||||
paidOn.set(claimDay, (paidOn.get(claimDay) ?? 0) + 1);
|
||||
high = Math.max(high, dd); // a payout locks the trail at breakeven
|
||||
out.push({ date: onDate, pnl: dayPnl, stage, drain, withdrawal: -gross, prob, gross, net });
|
||||
return false;
|
||||
@@ -613,14 +640,14 @@ export function projectPerfectPath(opts: {
|
||||
// requestable after that session's close. Never "today" — a projection
|
||||
// computed over the weekend must not slide a Friday finish to Sunday.
|
||||
{
|
||||
const eff = effectiveNow();
|
||||
const eff = effectiveOn(today);
|
||||
const probe = nextDay(today);
|
||||
const plan = computeDayPlan(
|
||||
accountId, probe, eff.profitTarget, eff.consistency, sim, eff.minDayPnL,
|
||||
eff.minTradingDays, balance - cfg.accountSize, 0, cfg.splitEnabled, eff.drain,
|
||||
);
|
||||
if (plan && plan.dayTarget <= 0 && eff.minTradingDays <= qualifyingDays(eff.minDayPnL, probe)) {
|
||||
const stage = 1 + funds.filter((f) => f.amount < 0).length;
|
||||
const stage = 1 + paidBefore(today);
|
||||
const traded = sim.filter((d) => d.pnl !== 0);
|
||||
const doneOn = traded.length > 0
|
||||
? traded.reduce((a, b) => (a.date > b.date ? a : b)).date
|
||||
@@ -629,11 +656,15 @@ export function projectPerfectPath(opts: {
|
||||
}
|
||||
}
|
||||
|
||||
// A same-day type's next stage trades on the date its payout landed,
|
||||
// today's included; anything else moves on a day.
|
||||
let date = today;
|
||||
for (let step = 0; step < horizon; step++) {
|
||||
date = nextDay(date);
|
||||
const eff = effectiveNow();
|
||||
const stage = 1 + funds.filter((f) => f.amount < 0).length;
|
||||
let again = todayHasSession && tradesAgain(today);
|
||||
for (let step = 0; again || step < horizon;) {
|
||||
if (again) again = false;
|
||||
else { date = nextDay(date); step++; }
|
||||
const eff = effectiveOn(date);
|
||||
const stage = 1 + paidBefore(date);
|
||||
const plan = computeDayPlan(
|
||||
accountId, date, eff.profitTarget, eff.consistency, sim, eff.minDayPnL,
|
||||
eff.minTradingDays, balance - cfg.accountSize, 0, cfg.splitEnabled, eff.drain,
|
||||
@@ -646,6 +677,7 @@ export function projectPerfectPath(opts: {
|
||||
// The cycle completed mid-projection with nothing left to trade —
|
||||
// the pending payout lands on this day.
|
||||
if (finishCycle(date, 0, stage, eff.drain)) break;
|
||||
again = tradesAgain(date);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -659,7 +691,7 @@ export function projectPerfectPath(opts: {
|
||||
|
||||
// Did this day complete the cycle? Then it IS the payout day.
|
||||
const nd = nextDay(date);
|
||||
const eff2 = effectiveNow();
|
||||
const eff2 = effectiveOn(date);
|
||||
const plan2 = computeDayPlan(
|
||||
accountId, nd, eff2.profitTarget, eff2.consistency, sim, eff2.minDayPnL,
|
||||
eff2.minTradingDays, balance - cfg.accountSize, 0, cfg.splitEnabled, eff2.drain,
|
||||
@@ -669,6 +701,7 @@ export function projectPerfectPath(opts: {
|
||||
&& eff2.minTradingDays <= qualifyingDays(eff2.minDayPnL, nd);
|
||||
if (doneAfter) {
|
||||
if (finishCycle(date, pnl, stage, eff.drain)) break;
|
||||
again = tradesAgain(date);
|
||||
} else {
|
||||
out.push({ date, pnl, stage, drain: eff.drain, prob });
|
||||
}
|
||||
@@ -865,6 +898,8 @@ export function endedCycleCompletion(opts: {
|
||||
targetSameEquity: boolean; withdrawalStages: WithdrawalStage[];
|
||||
combineCmeHolidays: boolean; splitEnabled: boolean;
|
||||
tradesEveryDay?: boolean;
|
||||
/** Stages reset each day (StageInputs). */
|
||||
stagesSameDay?: boolean;
|
||||
};
|
||||
/** Defaults to the current time. */
|
||||
now?: Date;
|
||||
@@ -898,10 +933,14 @@ export function endedCycleCompletion(opts: {
|
||||
const fundAmt = fundTransactions.filter((f) => f.date === lastFundDate).reduce((s, f) => s + f.amount, 0);
|
||||
const equityBefore = round2(opts.equityProfit - curTotal - fundAmt);
|
||||
|
||||
// The stage the ended cycle traded on: only the withdrawals before it.
|
||||
// The stage the ended cycle traded on: only the withdrawals before it —
|
||||
// for a type whose stages reset each day, only that day's. One fund row
|
||||
// a day shows none before it, and the rows here hold that whole day,
|
||||
// which began on the base stage: it is judged there.
|
||||
const eff = resolveEffectiveConfig(
|
||||
cfg.profitTarget, cfg.consistency, cfg.minTradingDays, cfg.targetSameEquity,
|
||||
cfg.withdrawalStages, 0, earlierFunds, cfg.minDayPnL,
|
||||
cfg.stagesSameDay === true ? { stagesSameDay: true, sameDayWithdrawals: 0 } : {},
|
||||
);
|
||||
const rows = combineCmeSessions(combine, endedRaw, lastFundDate, 0).dailyPnL;
|
||||
// A combining firm's folded day can net to 0 — nothing traded, as the firm counts it.
|
||||
@@ -912,6 +951,8 @@ export function endedCycleCompletion(opts: {
|
||||
|
||||
return {
|
||||
date: traded.map((d) => d.date).reduce(latest),
|
||||
// The payout's number among the account's withdrawals, a same-day
|
||||
// type's too: the master clears the entry once that many have booked.
|
||||
stage: 1 + earlierFunds.filter((f) => f.amount < 0).length,
|
||||
drain: eff.drain,
|
||||
cycle: prevFundDate ?? '',
|
||||
@@ -1599,10 +1640,15 @@ export function stopTicks(o: {
|
||||
}
|
||||
|
||||
/** Whether the bot places its own stops for this account type — the
|
||||
* obfuscation options or a hard daily loss limit. A red day on such an
|
||||
* account is the bot's own stop (the personal DLL), not a firm lockout. */
|
||||
export function botStopsActive(cfg: { firstDayStop: boolean; minDayStop: boolean; dailyLossLimit?: number }): boolean {
|
||||
return cfg.firstDayStop || cfg.minDayStop || (cfg.dailyLossLimit ?? 0) > 0;
|
||||
* obfuscation options or a hard daily loss limit: the current stage's
|
||||
* (resolveEffectiveConfig) when given, which a stage may set on a type with
|
||||
* none. A red day on such an account is the bot's own stop (the personal
|
||||
* DLL), not a firm lockout. */
|
||||
export function botStopsActive(
|
||||
cfg: { firstDayStop: boolean; minDayStop: boolean; dailyLossLimit?: number },
|
||||
effective?: { dailyLossLimit: number },
|
||||
): boolean {
|
||||
return cfg.firstDayStop || cfg.minDayStop || (effective ? effective.dailyLossLimit : cfg.dailyLossLimit ?? 0) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user