Report each account's future payout chain with closed-form reach probabilities
projectPerfectPath prices every projected withdrawal as min(withdrawalPct × equity profit, cap) × profit split, and tracks the cumulative probability of reaching it under the fair-walk ruin model — room/(room + dayTarget) per day (the per-leg product telescopes), trailing threshold locked at breakeven after the first withdrawal, no obfuscation branches. The reporter attaches the first 6 payouts per account so the master's payout calendar renders real amounts and probabilities instead of one static expectedPayout. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
50a5eb4713
commit
52d51afebb
@@ -484,6 +484,12 @@ export default function AccountPage() {
|
|||||||
fundTransactions,
|
fundTransactions,
|
||||||
balance: account.amount,
|
balance: account.amount,
|
||||||
realizedPnL: account.realizedPnL,
|
realizedPnL: account.realizedPnL,
|
||||||
|
payout: {
|
||||||
|
withdrawalPct: cfg.withdrawalPct,
|
||||||
|
payoutCap: cfg.payoutCap,
|
||||||
|
profitSplit: cfg.profitSplit,
|
||||||
|
drawdown: cfg.trailingDrawdown ?? 0,
|
||||||
|
},
|
||||||
}) : [];
|
}) : [];
|
||||||
const projPnlMap = new Map(projection.filter((p) => p.withdrawal === undefined && !p.payoutReady).map((p) => [p.date, p.pnl]));
|
const projPnlMap = new Map(projection.filter((p) => p.withdrawal === undefined && !p.payoutReady).map((p) => [p.date, p.pnl]));
|
||||||
const projFundMap = new Map(projection.filter((p) => p.withdrawal !== undefined).map((p) => [p.date, p.withdrawal!]));
|
const projFundMap = new Map(projection.filter((p) => p.withdrawal !== undefined).map((p) => [p.date, p.withdrawal!]));
|
||||||
|
|||||||
+27
-2
@@ -47,6 +47,16 @@ export interface AccountTypeStats {
|
|||||||
* after the 5 PM CT roll a day booked on tomorrow's trade date is
|
* after the 5 PM CT roll a day booked on tomorrow's trade date is
|
||||||
* already counted in `days`. */
|
* already counted in `days`. */
|
||||||
lastDay?: string;
|
lastDay?: string;
|
||||||
|
/** This account's projected payout chain on the perfect path: one
|
||||||
|
* entry per future withdrawal, with the gross amount
|
||||||
|
* (min(withdrawalPct × balance, cap)), the net received
|
||||||
|
* (gross × profitSplit), and prob = the CUMULATIVE probability of
|
||||||
|
* reaching that payout under the fair-walk ruin model. Optional so
|
||||||
|
* an older master simply ignores it. */
|
||||||
|
futurePayouts?: {
|
||||||
|
date: string; gross: number; net: number; prob: number;
|
||||||
|
stage: number; drain?: boolean;
|
||||||
|
}[];
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,8 +141,23 @@ export interface CatalogAccountType {
|
|||||||
/** Per-stage config after the Nth withdrawal; the last stage repeats.
|
/** Per-stage config after the Nth withdrawal; the last stage repeats.
|
||||||
* drain=true marks a spam-withdraw stage: no profit target — book
|
* drain=true marks a spam-withdraw stage: no profit target — book
|
||||||
* minTradingDays qualifying days of ~minDayPnL (default $5, randomized
|
* minTradingDays qualifying days of ~minDayPnL (default $5, randomized
|
||||||
* $5–$15, traded as tiny MNQ legs), then wait for the next withdrawal. */
|
* $5–$15, traded as tiny MNQ legs), then wait for the next withdrawal.
|
||||||
withdrawalStages: { profit: number; consistency: number; minTradingDays: number; drain?: boolean; minDayPnL?: number }[];
|
* withdrawalPct/payoutCap override the type's payout defaults per stage. */
|
||||||
|
withdrawalStages: { profit: number; consistency: number; minTradingDays: number; drain?: boolean; minDayPnL?: number; withdrawalPct?: number; payoutCap?: number }[];
|
||||||
|
/** Payout-projection settings, owned by the master's payout calendar but
|
||||||
|
* consumed here to compute each account's future payout chain. Optional:
|
||||||
|
* an older master doesn't send them. */
|
||||||
|
payoutable?: boolean;
|
||||||
|
/** Trailing drawdown in dollars for ruin-model probabilities. 0 = unset. */
|
||||||
|
trailingDrawdown?: number;
|
||||||
|
/** Legacy static payout estimate — superseded by the computed chain. */
|
||||||
|
expectedPayout?: number;
|
||||||
|
/** Share of the balance withdrawn when a cycle completes (default 0.5). */
|
||||||
|
withdrawalPct?: number;
|
||||||
|
/** Cap on the gross withdrawal in dollars. 0 = no cap. */
|
||||||
|
payoutCap?: number;
|
||||||
|
/** Share of the gross the person actually receives (default 0.9). */
|
||||||
|
profitSplit?: number;
|
||||||
/** ISO. Ties between two matching account types are broken by this. */
|
/** ISO. Ties between two matching account types are broken by this. */
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-1
@@ -17,7 +17,7 @@ import { getClients } from './clients';
|
|||||||
import { getTradableFirms } from './firm-catalog';
|
import { 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 { combineCmeSessions, computeDayPlan, currentTradeDateCT, LEG_COMPLETION_EPSILON, resolveEffectiveConfig } from './trading-logic';
|
import { combineCmeSessions, computeDayPlan, currentTradeDateCT, LEG_COMPLETION_EPSILON, projectPerfectPath, resolveEffectiveConfig } from './trading-logic';
|
||||||
import type { AccountTypeStats } from './master-protocol';
|
import type { AccountTypeStats } from './master-protocol';
|
||||||
|
|
||||||
function collectFirmStats() {
|
function collectFirmStats() {
|
||||||
@@ -126,6 +126,49 @@ function collectFirmStats() {
|
|||||||
if (run > highWater) highWater = run;
|
if (run > highWater) highWater = run;
|
||||||
}
|
}
|
||||||
const traded = combined.dailyPnL.filter((d) => d.pnl !== 0);
|
const traded = combined.dailyPnL.filter((d) => d.pnl !== 0);
|
||||||
|
|
||||||
|
// The account's future payout chain on the perfect path —
|
||||||
|
// dates, dynamic amounts (withdrawal % of balance, capped,
|
||||||
|
// × profit split) and cumulative fair-walk reach probability.
|
||||||
|
// The master renders these directly instead of running its
|
||||||
|
// own Monte-Carlo for this account.
|
||||||
|
let futurePayouts: NonNullable<AccountTypeStats['payoutStates']>[number]['futurePayouts'];
|
||||||
|
try {
|
||||||
|
futurePayouts = projectPerfectPath({
|
||||||
|
accountId: acc.id,
|
||||||
|
cfg,
|
||||||
|
cycleDailyPnL: client.dailyPnL[acc.id] ?? [],
|
||||||
|
fundTransactions: fundTxns,
|
||||||
|
balance: cash.amount,
|
||||||
|
realizedPnL: cash.realizedPnL,
|
||||||
|
payout: {
|
||||||
|
withdrawalPct: cfg.withdrawalPct,
|
||||||
|
payoutCap: cfg.payoutCap,
|
||||||
|
profitSplit: cfg.profitSplit,
|
||||||
|
// Same default chain the master's calendar always
|
||||||
|
// used: configured trailing drawdown, else the
|
||||||
|
// max-loss size, else $2,000.
|
||||||
|
drawdown: (cfg.trailingDrawdown ?? 0) > 0
|
||||||
|
? cfg.trailingDrawdown!
|
||||||
|
: Math.abs(cfg.maxLoss) > 0 ? Math.abs(cfg.maxLoss) : 2000,
|
||||||
|
startHigh: highWater,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.filter((p) => p.withdrawal !== undefined || p.payoutReady)
|
||||||
|
.slice(0, 6)
|
||||||
|
.map((p) => ({
|
||||||
|
date: p.date,
|
||||||
|
gross: p.gross ?? 0,
|
||||||
|
net: p.net ?? 0,
|
||||||
|
prob: Math.round(p.prob * 10000) / 10000,
|
||||||
|
stage: p.stage,
|
||||||
|
...(p.drain ? { drain: true } : {}),
|
||||||
|
}));
|
||||||
|
if (futurePayouts.length === 0) futurePayouts = undefined;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[reporter] payout projection failed for account ${acc.id}`, err);
|
||||||
|
}
|
||||||
|
|
||||||
(slice.payoutStates ??= []).push({
|
(slice.payoutStates ??= []).push({
|
||||||
days: qualifying.length,
|
days: qualifying.length,
|
||||||
profit: equityProfit,
|
profit: equityProfit,
|
||||||
@@ -137,6 +180,7 @@ function collectFirmStats() {
|
|||||||
consistency: effective.consistency,
|
consistency: effective.consistency,
|
||||||
minDays: effective.minTradingDays,
|
minDays: effective.minTradingDays,
|
||||||
lastDay: traded.length > 0 ? traded.reduce((a, b) => (a.date > b.date ? a : b)).date : undefined,
|
lastDay: traded.length > 0 ? traded.reduce((a, b) => (a.date > b.date ? a : b)).date : undefined,
|
||||||
|
...(futurePayouts ? { futurePayouts } : {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+77
-6
@@ -8,6 +8,10 @@ export interface WithdrawalStage {
|
|||||||
/** Drain stages only: the per-day booked amount (default $5; randomized
|
/** Drain stages only: the per-day booked amount (default $5; randomized
|
||||||
* $5–$15 by drainDayAmount). */
|
* $5–$15 by drainDayAmount). */
|
||||||
minDayPnL?: number;
|
minDayPnL?: number;
|
||||||
|
/** Payout overrides for the withdrawal that completes THIS stage; absent
|
||||||
|
* fields fall back to the account type's defaults. */
|
||||||
|
withdrawalPct?: number;
|
||||||
|
payoutCap?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EffectiveConfig {
|
export interface EffectiveConfig {
|
||||||
@@ -337,6 +341,17 @@ export interface ProjectedDay {
|
|||||||
withdrawal?: number;
|
withdrawal?: number;
|
||||||
/** Non-staged account completed here; the projection stops. */
|
/** Non-staged account completed here; the projection stops. */
|
||||||
payoutReady?: boolean;
|
payoutReady?: boolean;
|
||||||
|
/** Cumulative probability of the path having survived through this day,
|
||||||
|
* under the fair-walk ruin model: each trading day survives with
|
||||||
|
* room/(room + dayTarget), where room is the distance to the trailing
|
||||||
|
* liquidation threshold (the per-leg product telescopes to exactly
|
||||||
|
* this). 1 when no drawdown applies. */
|
||||||
|
prob: number;
|
||||||
|
/** Withdrawal entries: gross amount leaving the account
|
||||||
|
* (min(withdrawalPct × balance, cap)) and the net the person receives
|
||||||
|
* (gross × profitSplit). */
|
||||||
|
gross?: number;
|
||||||
|
net?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Next weekday after d (YYYY-MM-DD). Trade dates never fall on Sat/Sun. */
|
/** Next weekday after d (YYYY-MM-DD). Trade dates never fall on Sat/Sun. */
|
||||||
@@ -373,10 +388,25 @@ export function projectPerfectPath(opts: {
|
|||||||
/** Trading days to look ahead. */
|
/** Trading days to look ahead. */
|
||||||
horizonDays?: number;
|
horizonDays?: number;
|
||||||
maxWithdrawals?: number;
|
maxWithdrawals?: number;
|
||||||
|
/** Payout + risk model. Absent fields fall back to: withdraw 50%,
|
||||||
|
* uncapped, 90% split, no drawdown (prob 1). */
|
||||||
|
payout?: {
|
||||||
|
withdrawalPct?: number;
|
||||||
|
payoutCap?: number;
|
||||||
|
profitSplit?: number;
|
||||||
|
/** Trailing drawdown in dollars for the ruin probabilities. */
|
||||||
|
drawdown?: number;
|
||||||
|
/** Current cycle's end-of-day equity-profit high-water mark. */
|
||||||
|
startHigh?: number;
|
||||||
|
};
|
||||||
}): ProjectedDay[] {
|
}): ProjectedDay[] {
|
||||||
const { accountId, cfg } = opts;
|
const { accountId, cfg } = opts;
|
||||||
const horizon = opts.horizonDays ?? 40;
|
const horizon = opts.horizonDays ?? 40;
|
||||||
const maxWithdrawals = opts.maxWithdrawals ?? 6;
|
const maxWithdrawals = opts.maxWithdrawals ?? 6;
|
||||||
|
const basePct = opts.payout?.withdrawalPct ?? 0.5;
|
||||||
|
const baseCap = opts.payout?.payoutCap ?? 0;
|
||||||
|
const profitSplit = opts.payout?.profitSplit ?? 0.9;
|
||||||
|
const dd = Math.max(0, opts.payout?.drawdown ?? 0);
|
||||||
|
|
||||||
const today = currentTradeDateCT();
|
const today = currentTradeDateCT();
|
||||||
let sim = opts.cycleDailyPnL.filter((d) => d.date !== today);
|
let sim = opts.cycleDailyPnL.filter((d) => d.date !== today);
|
||||||
@@ -387,10 +417,34 @@ export function projectPerfectPath(opts: {
|
|||||||
let withdrawals = 0;
|
let withdrawals = 0;
|
||||||
const out: ProjectedDay[] = [];
|
const out: ProjectedDay[] = [];
|
||||||
|
|
||||||
|
// Fair-walk ruin model, mirroring the master's payout sim: the trailing
|
||||||
|
// liquidation threshold follows the end-of-day equity-profit high until
|
||||||
|
// profit reaches the drawdown, then locks at breakeven — and a past
|
||||||
|
// withdrawal means it locked long ago. Within a day the per-leg odds
|
||||||
|
// telescope, so a whole day survives with exactly room/(room + target).
|
||||||
|
let high = Math.max(0, opts.payout?.startHigh ?? 0, balance - cfg.accountSize);
|
||||||
|
if (funds.some((f) => f.amount < 0)) high = Math.max(high, dd);
|
||||||
|
let prob = 1;
|
||||||
|
const dayProb = (dayTarget: number): number => {
|
||||||
|
if (dd <= 0 || dayTarget <= 0) return 1;
|
||||||
|
const threshold = high >= dd ? 0 : high - dd;
|
||||||
|
const room = (balance - cfg.accountSize) - threshold;
|
||||||
|
if (room <= 0) return 0;
|
||||||
|
return room / (room + dayTarget);
|
||||||
|
};
|
||||||
|
|
||||||
const effectiveNow = () => resolveEffectiveConfig(
|
const effectiveNow = () => resolveEffectiveConfig(
|
||||||
cfg.profitTarget, cfg.consistency, cfg.minTradingDays, cfg.targetSameEquity,
|
cfg.profitTarget, cfg.consistency, cfg.minTradingDays, cfg.targetSameEquity,
|
||||||
cfg.withdrawalStages, 0, funds, cfg.minDayPnL,
|
cfg.withdrawalStages, 0, funds, cfg.minDayPnL,
|
||||||
);
|
);
|
||||||
|
/** The payout parameters of the stage currently being traded: per-stage
|
||||||
|
* overrides when set, else the type defaults. */
|
||||||
|
const stagePayout = (): { pct: number; cap: number } => {
|
||||||
|
const count = funds.filter((f) => f.amount < 0).length;
|
||||||
|
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 qualifyingDays = (minDay: number, excludeDate: string) => (
|
const qualifyingDays = (minDay: number, excludeDate: string) => (
|
||||||
minDay > 0
|
minDay > 0
|
||||||
? sim.filter((d) => d.date !== excludeDate && d.pnl >= minDay).length
|
? sim.filter((d) => d.date !== excludeDate && d.pnl >= minDay).length
|
||||||
@@ -408,11 +462,13 @@ export function projectPerfectPath(opts: {
|
|||||||
eff.minTradingDays, balance - cfg.accountSize, opts.realizedPnL, cfg.splitEnabled, eff.drain,
|
eff.minTradingDays, balance - cfg.accountSize, opts.realizedPnL, cfg.splitEnabled, eff.drain,
|
||||||
);
|
);
|
||||||
if (plan && plan.dayTarget > 0 && plan.remainingToday > 0) {
|
if (plan && plan.dayTarget > 0 && plan.remainingToday > 0) {
|
||||||
|
prob *= dayProb(plan.remainingToday);
|
||||||
const idx = sim.findIndex((d) => d.date === today);
|
const idx = sim.findIndex((d) => d.date === today);
|
||||||
const pnl = round2(opts.realizedPnL + plan.remainingToday);
|
const pnl = round2(opts.realizedPnL + plan.remainingToday);
|
||||||
if (idx >= 0) sim[idx] = { date: today, pnl };
|
if (idx >= 0) sim[idx] = { date: today, pnl };
|
||||||
else sim.push({ date: today, pnl });
|
else sim.push({ date: today, pnl });
|
||||||
balance = round2(balance + plan.remainingToday);
|
balance = round2(balance + plan.remainingToday);
|
||||||
|
high = Math.max(high, balance - cfg.accountSize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -430,26 +486,41 @@ export function projectPerfectPath(opts: {
|
|||||||
const daysNeeded = eff.minTradingDays > qualifyingDays(eff.minDayPnL, date);
|
const daysNeeded = eff.minTradingDays > qualifyingDays(eff.minDayPnL, date);
|
||||||
|
|
||||||
if (plan.dayTarget <= 0 && !daysNeeded) {
|
if (plan.dayTarget <= 0 && !daysNeeded) {
|
||||||
// Cycle complete — nothing left to trade.
|
// Cycle complete — nothing left to trade. The withdrawal takes
|
||||||
const withdrawal = -round2(balance / 2);
|
// the completed stage's share of the EQUITY PROFIT (for a
|
||||||
|
// $0-start account that is the whole balance), capped. Prop
|
||||||
|
// payouts come out of profit, never the funded principal.
|
||||||
|
const { pct, cap } = stagePayout();
|
||||||
|
const rawGross = round2(Math.max(0, balance - cfg.accountSize) * pct);
|
||||||
|
const gross = cap > 0 ? Math.min(rawGross, round2(cap)) : rawGross;
|
||||||
|
const withdrawal = -gross;
|
||||||
if (cfg.withdrawalStages.length > 0 && withdrawals < maxWithdrawals && withdrawal < -1) {
|
if (cfg.withdrawalStages.length > 0 && withdrawals < maxWithdrawals && withdrawal < -1) {
|
||||||
funds.push({ date, amount: withdrawal });
|
funds.push({ date, amount: withdrawal });
|
||||||
balance = round2(balance + withdrawal);
|
balance = round2(balance + withdrawal);
|
||||||
sim = []; // withdrawal excludes its day — fresh cycle
|
sim = []; // withdrawal excludes its day — fresh cycle
|
||||||
withdrawals++;
|
withdrawals++;
|
||||||
out.push({ date, pnl: 0, stage, drain: eff.drain, withdrawal });
|
high = Math.max(high, dd); // a payout locks the trail at breakeven
|
||||||
|
out.push({
|
||||||
|
date, pnl: 0, stage, drain: eff.drain, withdrawal, prob,
|
||||||
|
gross, net: round2(gross * profitSplit),
|
||||||
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
out.push({ date, pnl: 0, stage, drain: eff.drain, payoutReady: true });
|
out.push({
|
||||||
|
date, pnl: 0, stage, drain: eff.drain, payoutReady: true, prob,
|
||||||
|
gross, net: round2(gross * profitSplit),
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extra days (min-day 0, days still owed) book a token dollar so the
|
// Extra days (min-day 0, days still owed) book a token dollar so the
|
||||||
// day counts, matching the real in-and-out MNQ trade.
|
// day counts, matching the real in-and-out MNQ trade — riskless.
|
||||||
const pnl = plan.dayTarget > 0 ? plan.remainingToday : 1;
|
const pnl = plan.dayTarget > 0 ? plan.remainingToday : 1;
|
||||||
|
if (plan.dayTarget > 0) prob *= dayProb(plan.remainingToday);
|
||||||
sim.push({ date, pnl });
|
sim.push({ date, pnl });
|
||||||
balance = round2(balance + pnl);
|
balance = round2(balance + pnl);
|
||||||
out.push({ date, pnl, stage, drain: eff.drain });
|
high = Math.max(high, balance - cfg.accountSize);
|
||||||
|
out.push({ date, pnl, stage, drain: eff.drain, prob });
|
||||||
}
|
}
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
|
|||||||
@@ -16,7 +16,15 @@ export interface AccountConfig {
|
|||||||
firstDayStop: boolean; // first cycle day carries a $1,000 account stop - books red and ends
|
firstDayStop: boolean; // first cycle day carries a $1,000 account stop - books red and ends
|
||||||
minDayStop: boolean; // min-day trades carry a 2x-target stop; next day after a loss rides stopless
|
minDayStop: boolean; // min-day trades carry a 2x-target stop; next day after a loss rides stopless
|
||||||
combineCmeHolidays: boolean; // true = CME holiday sessions count with the next full trading day (Apex-style)
|
combineCmeHolidays: boolean; // true = CME holiday sessions count with the next full trading day (Apex-style)
|
||||||
withdrawalStages: { profit: number; consistency: number; minTradingDays: number; drain?: boolean; minDayPnL?: number }[]; // per-stage fresh profit + consistency (empty = use base values); drain = spam-withdraw stage
|
withdrawalStages: { profit: number; consistency: number; minTradingDays: number; drain?: boolean; minDayPnL?: number; withdrawalPct?: number; payoutCap?: number }[]; // per-stage fresh profit + consistency (empty = use base values); drain = spam-withdraw stage
|
||||||
|
/** Payout model, from the master's payout settings (absent on old masters):
|
||||||
|
* withdraw withdrawalPct of balance (capped at payoutCap) per cycle,
|
||||||
|
* receive profitSplit of the gross; trailingDrawdown feeds the ruin
|
||||||
|
* probabilities. */
|
||||||
|
withdrawalPct?: number;
|
||||||
|
payoutCap?: number;
|
||||||
|
profitSplit?: number;
|
||||||
|
trailingDrawdown?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FirmConfig {
|
export interface FirmConfig {
|
||||||
|
|||||||
Reference in New Issue
Block a user