Compare commits
2
Commits
4edb771701
...
5e14daa669
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e14daa669 | ||
|
|
fc65b4340d |
@@ -411,9 +411,11 @@ export default function AccountPage() {
|
|||||||
|
|
||||||
const hasLossLimit = cfg != null && cfg.minDayPnL !== -999;
|
const hasLossLimit = cfg != null && cfg.minDayPnL !== -999;
|
||||||
const daysPassed = cfg != null && account.daysTraded >= cfg.minTradingDays;
|
const daysPassed = cfg != null && account.daysTraded >= cfg.minTradingDays;
|
||||||
// Dead when balance hits Tradovate's auto-liquidation floor
|
// Dead when balance hits the liquidation floor — Tradovate reports its
|
||||||
const isDead = account.autoLiqThreshold > 0 && account.amount <= account.autoLiqThreshold;
|
// (positive) floor; TopstepX floors are reconstructed MLLs, negative or
|
||||||
const liqFloor = account.autoLiqThreshold > 0 ? account.autoLiqThreshold : null;
|
// +$0.01 once locked at breakeven. 0 = no floor known.
|
||||||
|
const isDead = account.autoLiqThreshold !== 0 && account.amount <= account.autoLiqThreshold;
|
||||||
|
const liqFloor = account.autoLiqThreshold !== 0 ? account.autoLiqThreshold : null;
|
||||||
|
|
||||||
// Ground-truth profit = balance minus funded account size.
|
// Ground-truth profit = balance minus funded account size.
|
||||||
// Falls back to FIFO total when accountSize is unavailable.
|
// Falls back to FIFO total when accountSize is unavailable.
|
||||||
@@ -508,8 +510,9 @@ export default function AccountPage() {
|
|||||||
drawdown: (cfg.trailingDrawdown ?? 0) > 0
|
drawdown: (cfg.trailingDrawdown ?? 0) > 0
|
||||||
? cfg.trailingDrawdown!
|
? cfg.trailingDrawdown!
|
||||||
: Math.abs(cfg.maxLoss) > 0 ? Math.abs(cfg.maxLoss) : 2000,
|
: Math.abs(cfg.maxLoss) > 0 ? Math.abs(cfg.maxLoss) : 2000,
|
||||||
// The platform's real liquidation floor, when it reports one.
|
// The platform's real liquidation floor, when one is known —
|
||||||
startThreshold: account.autoLiqThreshold > 0
|
// reported by Tradovate, reconstructed for TopstepX.
|
||||||
|
startThreshold: account.autoLiqThreshold !== 0
|
||||||
? Math.round((account.autoLiqThreshold - cfg.accountSize) * 100) / 100
|
? Math.round((account.autoLiqThreshold - cfg.accountSize) * 100) / 100
|
||||||
: undefined,
|
: undefined,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ export async function GET() {
|
|||||||
const dailyPnL = combined.dailyPnL;
|
const dailyPnL = combined.dailyPnL;
|
||||||
const totalProfit = dailyPnL.reduce((sum, d) => sum + d.pnl, 0);
|
const totalProfit = dailyPnL.reduce((sum, d) => sum + d.pnl, 0);
|
||||||
const autoLiqThreshold = client.autoLiqThresholds[acc.id] ?? 0;
|
const autoLiqThreshold = client.autoLiqThresholds[acc.id] ?? 0;
|
||||||
const isDead = autoLiqThreshold > 0 && cash.amount <= autoLiqThreshold;
|
// 0 = no floor; negative floors are reconstructed TopstepX MLLs.
|
||||||
|
const isDead = autoLiqThreshold !== 0 && cash.amount <= autoLiqThreshold;
|
||||||
const allFundTxns = client.fundTransactions?.[acc.id] ?? [];
|
const allFundTxns = client.fundTransactions?.[acc.id] ?? [];
|
||||||
const priorProfit = client.priorProfit?.[acc.id] ?? 0;
|
const priorProfit = client.priorProfit?.[acc.id] ?? 0;
|
||||||
// Stage = 1 + number of withdrawals (negative fund transactions)
|
// Stage = 1 + number of withdrawals (negative fund transactions)
|
||||||
|
|||||||
+2
-1
@@ -24,7 +24,8 @@ function fmt(value: number) {
|
|||||||
|
|
||||||
/** Returns true when an account's balance has breached Tradovate's auto-liquidation floor. */
|
/** Returns true when an account's balance has breached Tradovate's auto-liquidation floor. */
|
||||||
function isAccountDead(account: AccountState): boolean {
|
function isAccountDead(account: AccountState): boolean {
|
||||||
return account.autoLiqThreshold > 0 && account.amount <= account.autoLiqThreshold;
|
// 0 = no floor; negative floors are reconstructed TopstepX MLLs.
|
||||||
|
return account.autoLiqThreshold !== 0 && account.amount <= account.autoLiqThreshold;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SettingsIcon() {
|
function SettingsIcon() {
|
||||||
|
|||||||
+3
-1
@@ -22,7 +22,9 @@ import { getSetting, setSetting } from './db';
|
|||||||
// ── helpers ──────────────────────────────────────────────────────────────────
|
// ── helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function isAccountDead(amount: number, autoLiqThreshold: number): boolean {
|
function isAccountDead(amount: number, autoLiqThreshold: number): boolean {
|
||||||
return autoLiqThreshold > 0 && amount <= autoLiqThreshold;
|
// Tradovate floors are positive balances; reconstructed TopstepX MLL
|
||||||
|
// floors are negative (or +$0.01 once locked at breakeven). 0 = no floor.
|
||||||
|
return autoLiqThreshold !== 0 && amount <= autoLiqThreshold;
|
||||||
}
|
}
|
||||||
|
|
||||||
// In-memory runaway guard: legs fired per account per trade date, plus the sum
|
// In-memory runaway guard: legs fired per account per trade date, plus the sum
|
||||||
|
|||||||
@@ -236,18 +236,27 @@ db.exec(`
|
|||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
export function saveDailyPnL(accountId: number, accountName: string, entries: { date: string; pnl: number }[]): void {
|
// Migration: the day's reported fees (TopstepX). The MLL reconstruction
|
||||||
const ins = db.prepare('INSERT OR REPLACE INTO daily_pnl (account_id, account_name, date, pnl) VALUES (?, ?, ?, ?)');
|
// distributes off-book fee drift across days proportionally to these; rows
|
||||||
|
// from before the migration read 0 until the next fetch overwrites them.
|
||||||
|
try {
|
||||||
|
db.exec('ALTER TABLE daily_pnl ADD COLUMN fees REAL NOT NULL DEFAULT 0');
|
||||||
|
} catch {
|
||||||
|
// Column already exists
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveDailyPnL(accountId: number, accountName: string, entries: { date: string; pnl: number; fees?: number }[]): void {
|
||||||
|
const ins = db.prepare('INSERT OR REPLACE INTO daily_pnl (account_id, account_name, date, pnl, fees) VALUES (?, ?, ?, ?, ?)');
|
||||||
const txn = db.transaction(() => {
|
const txn = db.transaction(() => {
|
||||||
for (const e of entries) {
|
for (const e of entries) {
|
||||||
ins.run(accountId, accountName, e.date, e.pnl);
|
ins.run(accountId, accountName, e.date, e.pnl, e.fees ?? 0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
txn();
|
txn();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadDailyPnL(accountId: number): { date: string; pnl: number }[] {
|
export function loadDailyPnL(accountId: number): { date: string; pnl: number; fees: number }[] {
|
||||||
return (db.prepare('SELECT date, pnl FROM daily_pnl WHERE account_id = ? ORDER BY date').all(accountId) as { date: string; pnl: number }[]);
|
return (db.prepare('SELECT date, pnl, fees FROM daily_pnl WHERE account_id = ? ORDER BY date').all(accountId) as { date: string; pnl: number; fees: number }[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Fund Transactions Cache ───────────────────────────────────────────────────
|
// ── Fund Transactions Cache ───────────────────────────────────────────────────
|
||||||
|
|||||||
+3
-2
@@ -73,7 +73,8 @@ function collectFirmStats() {
|
|||||||
for (const acc of client.accountList) {
|
for (const acc of client.accountList) {
|
||||||
const cash = client.accountCashBalances[acc.id] ?? { amount: 0, realizedPnL: 0 };
|
const cash = client.accountCashBalances[acc.id] ?? { amount: 0, realizedPnL: 0 };
|
||||||
const autoLiqThreshold = client.autoLiqThresholds[acc.id] ?? 0;
|
const autoLiqThreshold = client.autoLiqThresholds[acc.id] ?? 0;
|
||||||
const isDead = autoLiqThreshold > 0 && cash.amount <= autoLiqThreshold;
|
// 0 = no floor; negative floors are reconstructed TopstepX MLLs.
|
||||||
|
const isDead = autoLiqThreshold !== 0 && cash.amount <= autoLiqThreshold;
|
||||||
if (isDead) continue;
|
if (isDead) continue;
|
||||||
|
|
||||||
const cfg = getAccountConfig(acc.id).accountType;
|
const cfg = getAccountConfig(acc.id).accountType;
|
||||||
@@ -155,7 +156,7 @@ function collectFirmStats() {
|
|||||||
startHigh: highWater,
|
startHigh: highWater,
|
||||||
// Tradovate reports the account's REAL liquidation
|
// Tradovate reports the account's REAL liquidation
|
||||||
// floor — anchor the trail there for exact room.
|
// floor — anchor the trail there for exact room.
|
||||||
startThreshold: autoLiqThreshold > 0
|
startThreshold: autoLiqThreshold !== 0
|
||||||
? r2(autoLiqThreshold - cfg.accountSize)
|
? r2(autoLiqThreshold - cfg.accountSize)
|
||||||
: undefined,
|
: undefined,
|
||||||
},
|
},
|
||||||
|
|||||||
+89
-3
@@ -25,6 +25,7 @@ import * as signalR from '@microsoft/signalr';
|
|||||||
import { currentTradeDateCT } from './trading-logic';
|
import { currentTradeDateCT } from './trading-logic';
|
||||||
import { getCachedContract } from './contract-resolver';
|
import { getCachedContract } from './contract-resolver';
|
||||||
import { saveDailyPnL, loadDailyPnL, loadAccountMeta, saveAccountMeta, loadFundTransactions, saveFundTransactions } from './db';
|
import { saveDailyPnL, loadDailyPnL, loadAccountMeta, saveAccountMeta, loadFundTransactions, saveFundTransactions } from './db';
|
||||||
|
import { getAccountConfig } from './account-type-resolver';
|
||||||
import type { PlatformClient, PlatformAccount, PlatformContract } from './platform-client';
|
import type { PlatformClient, PlatformAccount, PlatformContract } from './platform-client';
|
||||||
|
|
||||||
const API_BASE = 'https://api.topstepx.com';
|
const API_BASE = 'https://api.topstepx.com';
|
||||||
@@ -112,6 +113,10 @@ export class TopstepXClient implements PlatformClient {
|
|||||||
* recomputed from these on every REST sync and every live trade event. */
|
* recomputed from these on every REST sync and every live trade event. */
|
||||||
private todayTrades: { [accountId: number]: Map<number, TsxTrade> } = {};
|
private todayTrades: { [accountId: number]: Map<number, TsxTrade> } = {};
|
||||||
|
|
||||||
|
/** Reported fees per trade date, per account — weights for spreading the
|
||||||
|
* off-book fee drift when reconstructing the MLL floor. */
|
||||||
|
private dailyFees: { [accountId: number]: Map<string, number> } = {};
|
||||||
|
|
||||||
/** Front-month contract per product symbol, plus name→contract for order
|
/** Front-month contract per product symbol, plus name→contract for order
|
||||||
* placement (sendOrder receives the contract NAME, e.g. "NQZ5"). */
|
* placement (sendOrder receives the contract NAME, e.g. "NQZ5"). */
|
||||||
private contractByProduct = new Map<string, { contract: TsxContract; resolvedAt: number }>();
|
private contractByProduct = new Map<string, { contract: TsxContract; resolvedAt: number }>();
|
||||||
@@ -474,6 +479,12 @@ export class TopstepXClient implements PlatformClient {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[topstepx] fund reconcile failed for ${acc.name}`, err instanceof Error ? err.message : err);
|
console.error(`[topstepx] fund reconcile failed for ${acc.name}`, err instanceof Error ? err.message : err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.updateMllFloor(acc);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[topstepx] MLL reconstruction failed for ${acc.name}`, err instanceof Error ? err.message : err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[topstepx] sync — ${this.accountList.length} account(s), ${Object.keys(this.positions).length} open position(s)`);
|
console.log(`[topstepx] sync — ${this.accountList.length} account(s), ${Object.keys(this.positions).length} open position(s)`);
|
||||||
@@ -553,6 +564,66 @@ export class TopstepXClient implements PlatformClient {
|
|||||||
this.applyCycleSlice(account.id);
|
this.applyCycleSlice(account.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse-engineer the account's MLL floor — TopstepX never reports it.
|
||||||
|
*
|
||||||
|
* Topstep's Maximum Loss Limit sits `trailingDrawdown` below the END-OF-DAY
|
||||||
|
* balance high, ratchets up only, and locks at the starting balance ($0 for
|
||||||
|
* Express accounts) once the high has climbed the full drawdown:
|
||||||
|
*
|
||||||
|
* floor = min(0, eodBalanceHigh − trailingDrawdown)
|
||||||
|
*
|
||||||
|
* The EOD balance curve is rebuilt from daily P&L + recorded fund events.
|
||||||
|
* Balance the books don't explain (sub-threshold off-book fees) is spread
|
||||||
|
* across days proportionally to each day's reported fees — validated
|
||||||
|
* against a platform-reported MLL to within $0.11.
|
||||||
|
*
|
||||||
|
* Stored in autoLiqThresholds (negative, or +$0.01 once locked at
|
||||||
|
* breakeven — 0 must keep meaning "no floor"), which lights up the same
|
||||||
|
* dead-account handling Tradovate's reported floors use. canTrade=false
|
||||||
|
* accounts are skipped: the platform has already retired them.
|
||||||
|
*/
|
||||||
|
private updateMllFloor(account: PlatformAccount): void {
|
||||||
|
if (!account.active) { this.autoLiqThresholds[account.id] = 0; return; }
|
||||||
|
const dd = getAccountConfig(account.id).accountType?.trailingDrawdown ?? 0;
|
||||||
|
const history = this.fullDailyPnL[account.id];
|
||||||
|
const cash = this.accountCashBalances[account.id];
|
||||||
|
if (!(dd > 0) || !history || !cash) { this.autoLiqThresholds[account.id] = 0; return; }
|
||||||
|
|
||||||
|
const today = currentTradeDateCT();
|
||||||
|
const fees = this.dailyFees[account.id] ?? new Map<string, number>();
|
||||||
|
|
||||||
|
// One event per past date: booked P&L plus any fund transaction.
|
||||||
|
const events = new Map<string, number>();
|
||||||
|
const add = (date: string, amount: number) => events.set(date, (events.get(date) ?? 0) + amount);
|
||||||
|
for (const d of history) if (d.date !== today) add(d.date, d.pnl);
|
||||||
|
for (const f of this.fundTransactions[account.id] ?? []) if (f.date !== today) add(f.date, f.amount);
|
||||||
|
const dates = [...events.keys()].sort();
|
||||||
|
if (dates.length === 0) { this.autoLiqThresholds[account.id] = 0; return; }
|
||||||
|
|
||||||
|
// What the books don't explain (bounded by the reconcile threshold) is
|
||||||
|
// off-book fee drift — attribute it in proportion to reported fees.
|
||||||
|
// Today is handled live: the balance carries today's realized P&L and
|
||||||
|
// any fund event detected today, neither of which is in `events`.
|
||||||
|
const booked = dates.reduce((s, d) => s + events.get(d)!, 0);
|
||||||
|
const todayFund = (this.fundTransactions[account.id] ?? []).find((f) => f.date === today)?.amount ?? 0;
|
||||||
|
const residual = Math.round((cash.amount - cash.realizedPnL - todayFund - booked) * 100) / 100;
|
||||||
|
const totalFees = dates.reduce((s, d) => s + (fees.get(d) ?? 0), 0);
|
||||||
|
|
||||||
|
let bal = 0;
|
||||||
|
let high = 0; // the MLL starts a full drawdown below the STARTING balance
|
||||||
|
for (const d of dates) {
|
||||||
|
const drift = totalFees > 0 ? residual * ((fees.get(d) ?? 0) / totalFees) : (d === dates[0] ? residual : 0);
|
||||||
|
bal += events.get(d)! + drift;
|
||||||
|
if (bal > high) high = bal;
|
||||||
|
}
|
||||||
|
|
||||||
|
const floor = high >= dd
|
||||||
|
? 0.01 // locked at breakeven; one cent keeps it distinct from "no floor"
|
||||||
|
: Math.round((high - dd) * 100) / 100;
|
||||||
|
this.autoLiqThresholds[account.id] = floor;
|
||||||
|
}
|
||||||
|
|
||||||
private maybeFetchDaysTraded(): void {
|
private maybeFetchDaysTraded(): void {
|
||||||
if (this.daysFetchInFlight) return;
|
if (this.daysFetchInFlight) return;
|
||||||
|
|
||||||
@@ -629,18 +700,31 @@ export class TopstepXClient implements PlatformClient {
|
|||||||
const trades = (res.trades ?? []).filter((t) => !t.voided);
|
const trades = (res.trades ?? []).filter((t) => !t.voided);
|
||||||
this.lastFetchRaw[account.name] = trades.length > 0 ? JSON.stringify(trades[0]) : '(no trades)';
|
this.lastFetchRaw[account.name] = trades.length > 0 ? JSON.stringify(trades[0]) : '(no trades)';
|
||||||
|
|
||||||
const dailyMap: { [date: string]: number } = {};
|
const dailyMap: { [date: string]: { pnl: number; fees: number } } = {};
|
||||||
for (const t of trades) {
|
for (const t of trades) {
|
||||||
const date = currentTradeDateCT(new Date(t.creationTimestamp));
|
const date = currentTradeDateCT(new Date(t.creationTimestamp));
|
||||||
dailyMap[date] = (dailyMap[date] ?? 0) + (t.profitAndLoss ?? 0) - (t.fees ?? 0);
|
const entry = (dailyMap[date] ??= { pnl: 0, fees: 0 });
|
||||||
|
entry.pnl += (t.profitAndLoss ?? 0) - (t.fees ?? 0);
|
||||||
|
entry.fees += t.fees ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fresh = Object.entries(dailyMap)
|
const fresh = Object.entries(dailyMap)
|
||||||
.map(([date, pnl]) => ({ date, pnl: Math.round(pnl * 100) / 100 }))
|
.map(([date, e]) => ({
|
||||||
|
date,
|
||||||
|
pnl: Math.round(e.pnl * 100) / 100,
|
||||||
|
fees: Math.round(e.fees * 10000) / 10000,
|
||||||
|
}))
|
||||||
.sort((a, b) => a.date.localeCompare(b.date));
|
.sort((a, b) => a.date.localeCompare(b.date));
|
||||||
|
|
||||||
const merged = mergePnL(cached, fresh);
|
const merged = mergePnL(cached, fresh);
|
||||||
|
|
||||||
|
// Reported fees per day — the weights the MLL reconstruction
|
||||||
|
// uses to place off-book fee drift. Fresh overwrites cached.
|
||||||
|
const fees = new Map<string, number>();
|
||||||
|
for (const e of cached) fees.set(e.date, e.fees ?? 0);
|
||||||
|
for (const e of fresh) fees.set(e.date, e.fees);
|
||||||
|
this.dailyFees[account.id] = fees;
|
||||||
|
|
||||||
// The last fund transaction (usually one this client detected
|
// The last fund transaction (usually one this client detected
|
||||||
// itself — see reconcileFunds) splits cycles: withdrawals
|
// itself — see reconcileFunds) splits cycles: withdrawals
|
||||||
// exclude their day, deposits include it.
|
// exclude their day, deposits include it.
|
||||||
@@ -648,12 +732,14 @@ export class TopstepXClient implements PlatformClient {
|
|||||||
this.applyCycleSlice(account.id);
|
this.applyCycleSlice(account.id);
|
||||||
saveDailyPnL(account.id, account.name, fresh);
|
saveDailyPnL(account.id, account.name, fresh);
|
||||||
delete this.lastFetchErrors[account.name];
|
delete this.lastFetchErrors[account.name];
|
||||||
|
this.updateMllFloor(account);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
console.error(`[topstepx] trade history failed for ${account.name}:`, msg);
|
console.error(`[topstepx] trade history failed for ${account.name}:`, msg);
|
||||||
failedAccounts++;
|
failedAccounts++;
|
||||||
if (cached.length > 0) {
|
if (cached.length > 0) {
|
||||||
this.fullDailyPnL[account.id] = cached;
|
this.fullDailyPnL[account.id] = cached;
|
||||||
|
this.dailyFees[account.id] = new Map(cached.map((e) => [e.date, e.fees ?? 0]));
|
||||||
this.applyCycleSlice(account.id);
|
this.applyCycleSlice(account.id);
|
||||||
this.lastFetchErrors[account.name] = `${msg} (using ${cached.length} cached entries from database)`;
|
this.lastFetchErrors[account.name] = `${msg} (using ${cached.length} cached entries from database)`;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+14
-5
@@ -6,7 +6,8 @@ export interface WithdrawalStage {
|
|||||||
* days of ~minDayPnL (tiny MNQ trades), then wait for the withdrawal. */
|
* days of ~minDayPnL (tiny MNQ trades), then wait for the withdrawal. */
|
||||||
drain?: boolean;
|
drain?: boolean;
|
||||||
/** 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). */
|
* the fixed $4–$15 half-dollar grid by drainDayAmount; this field is
|
||||||
|
* kept for the wire format but no longer drives the amount). */
|
||||||
minDayPnL?: number;
|
minDayPnL?: number;
|
||||||
/** Payout overrides for the withdrawal that completes THIS stage; absent
|
/** Payout overrides for the withdrawal that completes THIS stage; absent
|
||||||
* fields fall back to the account type's defaults. */
|
* fields fall back to the account type's defaults. */
|
||||||
@@ -62,7 +63,9 @@ export function resolveEffectiveConfig(
|
|||||||
profitTarget: 0,
|
profitTarget: 0,
|
||||||
consistency: 0,
|
consistency: 0,
|
||||||
minTradingDays: stage.minTradingDays || minTradingDays,
|
minTradingDays: stage.minTradingDays || minTradingDays,
|
||||||
minDayPnL: stage.minDayPnL || 5,
|
// Drain amounts come from the fixed $4–$15 grid, so the
|
||||||
|
// qualifying bar sits at the grid floor — every draw counts.
|
||||||
|
minDayPnL: DRAIN_DAY_MIN,
|
||||||
drain: true,
|
drain: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -138,14 +141,20 @@ export function currentTradeDateCT(now: Date = new Date()): string {
|
|||||||
* depending on the (account, trade date) pair). Seeded like dailyBump so
|
* depending on the (account, trade date) pair). Seeded like dailyBump so
|
||||||
* every consumer of the plan computes the same number.
|
* every consumer of the plan computes the same number.
|
||||||
*/
|
*/
|
||||||
export function drainDayAmount(accountId: number, tradeDate: string, minDay: number): number {
|
/** Drain-day target grid: $4–$15 in $0.50 steps (23 values), always traded
|
||||||
|
* on MNQ. Drain days qualify at ≥ DRAIN_DAY_MIN, so every draw counts. */
|
||||||
|
export const DRAIN_DAY_MIN = 4;
|
||||||
|
export const DRAIN_DAY_MAX = 15;
|
||||||
|
const DRAIN_DAY_STEPS = Math.round((DRAIN_DAY_MAX - DRAIN_DAY_MIN) / 0.5) + 1;
|
||||||
|
|
||||||
|
export function drainDayAmount(accountId: number, tradeDate: string): number {
|
||||||
const key = `${accountId}|drain|${tradeDate}`;
|
const key = `${accountId}|drain|${tradeDate}`;
|
||||||
let hash = 0x811c9dc5;
|
let hash = 0x811c9dc5;
|
||||||
for (let i = 0; i < key.length; i++) {
|
for (let i = 0; i < key.length; i++) {
|
||||||
hash ^= key.charCodeAt(i);
|
hash ^= key.charCodeAt(i);
|
||||||
hash = Math.imul(hash, 0x01000193) >>> 0;
|
hash = Math.imul(hash, 0x01000193) >>> 0;
|
||||||
}
|
}
|
||||||
return round2(minDay + (hash % 21) * 0.5);
|
return round2(DRAIN_DAY_MIN + (hash % DRAIN_DAY_STEPS) * 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -759,7 +768,7 @@ export function computeDayPlan(
|
|||||||
: priorDays.filter((d) => d.pnl !== 0);
|
: priorDays.filter((d) => d.pnl !== 0);
|
||||||
const dayTarget = qualifying.length >= minTradingDays
|
const dayTarget = qualifying.length >= minTradingDays
|
||||||
? 0
|
? 0
|
||||||
: drainDayAmount(accountId, tradeDate, minDayPnL || 5);
|
: drainDayAmount(accountId, tradeDate);
|
||||||
return {
|
return {
|
||||||
dayTarget,
|
dayTarget,
|
||||||
remainingToday: round2(dayTarget - realizedPnL),
|
remainingToday: round2(dayTarget - realizedPnL),
|
||||||
|
|||||||
Reference in New Issue
Block a user