Place OCO stop brackets for the catalog's obfuscation options
When an account type opts in, legs are placed as an OCO pair via order/placeoco - the usual limit plus a stop: the first cycle day's stop keeps the day's net floor at -$1,000, min-day trades stop at 2x their target, and the day after any red day rides stopless to make it back. A stop-out books the day: eligibility refuses the account once realized P&L falls short of the legs fired (or is simply negative on an opted-in account, which survives restarts). If placeoco is rejected the leg falls back to the classic lone limit with a loud log rather than leave a position without an exit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
ae0f93a44f
commit
c58e981b1e
@@ -38,6 +38,8 @@ export async function GET() {
|
|||||||
targetSameEquity: a.targetSameEquity,
|
targetSameEquity: a.targetSameEquity,
|
||||||
splitEnabled: a.splitEnabled,
|
splitEnabled: a.splitEnabled,
|
||||||
minLegs: a.minLegs,
|
minLegs: a.minLegs,
|
||||||
|
firstDayStop: a.firstDayStop,
|
||||||
|
minDayStop: a.minDayStop,
|
||||||
combineCmeHolidays: a.combineCmeHolidays,
|
combineCmeHolidays: a.combineCmeHolidays,
|
||||||
withdrawalStages: a.withdrawalStages,
|
withdrawalStages: a.withdrawalStages,
|
||||||
})),
|
})),
|
||||||
|
|||||||
+98
-12
@@ -14,6 +14,7 @@ import { getAccountConfig } from './account-type-resolver';
|
|||||||
import {
|
import {
|
||||||
combineCmeSessions, computeDayPlan, groupLegFraction, pickLegAmount, resolveEffectiveConfig, currentTradeDateCT,
|
combineCmeSessions, computeDayPlan, groupLegFraction, pickLegAmount, resolveEffectiveConfig, currentTradeDateCT,
|
||||||
BIG_DAY_THRESHOLD, LEG_COMPLETION_EPSILON, MAX_LEGS_PER_DAY, POINT_VALUES,
|
BIG_DAY_THRESHOLD, LEG_COMPLETION_EPSILON, MAX_LEGS_PER_DAY, POINT_VALUES,
|
||||||
|
FIRST_DAY_STOP, MIN_DAY_STOP_MULT,
|
||||||
type DayPlan,
|
type DayPlan,
|
||||||
} from './trading-logic';
|
} from './trading-logic';
|
||||||
import { getSetting, setSetting } from './db';
|
import { getSetting, setSetting } from './db';
|
||||||
@@ -24,17 +25,51 @@ function isAccountDead(amount: number, autoLiqThreshold: number): boolean {
|
|||||||
return autoLiqThreshold > 0 && amount <= autoLiqThreshold;
|
return autoLiqThreshold > 0 && amount <= autoLiqThreshold;
|
||||||
}
|
}
|
||||||
|
|
||||||
// In-memory runaway guard: legs fired per account per trade date. Resets on restart,
|
// In-memory runaway guard: legs fired per account per trade date, plus the sum
|
||||||
// which is fine — day completion is derived from realized PnL, not from this counter.
|
// of their targets — realized P&L falling short of that sum is how a stop-out
|
||||||
const _legCounts = new Map<number, { date: string; count: number }>();
|
// is detected. Resets on restart, which is fine — day completion is derived
|
||||||
|
// from realized PnL, not from this counter.
|
||||||
|
const _legCounts = new Map<number, { date: string; count: number; sum: number }>();
|
||||||
|
|
||||||
function legsToday(accountId: number, tradeDate: string): number {
|
function legsToday(accountId: number, tradeDate: string): number {
|
||||||
const entry = _legCounts.get(accountId);
|
const entry = _legCounts.get(accountId);
|
||||||
return entry && entry.date === tradeDate ? entry.count : 0;
|
return entry && entry.date === tradeDate ? entry.count : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function recordLeg(accountId: number, tradeDate: string): void {
|
function legSumToday(accountId: number, tradeDate: string): number {
|
||||||
_legCounts.set(accountId, { date: tradeDate, count: legsToday(accountId, tradeDate) + 1 });
|
const entry = _legCounts.get(accountId);
|
||||||
|
return entry && entry.date === tradeDate ? entry.sum : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function recordLeg(accountId: number, tradeDate: string, legAmount: number): void {
|
||||||
|
_legCounts.set(accountId, {
|
||||||
|
date: tradeDate,
|
||||||
|
count: legsToday(accountId, tradeDate) + 1,
|
||||||
|
sum: Math.round((legSumToday(accountId, tradeDate) + legAmount) * 100) / 100,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Obfuscation stop for the current leg, in dollars. Infinity = no stop.
|
||||||
|
* Day 1 of a cycle carries the $1,000 account stop (firstDayStop), min-day
|
||||||
|
* (small) trades a 2×-target stop (minDayStop) — and the day after a losing
|
||||||
|
* day always rides stopless, the make-back day. */
|
||||||
|
function obfuscationStopDollars(
|
||||||
|
cfg: { firstDayStop: boolean; minDayStop: boolean },
|
||||||
|
dailyPnL: { date: string; pnl: number }[],
|
||||||
|
plan: DayPlan,
|
||||||
|
tradeDate: string,
|
||||||
|
legAmount: number,
|
||||||
|
): number {
|
||||||
|
if (!cfg.firstDayStop && !cfg.minDayStop) return Infinity;
|
||||||
|
const prior = dailyPnL
|
||||||
|
.filter((d) => d.date < tradeDate && d.pnl !== 0)
|
||||||
|
.sort((a, b) => a.date.localeCompare(b.date));
|
||||||
|
if (prior.length > 0 && prior[prior.length - 1].pnl < 0) return Infinity;
|
||||||
|
let stop = Infinity;
|
||||||
|
const realizedToday = Math.round((plan.dayTarget - plan.remainingToday) * 100) / 100;
|
||||||
|
if (cfg.firstDayStop && prior.length === 0) stop = Math.max(0, FIRST_DAY_STOP + realizedToday);
|
||||||
|
if (cfg.minDayStop && plan.dayTarget <= BIG_DAY_THRESHOLD) stop = Math.min(stop, MIN_DAY_STOP_MULT * legAmount);
|
||||||
|
return stop;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── per-account trade evaluation (shared by runTrade / copyTrade / hasRemaining…) ──
|
// ── per-account trade evaluation (shared by runTrade / copyTrade / hasRemaining…) ──
|
||||||
@@ -80,6 +115,20 @@ function evaluateAccountForTrade(client: any, acc: { id: number; name: string; a
|
|||||||
if (!acc.active) return { ...base, eligible: false, reason: 'inactive (DLL)' };
|
if (!acc.active) return { ...base, eligible: false, reason: 'inactive (DLL)' };
|
||||||
if (client.positions[acc.id]) return { ...base, eligible: false, reason: 'already in trade' };
|
if (client.positions[acc.id]) return { ...base, eligible: false, reason: 'already in trade' };
|
||||||
|
|
||||||
|
// Obfuscation stops: a stop-out shows as realized P&L falling short of the
|
||||||
|
// legs fired today — the day is booked; the make-back ride is tomorrow.
|
||||||
|
// The plain negative-realized check is what survives a restart, when the
|
||||||
|
// in-memory leg counter has forgotten the morning's stop-out.
|
||||||
|
if (cfg.firstDayStop || cfg.minDayStop) {
|
||||||
|
if (combined.realizedPnL <= -25) {
|
||||||
|
return { ...base, eligible: false, reason: 'red day booked — make-back rides tomorrow' };
|
||||||
|
}
|
||||||
|
if (legsToday(acc.id, tradeDate) > 0
|
||||||
|
&& combined.realizedPnL < legSumToday(acc.id, tradeDate) - 25) {
|
||||||
|
return { ...base, eligible: false, reason: 'stopped out — day booked' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const priorProfit = client.priorProfit?.[acc.id] ?? 0;
|
const priorProfit = client.priorProfit?.[acc.id] ?? 0;
|
||||||
const allFundTxns = client.fundTransactions?.[acc.id] ?? [];
|
const allFundTxns = client.fundTransactions?.[acc.id] ?? [];
|
||||||
const effective = resolveEffectiveConfig(
|
const effective = resolveEffectiveConfig(
|
||||||
@@ -368,7 +417,7 @@ export async function runTrade(action: 'Buy' | 'Sell' | 'Auto', symbol: string)
|
|||||||
minLegs - legsToday(acc.id, tradeDate),
|
minLegs - legsToday(acc.id, tradeDate),
|
||||||
)
|
)
|
||||||
: item.plan.remainingToday;
|
: item.plan.remainingToday;
|
||||||
recordLeg(acc.id, tradeDate);
|
recordLeg(acc.id, tradeDate, legAmount);
|
||||||
|
|
||||||
const rawContracts = Math.max(1, Math.ceil(legAmount / 1000));
|
const rawContracts = Math.max(1, Math.ceil(legAmount / 1000));
|
||||||
const contracts = cfg.maxPositionSize > 0 ? Math.min(rawContracts, cfg.maxPositionSize) : rawContracts;
|
const contracts = cfg.maxPositionSize > 0 ? Math.min(rawContracts, cfg.maxPositionSize) : rawContracts;
|
||||||
@@ -391,9 +440,28 @@ export async function runTrade(action: 'Buy' | 'Sell' | 'Auto', symbol: string)
|
|||||||
: fill.price - (ticks * contract.tickSize);
|
: fill.price - (ticks * contract.tickSize);
|
||||||
|
|
||||||
const exitAction: 'Buy' | 'Sell' = resolvedAction === 'Buy' ? 'Sell' : 'Buy';
|
const exitAction: 'Buy' | 'Sell' = resolvedAction === 'Buy' ? 'Sell' : 'Buy';
|
||||||
const exitOrder = await client.placeOrderNoWait(acc.id, contract.name, contracts, exitAction, 'Limit', exitPrice);
|
const stopDollars = obfuscationStopDollars(cfg, item.dailyPnL, item.plan, tradeDate, legAmount);
|
||||||
|
let exitOrder: { orderId?: number };
|
||||||
|
let stopPrice: number | null = null;
|
||||||
|
if (Number.isFinite(stopDollars) && stopDollars > 0) {
|
||||||
|
const stopTicks = Math.max(1, Math.ceil(stopDollars / (pointValue * contracts) / contract.tickSize));
|
||||||
|
stopPrice = resolvedAction === 'Buy'
|
||||||
|
? fill.price - (stopTicks * contract.tickSize)
|
||||||
|
: fill.price + (stopTicks * contract.tickSize);
|
||||||
|
try {
|
||||||
|
exitOrder = await client.placeOcoExit(acc.id, contract.name, contracts, exitAction, exitPrice, stopPrice);
|
||||||
|
} catch (err) {
|
||||||
|
// The position is already open — never leave it exit-less. Fall
|
||||||
|
// back to the classic lone limit and shout about it.
|
||||||
|
console.error(`[auto-trade] ${acc.name}: placeoco REJECTED (${err instanceof Error ? err.message : err}) — falling back to lone limit exit, NO STOP`);
|
||||||
|
stopPrice = null;
|
||||||
|
exitOrder = await client.placeOrderNoWait(acc.id, contract.name, contracts, exitAction, 'Limit', exitPrice);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
exitOrder = await client.placeOrderNoWait(acc.id, contract.name, contracts, exitAction, 'Limit', exitPrice);
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`[auto-trade] ${acc.name} (${item.firmName}) ${resolvedAction} ${contracts}x${resolvedSymbol} @ ${fill.price} | leg $${legAmount} of day $${item.plan.dayTarget} (remaining $${item.plan.remainingToday}) [${item.plan.path}] (+$${totalCommission.toFixed(2)} comm) | exit @ ${exitPrice} (orderId=${exitOrder.orderId})`);
|
console.log(`[auto-trade] ${acc.name} (${item.firmName}) ${resolvedAction} ${contracts}x${resolvedSymbol} @ ${fill.price} | leg $${legAmount} of day $${item.plan.dayTarget} (remaining $${item.plan.remainingToday}) [${item.plan.path}] (+$${totalCommission.toFixed(2)} comm) | exit @ ${exitPrice}${stopPrice !== null ? ` / stop @ ${stopPrice}` : ''} (orderId=${exitOrder.orderId})`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
account: acc.name,
|
account: acc.name,
|
||||||
@@ -522,6 +590,7 @@ export async function copyTrade() {
|
|||||||
acc: { id: number; name: string; active: boolean };
|
acc: { id: number; name: string; active: boolean };
|
||||||
contract: { name: string; tickSize: number };
|
contract: { name: string; tickSize: number };
|
||||||
cash: { amount: number; realizedPnL: number };
|
cash: { amount: number; realizedPnL: number };
|
||||||
|
dailyPnL: { date: string; pnl: number }[];
|
||||||
plan: DayPlan;
|
plan: DayPlan;
|
||||||
prefix: string;
|
prefix: string;
|
||||||
};
|
};
|
||||||
@@ -552,7 +621,7 @@ export async function copyTrade() {
|
|||||||
if (ev.isExtraDay) { console.log(`[copy-trade] ${acc.name}: extra-day account — skip`); continue; }
|
if (ev.isExtraDay) { console.log(`[copy-trade] ${acc.name}: extra-day account — skip`); continue; }
|
||||||
|
|
||||||
eligible.push({
|
eligible.push({
|
||||||
firmName: firm.name, client, acc, contract, cash: ev.cash, plan: ev.plan,
|
firmName: firm.name, client, acc, contract, cash: ev.cash, dailyPnL: ev.dailyPnL, plan: ev.plan,
|
||||||
prefix: getAccountConfig(acc.id).accountType?.prefix ?? '',
|
prefix: getAccountConfig(acc.id).accountType?.prefix ?? '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -583,7 +652,7 @@ export async function copyTrade() {
|
|||||||
minLegs - legsToday(acc.id, tradeDate),
|
minLegs - legsToday(acc.id, tradeDate),
|
||||||
)
|
)
|
||||||
: item.plan.remainingToday;
|
: item.plan.remainingToday;
|
||||||
recordLeg(acc.id, tradeDate);
|
recordLeg(acc.id, tradeDate, legAmount);
|
||||||
|
|
||||||
const rawContracts = Math.max(1, Math.ceil(legAmount / 1000));
|
const rawContracts = Math.max(1, Math.ceil(legAmount / 1000));
|
||||||
const contracts = cfg.maxPositionSize > 0 ? Math.min(rawContracts, cfg.maxPositionSize) : rawContracts;
|
const contracts = cfg.maxPositionSize > 0 ? Math.min(rawContracts, cfg.maxPositionSize) : rawContracts;
|
||||||
@@ -602,9 +671,26 @@ export async function copyTrade() {
|
|||||||
: fill.price - (ticks * contract.tickSize);
|
: fill.price - (ticks * contract.tickSize);
|
||||||
|
|
||||||
const exitAction: 'Buy' | 'Sell' = resolvedAction === 'Buy' ? 'Sell' : 'Buy';
|
const exitAction: 'Buy' | 'Sell' = resolvedAction === 'Buy' ? 'Sell' : 'Buy';
|
||||||
const exitOrder = await client.placeOrderNoWait(acc.id, contract.name, contracts, exitAction, 'Limit', exitPrice);
|
const stopDollars = obfuscationStopDollars(cfg, item.dailyPnL, item.plan, tradeDate, legAmount);
|
||||||
|
let exitOrder: { orderId?: number };
|
||||||
|
let stopPrice: number | null = null;
|
||||||
|
if (Number.isFinite(stopDollars) && stopDollars > 0) {
|
||||||
|
const stopTicks = Math.max(1, Math.ceil(stopDollars / (pointValue * contracts) / contract.tickSize));
|
||||||
|
stopPrice = resolvedAction === 'Buy'
|
||||||
|
? fill.price - (stopTicks * contract.tickSize)
|
||||||
|
: fill.price + (stopTicks * contract.tickSize);
|
||||||
|
try {
|
||||||
|
exitOrder = await client.placeOcoExit(acc.id, contract.name, contracts, exitAction, exitPrice, stopPrice);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[copy-trade] ${acc.name}: placeoco REJECTED (${err instanceof Error ? err.message : err}) — falling back to lone limit exit, NO STOP`);
|
||||||
|
stopPrice = null;
|
||||||
|
exitOrder = await client.placeOrderNoWait(acc.id, contract.name, contracts, exitAction, 'Limit', exitPrice);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
exitOrder = await client.placeOrderNoWait(acc.id, contract.name, contracts, exitAction, 'Limit', exitPrice);
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`[copy-trade] ${acc.name} (${item.firmName}) ${resolvedAction} ${contracts}x${resolvedSymbol} @ ${fill.price} | leg $${legAmount} of day $${item.plan.dayTarget} (remaining $${item.plan.remainingToday}) [${item.plan.path}] | exit @ ${exitPrice} (orderId=${exitOrder.orderId})`);
|
console.log(`[copy-trade] ${acc.name} (${item.firmName}) ${resolvedAction} ${contracts}x${resolvedSymbol} @ ${fill.price} | leg $${legAmount} of day $${item.plan.dayTarget} (remaining $${item.plan.remainingToday}) [${item.plan.path}] | exit @ ${exitPrice}${stopPrice !== null ? ` / stop @ ${stopPrice}` : ''} (orderId=${exitOrder.orderId})`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
account: acc.name,
|
account: acc.name,
|
||||||
|
|||||||
@@ -163,6 +163,9 @@ function validatePayload(raw: unknown): FirmCatalogPayload {
|
|||||||
combineCmeHolidays: type.combineCmeHolidays === true,
|
combineCmeHolidays: type.combineCmeHolidays === true,
|
||||||
// Lenient: a master from before the min-legs field means "no minimum".
|
// Lenient: a master from before the min-legs field means "no minimum".
|
||||||
minLegs: typeof type.minLegs === 'number' ? type.minLegs : 0,
|
minLegs: typeof type.minLegs === 'number' ? type.minLegs : 0,
|
||||||
|
// Lenient: the obfuscation stops are opt-in, so absent means off.
|
||||||
|
firstDayStop: type.firstDayStop === true,
|
||||||
|
minDayStop: type.minDayStop === true,
|
||||||
} as unknown as CatalogAccountType;
|
} as unknown as CatalogAccountType;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -114,6 +114,12 @@ export interface CatalogAccountType {
|
|||||||
* legs (0 = however the carve falls). Smaller days always trade as one
|
* legs (0 = however the carve falls). Smaller days always trade as one
|
||||||
* leg; clamped to what ≥$500 pieces make possible. */
|
* leg; clamped to what ≥$500 pieces make possible. */
|
||||||
minLegs: number;
|
minLegs: number;
|
||||||
|
/** Obfuscation, part 1: the first trading day of a cycle carries a $1,000
|
||||||
|
* account stop — the day books red and ends instead of riding. */
|
||||||
|
firstDayStop: boolean;
|
||||||
|
/** Obfuscation, part 2: min-day (small) trades carry a 2×-target stop; a
|
||||||
|
* stopped-out day books, and the next day rides stopless. */
|
||||||
|
minDayStop: boolean;
|
||||||
/** True = CME holiday sessions count with the next full trading day, the way
|
/** True = CME holiday sessions count with the next full trading day, the way
|
||||||
* firms that follow CME clearing report them (e.g. Apex: Sun/Mon/Tue around
|
* firms that follow CME clearing report them (e.g. Apex: Sun/Mon/Tue around
|
||||||
* a Monday holiday is ONE trading day, recorded on the Tuesday). */
|
* a Monday holiday is ONE trading day, recorded on the Tuesday). */
|
||||||
|
|||||||
@@ -56,6 +56,10 @@ export const BIG_DAY_THRESHOLD = 1700;
|
|||||||
export const MIN_LEG = 500;
|
export const MIN_LEG = 500;
|
||||||
/** After at least one trade, a residual at or below this counts as "day done". */
|
/** After at least one trade, a residual at or below this counts as "day done". */
|
||||||
export const LEG_COMPLETION_EPSILON = 25;
|
export const LEG_COMPLETION_EPSILON = 25;
|
||||||
|
/** firstDayStop: the first cycle day books at this loss and stops. */
|
||||||
|
export const FIRST_DAY_STOP = 1000;
|
||||||
|
/** minDayStop: a min-day trade's stop sits at this multiple of its target. */
|
||||||
|
export const MIN_DAY_STOP_MULT = 2;
|
||||||
/** Safety valve: never fire more than this many legs per account per trade date. */
|
/** Safety valve: never fire more than this many legs per account per trade date. */
|
||||||
export const MAX_LEGS_PER_DAY = 8;
|
export const MAX_LEGS_PER_DAY = 8;
|
||||||
|
|
||||||
|
|||||||
@@ -951,6 +951,52 @@ export class TradovateClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Place an OCO exit pair after an entry fill: a limit take-profit linked
|
||||||
|
* to a stop, via order/placeoco — Tradovate cancels the sibling when one
|
||||||
|
* fills. Resolves on command ack (does not wait for a fill), like
|
||||||
|
* placeOrderNoWait. */
|
||||||
|
async placeOcoExit(
|
||||||
|
accountId: number,
|
||||||
|
contractSymbol: string,
|
||||||
|
quantity: number,
|
||||||
|
exitAction: 'Buy' | 'Sell',
|
||||||
|
limitPrice: number,
|
||||||
|
stopPrice: number,
|
||||||
|
): Promise<{ orderId?: number }> {
|
||||||
|
if (!this.ws) throw new Error('WebSocket not connected');
|
||||||
|
|
||||||
|
const msgId = this.getMsgId();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.directEventCallbacks[msgId] = (response: any) => {
|
||||||
|
if (typeof response === 'string') {
|
||||||
|
reject(new Error(response));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve({ orderId: response?.orderId ?? response?.id });
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.send(
|
||||||
|
`order/placeoco\n${msgId}\n\n${JSON.stringify({
|
||||||
|
accountId,
|
||||||
|
action: exitAction,
|
||||||
|
symbol: contractSymbol,
|
||||||
|
orderQty: quantity,
|
||||||
|
orderType: 'Limit',
|
||||||
|
price: limitPrice,
|
||||||
|
timeInForce: 'Day',
|
||||||
|
text: 'DOM',
|
||||||
|
other: {
|
||||||
|
action: exitAction,
|
||||||
|
orderType: 'Stop',
|
||||||
|
stopPrice,
|
||||||
|
timeInForce: 'Day',
|
||||||
|
text: 'DOM',
|
||||||
|
},
|
||||||
|
})}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** Close the WebSocket connection and stop all intervals. Call before discarding the instance. */
|
/** Close the WebSocket connection and stop all intervals. Call before discarding the instance. */
|
||||||
public disconnect(): void {
|
public disconnect(): void {
|
||||||
this.intentionalDisconnect = true;
|
this.intentionalDisconnect = true;
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ export interface AccountConfig {
|
|||||||
targetSameEquity: boolean; // if true, reduce profitTarget by priorProfit after withdrawal
|
targetSameEquity: boolean; // if true, reduce profitTarget by priorProfit after withdrawal
|
||||||
splitEnabled: boolean; // false = big days trade as one unsplit leg instead of random ≥$500 pieces
|
splitEnabled: boolean; // false = big days trade as one unsplit leg instead of random ≥$500 pieces
|
||||||
minLegs: number; // big days (> $1,700) must carve at least this many legs (0 = however the carve falls)
|
minLegs: number; // big days (> $1,700) must carve at least this many legs (0 = however the carve falls)
|
||||||
|
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
|
||||||
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 }[]; // per-stage fresh profit + consistency (empty = use base values)
|
withdrawalStages: { profit: number; consistency: number; minTradingDays: number }[]; // per-stage fresh profit + consistency (empty = use base values)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user