Exit prices on the tick grid, and a refused take-profit flattens
4217.6 − 87 × 0.1 is 4208.900000000001 in floating point. TopstepX refused
that take-profit ("Invalid price. Price is outside allowed range") on two
GC shorts on 2026-10-02, and with no stop on the leg nothing else was
working: both rode far past their target with no exit. ES and NQ never hit
it because 0.25 is exact in binary; GC and CL ticks aren't.
- onTick puts the measured-fee take-profit and every stop on the grid —
the take-profit away from the entry, the stop toward it — so an averaged
multi-contract fill between ticks is handled the same way.
- A leg's lone take-profit (no stop, or the fallback after a refused
bracket) that the broker refuses now flattens the position and benches
the account for the day, like a refused bracket. An unknown outcome still
places nothing more.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d6bb5a7221
commit
8747dd6b08
+44
-9
@@ -12,7 +12,7 @@ import { getCachedFirm, getTradableFirms } from './firm-catalog';
|
||||
import { getAccountConfig } from './account-type-resolver';
|
||||
import {
|
||||
combineCmeSessions, computeDayPlan, groupLegFraction, pickLegAmount, resolveEffectiveConfig, currentTradeDateCT,
|
||||
obfuscationStopDollars, takeProfitPlan, percentFee, dllRoom, dllStopDollars, dllViable, stopTicks,
|
||||
obfuscationStopDollars, takeProfitPlan, onTick, percentFee, dllRoom, dllStopDollars, dllViable, stopTicks,
|
||||
expectedSwapToWin, planLegSize, maxLossRoom, breachDistance,
|
||||
DEFAULT_TARGET_HOLD_HOURS,
|
||||
isNoTradeTime, isPastSessionClose,
|
||||
@@ -63,9 +63,10 @@ interface TradeRuntime {
|
||||
/** Lane → when the batch holding that lane's trade mutex took it (absent
|
||||
* = no batch in flight there). See laneOf. */
|
||||
tradeInFlight?: Map<string, number>;
|
||||
/** Accounts whose exit bracket the broker refused while a hard daily loss
|
||||
* limit was on: the position was flattened and the account sits out the
|
||||
* trade date rather than bleed a round trip per leg. */
|
||||
/** Accounts whose exit the broker refused — a bracket while a hard daily
|
||||
* loss limit was on, or a lone take-profit: the position was flattened
|
||||
* and the account sits out the trade date rather than bleed a round trip
|
||||
* per leg. */
|
||||
exitRefused?: Map<number, string>;
|
||||
// What getSchedulerHealth reports.
|
||||
lastTickAt: number;
|
||||
@@ -627,7 +628,7 @@ function evaluateAccountForTrade(client: any, acc: { id: number; name: string; a
|
||||
if (cash.pendingRoll) return { ...base, eligible: false, reason: ROLL_PENDING };
|
||||
if (unknownEntryHolds(client, acc.id, tradeDate)) return { ...base, eligible: false, reason: ENTRY_UNKNOWN };
|
||||
if (runtime().exitRefused?.get(acc.id) === tradeDate) {
|
||||
return { ...base, eligible: false, reason: 'exit bracket refused — flattened, benched for the day' };
|
||||
return { ...base, eligible: false, reason: 'exit refused — flattened, benched for the day' };
|
||||
}
|
||||
|
||||
// Obfuscation stops: a stop-out shows as realized P&L falling short of the
|
||||
@@ -1173,6 +1174,39 @@ async function flattenAndFail(
|
||||
throw new Error(`entry filled @ ${entryPrice}; flattened — ${why}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* The take-profit of a leg with no stop. The position is already open, so a
|
||||
* refusal must not leave it riding with no exit at all — on 2026-10-02 two GC
|
||||
* shorts did, far past their target. Refused: flatten and bench the account
|
||||
* for the day, like a refused bracket. Outcome unknown: it may be working, and
|
||||
* a second exit on top could flip the account, so nothing more is placed.
|
||||
*/
|
||||
async function placeLoneExit(
|
||||
tag: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
client: any,
|
||||
acc: { id: number; name: string },
|
||||
contractName: string,
|
||||
contracts: number,
|
||||
exitAction: 'Buy' | 'Sell',
|
||||
exitPrice: number,
|
||||
entryPrice: number,
|
||||
tradeDate: string,
|
||||
): Promise<{ orderId?: number }> {
|
||||
try {
|
||||
return await client.placeOrderNoWait(acc.id, contractName, contracts, exitAction, 'Limit', exitPrice);
|
||||
} catch (err) {
|
||||
if (!isOrderNotPlaced(err)) {
|
||||
console.error(`${tag} ${acc.name}: take-profit outcome UNKNOWN (${errorMessage(err)}) — NOT placing a second exit; check this position's exits at the broker`);
|
||||
throw new Error(`entry filled @ ${entryPrice} but the take-profit's outcome is unknown — ${errorMessage(err)}`);
|
||||
}
|
||||
(runtime().exitRefused ??= new Map()).set(acc.id, tradeDate);
|
||||
console.error(`${tag} ${acc.name}: take-profit @ ${exitPrice} REJECTED (${errorMessage(err)}) — flattening`);
|
||||
return flattenAndFail(tag, client, acc, contractName, contracts, exitAction, entryPrice,
|
||||
`take-profit @ ${exitPrice} refused (${errorMessage(err)})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The leg an account trades next. Split-allowed big days carve a ≥$500
|
||||
* piece; everything else trades the full remaining amount in one go. The
|
||||
@@ -1389,9 +1423,10 @@ async function fireLeg(
|
||||
let exitOrder: { orderId?: number };
|
||||
let stopPrice: number | null = null;
|
||||
if (ticks !== null) {
|
||||
// On the grid toward the entry: the stop's distance is a hard limit.
|
||||
stopPrice = action === 'Buy'
|
||||
? fill.price - (ticks * legContract.tickSize)
|
||||
: fill.price + (ticks * legContract.tickSize);
|
||||
? onTick(fill.price - (ticks * legContract.tickSize), legContract.tickSize, 'up')
|
||||
: onTick(fill.price + (ticks * legContract.tickSize), legContract.tickSize, 'down');
|
||||
try {
|
||||
exitOrder = await client.placeOcoExit(acc.id, legContract.name, contracts, exitAction, tp.exitPrice, stopPrice);
|
||||
} catch (err) {
|
||||
@@ -1414,10 +1449,10 @@ async function fireLeg(
|
||||
// No limit: the position is already open — never leave it exit-less.
|
||||
console.error(`${tag} ${acc.name}: placeoco REJECTED (${errorMessage(err)}) — falling back to lone limit exit, NO STOP`);
|
||||
stopPrice = null;
|
||||
exitOrder = await client.placeOrderNoWait(acc.id, legContract.name, contracts, exitAction, 'Limit', tp.exitPrice);
|
||||
exitOrder = await placeLoneExit(tag, client, acc, legContract.name, contracts, exitAction, tp.exitPrice, fill.price, tradeDate);
|
||||
}
|
||||
} else {
|
||||
exitOrder = await client.placeOrderNoWait(acc.id, legContract.name, contracts, exitAction, 'Limit', tp.exitPrice);
|
||||
exitOrder = await placeLoneExit(tag, client, acc, legContract.name, contracts, exitAction, tp.exitPrice, fill.price, tradeDate);
|
||||
}
|
||||
|
||||
const instrumentLabel = legContract.stepNotional || item.drain ? legContract.name : symbolLabel;
|
||||
|
||||
Reference in New Issue
Block a user