Claim now: a button on the account page claims a payout account's profit
For a payout account on a firm that takes claims by API (Vest), the account page shows what it could claim right now — its whole profit above the account size — or why not (not flat, history loading, below its start). Pressing it asks first, then claims to the login's main account under the lane's mutex, with the same checks and bookkeeping as the bot's own claim at a stage's target: the account trades its next stage. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c109aff3e5
commit
71606ffc9e
+93
-7
@@ -1989,13 +1989,7 @@ async function runClaims(lane: string, ready: FirmClient[]): Promise<void> {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const { claimed, received } = await (client as any).claimProfit(acc.id, amount);
|
||||
runtime().legCounts.delete(acc.id);
|
||||
runtime().entryOutcomes?.delete(acc.id);
|
||||
runtime().claimFailedAt?.delete(acc.id);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const stage = 1 + ((client as any).sameDayClaims?.(acc.id) ?? 0);
|
||||
console.log(`[auto-claim] ${firm.name} ${acc.name}: claimed $${claimed}${received !== null ? ` (you get $${received})` : ''}`
|
||||
+ ` — trading stage ${stage} next (${tradeDate})`);
|
||||
afterClaim('[auto-claim]', firm, client, acc, claimed, received, tradeDate);
|
||||
} catch (err) {
|
||||
(runtime().claimFailedAt ??= new Map()).set(acc.id, Date.now());
|
||||
console.error(`[auto-claim] ${firm.name} ${acc.name}: claim of $${Math.floor(amount * 100) / 100} failed — ${errorMessage(err)}; retrying in ${CLAIM_RETRY_MS / 60_000}m`);
|
||||
@@ -2006,6 +2000,98 @@ async function runClaims(lane: string, ready: FirmClient[]): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/** A claim went through: the account starts its next stage — the day's leg
|
||||
* count and entry outcome are cleared, as if it hadn't traded today. */
|
||||
function afterClaim(
|
||||
tag: string, firm: TradableFirm, client: PlatformClient, acc: { id: number; name: string },
|
||||
claimed: number, received: number | null, tradeDate: string,
|
||||
): number {
|
||||
runtime().legCounts.delete(acc.id);
|
||||
runtime().entryOutcomes?.delete(acc.id);
|
||||
runtime().claimFailedAt?.delete(acc.id);
|
||||
const stage = 1 + (client.sameDayClaims?.(acc.id) ?? 0);
|
||||
console.log(`${tag} ${firm.name} ${acc.name}: claimed $${claimed}${received !== null ? ` (you get $${received})` : ''}`
|
||||
+ ` — trading stage ${stage} next (${tradeDate})`);
|
||||
return stage;
|
||||
}
|
||||
|
||||
/** A Claim now the account can't take as asked — said back to the person
|
||||
* as is (the claim never reached the platform). */
|
||||
export class ClaimRefusedError extends Error {}
|
||||
|
||||
/** Whether an account can be claimed by hand right now, and how much: its
|
||||
* whole profit above the account size, whatever its stage's target. */
|
||||
function manualClaim(accountId: number):
|
||||
| { firm: TradableFirm; client: PlatformClient; acc: { id: number; name: string }; amount: number; target: number | null }
|
||||
| { reason: string; unsupported?: true } {
|
||||
const { ready, unready } = firmClients();
|
||||
const holds = ({ client }: FirmClient) => client.accountList.some((a) => a.id === accountId);
|
||||
const item = ready.find(holds);
|
||||
if (!item) {
|
||||
const stale = unready.find(holds);
|
||||
if (stale && typeof stale.client.claimProfit !== 'function') return { reason: `${stale.firm.name} takes no claims through its API`, unsupported: true };
|
||||
return { reason: stale ? `${stale.firm.name} hasn't synced recently — wait for its next sync` : 'the account is not on a connected firm' };
|
||||
}
|
||||
const { firm, client } = item;
|
||||
if (typeof client.claimProfit !== 'function') return { reason: `${firm.name} takes no claims through its API`, unsupported: true };
|
||||
const acc = client.accountList.find((a) => a.id === accountId)!;
|
||||
const cfg = getAccountConfig(acc.id).accountType;
|
||||
if (!cfg) return { reason: 'no account type is assigned' };
|
||||
if (cfg.payoutable !== true) return { reason: `${cfg.label} doesn't take payouts`, unsupported: true };
|
||||
if (!acc.active) return { reason: `${firm.name} shows the account inactive` };
|
||||
if (client.positions[acc.id] || client.hasWorkingOrders?.(acc.id) || runtime().accountsInFlight.has(acc.id)) {
|
||||
return { reason: 'a position or order is open — the account must be flat' };
|
||||
}
|
||||
if (!client.historyReady(acc.id)) return { reason: 'its history is still loading' };
|
||||
const cash = client.accountCashBalances[acc.id];
|
||||
if (!cash) return { reason: 'its balance has not been read yet' };
|
||||
const profit = Math.floor((cash.amount - cfg.accountSize) * 100) / 100;
|
||||
if (!(profit >= 1)) {
|
||||
return { reason: `$${Math.abs(profit).toFixed(2)} ${profit < 0 ? 'below' : 'above'} its $${cfg.accountSize.toLocaleString()} start — nothing to claim (the least is $1)` };
|
||||
}
|
||||
const effective = resolveEffectiveConfig(
|
||||
cfg.profitTarget, cfg.consistency, cfg.minTradingDays, cfg.targetSameEquity, cfg.withdrawalStages,
|
||||
client.priorProfit?.[acc.id] ?? 0, client.fundTransactions?.[acc.id] ?? [], cfg.minDayPnL,
|
||||
stageInputsFor(client, acc.id, cfg),
|
||||
);
|
||||
return { firm, client, acc, amount: profit, target: effective.profitTarget > 0 ? effective.profitTarget : null };
|
||||
}
|
||||
|
||||
/** The dashboard's view of manualClaim. */
|
||||
export function claimableNow(accountId: number):
|
||||
| { claimable: number; target: number | null; firm: string }
|
||||
| { claimable: null; reason: string; unsupported: boolean } {
|
||||
const c = manualClaim(accountId);
|
||||
if ('reason' in c) return { claimable: null, reason: c.reason, unsupported: c.unsupported === true };
|
||||
return { claimable: c.amount, target: c.target, firm: c.firm.name };
|
||||
}
|
||||
|
||||
/**
|
||||
* Claim now, from the dashboard: the person pressed it, so the stage's
|
||||
* target doesn't matter — any profit from $1 up to all of it above the
|
||||
* account size. The same checks as an automatic claim, made again under the
|
||||
* lane's mutex (no batch reads the account mid-claim), and the same
|
||||
* bookkeeping after: the account trades its next stage.
|
||||
*/
|
||||
export async function claimAccountNow(accountId: number, amount: number): Promise<{ claimed: number; received: number | null; stage: number }> {
|
||||
const first = manualClaim(accountId);
|
||||
if ('reason' in first) throw new ClaimRefusedError(first.reason);
|
||||
const lane = laneOf(first.firm);
|
||||
const lease = acquireTradeMutex('[claim-now]', lane);
|
||||
if (lease === null) throw new ClaimRefusedError(`the bot is placing a trade on ${first.firm.name} right now — try again in a minute`);
|
||||
try {
|
||||
const c = manualClaim(accountId);
|
||||
if ('reason' in c) throw new ClaimRefusedError(c.reason);
|
||||
if (!(amount >= 1)) throw new ClaimRefusedError(`$${amount} is below the $1 minimum claim`);
|
||||
if (amount > c.amount + 0.005) throw new ClaimRefusedError(`only $${c.amount.toFixed(2)} is claimable now`);
|
||||
const { claimed, received } = await c.client.claimProfit!(c.acc.id, amount);
|
||||
const stage = afterClaim('[claim-now]', c.firm, c.client, c.acc, claimed, received, currentTradeDateCT());
|
||||
return { claimed, received, stage };
|
||||
} finally {
|
||||
releaseTradeMutex(lease, lane);
|
||||
}
|
||||
}
|
||||
|
||||
// ── eligibility check ────────────────────────────────────────────────────────
|
||||
|
||||
/** Returns true if any configured account could still trade today (eligible for a next leg / extra-day, or has an open position). */
|
||||
|
||||
Reference in New Issue
Block a user