Add TopstepX platform support, drain withdrawal stages, and perfect-path calendar projection
- TopstepXClient (ProjectX Gateway): API-key auth, SignalR user hub, Trade/search daily P&L, emulated OCO exits, books-invariant withdrawal detection for $0-start accounts - Platform-aware client pool, credentials (api_key column), and credential form; $0-size account types match by prefix alone - Drain stages: min-day ($5-$15 randomized) MNQ legs for the configured days, then wait for the 50% withdrawal; stage pills show 'Stage N – Drain' - Account calendar projects the perfect path: future days book their planned targets, simulated 50% withdrawals, payout marker Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
1bca441247
commit
50a5eb4713
+208
-11
@@ -1,3 +1,23 @@
|
||||
export interface WithdrawalStage {
|
||||
profit: number;
|
||||
consistency: number;
|
||||
minTradingDays: number;
|
||||
/** Spam-withdraw stage: no profit target — book minTradingDays qualifying
|
||||
* days of ~minDayPnL (tiny MNQ trades), then wait for the withdrawal. */
|
||||
drain?: boolean;
|
||||
/** Drain stages only: the per-day booked amount (default $5; randomized
|
||||
* $5–$15 by drainDayAmount). */
|
||||
minDayPnL?: number;
|
||||
}
|
||||
|
||||
export interface EffectiveConfig {
|
||||
profitTarget: number;
|
||||
consistency: number;
|
||||
minTradingDays: number;
|
||||
minDayPnL: number;
|
||||
drain: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the effective profit target and consistency for an account based on its withdrawal strategy.
|
||||
*
|
||||
@@ -6,42 +26,53 @@
|
||||
* effectiveProfitTarget = profitTarget − remainingProfit
|
||||
*
|
||||
* Mode B (withdrawalStages non-empty): Stage 1 (no withdrawals yet) uses base profitTarget/consistency.
|
||||
* After the Nth withdrawal, use withdrawalStages[N-1]; last stage repeats.
|
||||
* Fallback: returns base profitTarget and consistency unchanged.
|
||||
* After the Nth withdrawal, use withdrawalStages[N-1]; last stage repeats. A drain stage carries no
|
||||
* profit target — it resolves to target 0 with drain=true and its own min-day amount.
|
||||
* Fallback: returns base values unchanged.
|
||||
*/
|
||||
export function resolveEffectiveConfig(
|
||||
profitTarget: number,
|
||||
consistency: number,
|
||||
minTradingDays: number,
|
||||
targetSameEquity: boolean,
|
||||
withdrawalStages: { profit: number; consistency: number; minTradingDays: number }[],
|
||||
withdrawalStages: WithdrawalStage[],
|
||||
priorProfit: number,
|
||||
fundTransactions: { date: string; amount: number }[]
|
||||
): { profitTarget: number; consistency: number; minTradingDays: number } {
|
||||
fundTransactions: { date: string; amount: number }[],
|
||||
minDayPnL = 0,
|
||||
): EffectiveConfig {
|
||||
if (targetSameEquity) {
|
||||
// Simple Mode A: effective target = profitTarget - equityProfit, computed in computeDailyTarget.
|
||||
// Since computeDailyTarget already compares (amount - accountSize) against profitTarget,
|
||||
// no adjustment needed here — just return the base values.
|
||||
return { profitTarget, consistency, minTradingDays };
|
||||
return { profitTarget, consistency, minTradingDays, minDayPnL, drain: false };
|
||||
}
|
||||
if (withdrawalStages.length > 0) {
|
||||
const withdrawalCount = fundTransactions.filter((f) => f.amount < 0).length;
|
||||
if (withdrawalCount === 0) {
|
||||
return { profitTarget, consistency, minTradingDays }; // Stage 1 = base values
|
||||
return { profitTarget, consistency, minTradingDays, minDayPnL, drain: false }; // Stage 1 = base values
|
||||
}
|
||||
const idx = Math.min(withdrawalCount - 1, withdrawalStages.length - 1);
|
||||
const stage = withdrawalStages[idx];
|
||||
if (stage.drain === true) {
|
||||
return {
|
||||
profitTarget: 0,
|
||||
consistency: 0,
|
||||
minTradingDays: stage.minTradingDays || minTradingDays,
|
||||
minDayPnL: stage.minDayPnL || 5,
|
||||
drain: true,
|
||||
};
|
||||
}
|
||||
// Fall back to base minTradingDays when the stage doesn't specify one (0 or missing)
|
||||
return { profitTarget: stage.profit, consistency: stage.consistency, minTradingDays: stage.minTradingDays || minTradingDays };
|
||||
return { profitTarget: stage.profit, consistency: stage.consistency, minTradingDays: stage.minTradingDays || minTradingDays, minDayPnL, drain: false };
|
||||
}
|
||||
return { profitTarget, consistency, minTradingDays };
|
||||
return { profitTarget, consistency, minTradingDays, minDayPnL, drain: false };
|
||||
}
|
||||
|
||||
/** @deprecated Use resolveEffectiveConfig instead */
|
||||
export function resolveEffectiveProfitTarget(
|
||||
profitTarget: number,
|
||||
targetSameEquity: boolean,
|
||||
withdrawalStages: { profit: number; consistency: number; minTradingDays: number }[],
|
||||
withdrawalStages: WithdrawalStage[],
|
||||
priorProfit: number,
|
||||
fundTransactions: { date: string; amount: number }[]
|
||||
): number {
|
||||
@@ -96,6 +127,22 @@ export function currentTradeDateCT(now: Date = new Date()): string {
|
||||
return new Date(date).toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Drain-day target: the stage's min-day amount plus a deterministic bump of
|
||||
* $0/$5/$10 — so a $5 min day trades as $5, $10 or $15 depending on the
|
||||
* (account, trade date) pair. Seeded like dailyBump so every consumer of the
|
||||
* plan computes the same number.
|
||||
*/
|
||||
export function drainDayAmount(accountId: number, tradeDate: string, minDay: number): number {
|
||||
const key = `${accountId}|drain|${tradeDate}`;
|
||||
let hash = 0x811c9dc5;
|
||||
for (let i = 0; i < key.length; i++) {
|
||||
hash ^= key.charCodeAt(i);
|
||||
hash = Math.imul(hash, 0x01000193) >>> 0;
|
||||
}
|
||||
return minDay + (hash % 3) * 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -277,6 +324,137 @@ export function combineCmeSessions(
|
||||
};
|
||||
}
|
||||
|
||||
// ── perfect-path projection ──────────────────────────────────────────────────
|
||||
|
||||
export interface ProjectedDay {
|
||||
date: string;
|
||||
/** The day's booked P&L on the perfect path (0 on withdrawal/payout days). */
|
||||
pnl: number;
|
||||
/** Stage the account is on while trading this day. */
|
||||
stage: number;
|
||||
drain: boolean;
|
||||
/** A simulated withdrawal (negative) lands on this day — cycle resets after it. */
|
||||
withdrawal?: number;
|
||||
/** Non-staged account completed here; the projection stops. */
|
||||
payoutReady?: boolean;
|
||||
}
|
||||
|
||||
/** Next weekday after d (YYYY-MM-DD). Trade dates never fall on Sat/Sun. */
|
||||
function nextTradingDate(d: string): string {
|
||||
const dt = new Date(`${d}T00:00:00Z`);
|
||||
do { dt.setUTCDate(dt.getUTCDate() + 1); } while (dt.getUTCDay() === 0 || dt.getUTCDay() === 6);
|
||||
return dt.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate the account's future assuming every day goes perfectly: each
|
||||
* trading day books exactly its planned target (deterministic bumps included,
|
||||
* so the numbers match what the trader would actually aim for), stages
|
||||
* complete, a 50% withdrawal fires the day a cycle finishes, and drain cycles
|
||||
* repeat. Built on the same computeDayPlan/resolveEffectiveConfig the live
|
||||
* trade path uses — the projection can't drift from reality's rules.
|
||||
*
|
||||
* An account without withdrawal stages stops at its first completion, marked
|
||||
* payoutReady. Projection starts on the next trading day (today's cell shows
|
||||
* live data); today's remaining target is booked silently first.
|
||||
*/
|
||||
export function projectPerfectPath(opts: {
|
||||
accountId: number;
|
||||
cfg: {
|
||||
profitTarget: number; consistency: number; minDayPnL: number;
|
||||
minTradingDays: number; accountSize: number; targetSameEquity: boolean;
|
||||
withdrawalStages: WithdrawalStage[]; splitEnabled: boolean;
|
||||
};
|
||||
/** Current-cycle daily P&L (account.dailyPnL). */
|
||||
cycleDailyPnL: { date: string; pnl: number }[];
|
||||
fundTransactions: { date: string; amount: number }[];
|
||||
balance: number;
|
||||
realizedPnL: number;
|
||||
/** Trading days to look ahead. */
|
||||
horizonDays?: number;
|
||||
maxWithdrawals?: number;
|
||||
}): ProjectedDay[] {
|
||||
const { accountId, cfg } = opts;
|
||||
const horizon = opts.horizonDays ?? 40;
|
||||
const maxWithdrawals = opts.maxWithdrawals ?? 6;
|
||||
|
||||
const today = currentTradeDateCT();
|
||||
let sim = opts.cycleDailyPnL.filter((d) => d.date !== today);
|
||||
if (opts.realizedPnL !== 0) sim = [...sim, { date: today, pnl: opts.realizedPnL }];
|
||||
else sim = [...sim];
|
||||
const funds = [...opts.fundTransactions];
|
||||
let balance = opts.balance;
|
||||
let withdrawals = 0;
|
||||
const out: ProjectedDay[] = [];
|
||||
|
||||
const effectiveNow = () => resolveEffectiveConfig(
|
||||
cfg.profitTarget, cfg.consistency, cfg.minTradingDays, cfg.targetSameEquity,
|
||||
cfg.withdrawalStages, 0, funds, cfg.minDayPnL,
|
||||
);
|
||||
const qualifyingDays = (minDay: number, excludeDate: string) => (
|
||||
minDay > 0
|
||||
? sim.filter((d) => d.date !== excludeDate && d.pnl >= minDay).length
|
||||
: sim.filter((d) => d.date !== excludeDate && d.pnl !== 0).length
|
||||
);
|
||||
|
||||
// Book the rest of today silently — the perfect path finishes today too.
|
||||
// Skipped when "today" is a weekend date (Saturday, or Friday evening
|
||||
// after the roll): there is no session to finish.
|
||||
const todayDow = new Date(`${today}T00:00:00Z`).getUTCDay();
|
||||
if (todayDow !== 0 && todayDow !== 6) {
|
||||
const eff = effectiveNow();
|
||||
const plan = computeDayPlan(
|
||||
accountId, today, eff.profitTarget, eff.consistency, sim, eff.minDayPnL,
|
||||
eff.minTradingDays, balance - cfg.accountSize, opts.realizedPnL, cfg.splitEnabled, eff.drain,
|
||||
);
|
||||
if (plan && plan.dayTarget > 0 && plan.remainingToday > 0) {
|
||||
const idx = sim.findIndex((d) => d.date === today);
|
||||
const pnl = round2(opts.realizedPnL + plan.remainingToday);
|
||||
if (idx >= 0) sim[idx] = { date: today, pnl };
|
||||
else sim.push({ date: today, pnl });
|
||||
balance = round2(balance + plan.remainingToday);
|
||||
}
|
||||
}
|
||||
|
||||
let date = today;
|
||||
for (let step = 0; step < horizon; step++) {
|
||||
date = nextTradingDate(date);
|
||||
const eff = effectiveNow();
|
||||
const stage = 1 + funds.filter((f) => f.amount < 0).length;
|
||||
const plan = computeDayPlan(
|
||||
accountId, date, eff.profitTarget, eff.consistency, sim, eff.minDayPnL,
|
||||
eff.minTradingDays, balance - cfg.accountSize, 0, cfg.splitEnabled, eff.drain,
|
||||
);
|
||||
if (!plan) break;
|
||||
|
||||
const daysNeeded = eff.minTradingDays > qualifyingDays(eff.minDayPnL, date);
|
||||
|
||||
if (plan.dayTarget <= 0 && !daysNeeded) {
|
||||
// Cycle complete — nothing left to trade.
|
||||
const withdrawal = -round2(balance / 2);
|
||||
if (cfg.withdrawalStages.length > 0 && withdrawals < maxWithdrawals && withdrawal < -1) {
|
||||
funds.push({ date, amount: withdrawal });
|
||||
balance = round2(balance + withdrawal);
|
||||
sim = []; // withdrawal excludes its day — fresh cycle
|
||||
withdrawals++;
|
||||
out.push({ date, pnl: 0, stage, drain: eff.drain, withdrawal });
|
||||
continue;
|
||||
}
|
||||
out.push({ date, pnl: 0, stage, drain: eff.drain, payoutReady: true });
|
||||
break;
|
||||
}
|
||||
|
||||
// Extra days (min-day 0, days still owed) book a token dollar so the
|
||||
// day counts, matching the real in-and-out MNQ trade.
|
||||
const pnl = plan.dayTarget > 0 ? plan.remainingToday : 1;
|
||||
sim.push({ date, pnl });
|
||||
balance = round2(balance + pnl);
|
||||
out.push({ date, pnl, stage, drain: eff.drain });
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Dollar-per-point value for common futures products. */
|
||||
export const POINT_VALUES: { [symbol: string]: number } = {
|
||||
NQ: 20, MNQ: 2, ES: 50, MES: 5,
|
||||
@@ -438,11 +616,30 @@ export function computeDayPlan(
|
||||
minTradingDays: number,
|
||||
equityProfit: number | null | undefined,
|
||||
realizedPnL: number,
|
||||
splitEnabled: boolean = true
|
||||
splitEnabled: boolean = true,
|
||||
drain: boolean = false
|
||||
): DayPlan | null {
|
||||
if (typeof equityProfit !== 'number' || !Number.isFinite(equityProfit)) return null;
|
||||
|
||||
const priorDays = dailyPnL.filter((d) => d.date !== tradeDate);
|
||||
|
||||
// Drain stage: no profit target at all. Book minTradingDays qualifying
|
||||
// days at the randomized min-day amount, then go quiet until the next
|
||||
// withdrawal resets the cycle. One unsplit tiny trade per day.
|
||||
if (drain) {
|
||||
const qualifying = minDayPnL > 0
|
||||
? priorDays.filter((d) => d.pnl >= minDayPnL)
|
||||
: priorDays.filter((d) => d.pnl !== 0);
|
||||
const dayTarget = qualifying.length >= minTradingDays
|
||||
? 0
|
||||
: drainDayAmount(accountId, tradeDate, minDayPnL || 5);
|
||||
return {
|
||||
dayTarget,
|
||||
remainingToday: round2(dayTarget - realizedPnL),
|
||||
path: 'reduced_day',
|
||||
splitAllowed: false,
|
||||
};
|
||||
}
|
||||
const dayStartEquity = equityProfit - realizedPnL;
|
||||
const target = computeDailyTarget(
|
||||
profitTarget, consistency, priorDays, minDayPnL, minTradingDays,
|
||||
|
||||
Reference in New Issue
Block a user