Merge branch 'vest-fix-intent' into vest-fixes
This commit is contained in:
+68
-1
@@ -55,6 +55,9 @@ export interface PlatformContract {
|
||||
closesAt?: number;
|
||||
}
|
||||
|
||||
/** What an order is for, where the caller knows (see sendOrder). */
|
||||
export type OrderIntent = 'open' | 'close';
|
||||
|
||||
export interface PlatformClient {
|
||||
accountList: PlatformAccount[];
|
||||
/** realizedPnL is today's. pendingRoll (Tradovate): the broker still shows
|
||||
@@ -181,13 +184,20 @@ export interface PlatformClient {
|
||||
findFrontMonthContract(productName: string, pick?: PickContext): Promise<PlatformContract | null>;
|
||||
fetchInstrumentFees(symbols: string[]): Promise<{ [symbol: string]: number }>;
|
||||
|
||||
/** `intent`, where the caller knows what the order is for: an entry
|
||||
* ('open') must never close a position it didn't know was there, a
|
||||
* flatten ('close') must never open one on an account that turned out
|
||||
* flat. A platform that checks it (orderEffect) refuses the order
|
||||
* otherwise (NothingToCloseError for a close with nothing to close);
|
||||
* left out, or on a platform that doesn't check, the position decides. */
|
||||
sendOrder(
|
||||
accountId: number,
|
||||
contractSymbol: string,
|
||||
quantity: number,
|
||||
action: 'Buy' | 'Sell',
|
||||
orderType: 'Market' | 'Limit',
|
||||
price?: number
|
||||
price?: number,
|
||||
intent?: OrderIntent,
|
||||
): Promise<Record<string, unknown>>;
|
||||
|
||||
placeOrderNoWait(
|
||||
@@ -468,6 +478,63 @@ export function isOrderNotPlaced(err: unknown): boolean {
|
||||
return !!err && typeof err === 'object' && (err as { orderNotPlaced?: unknown }).orderNotPlaced === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* A close-only order (sendOrder's 'close') refused because a fresh read
|
||||
* showed no position in the symbol to close, and the client's own record
|
||||
* agrees: the account is flat there, so a flatten that gets this has nothing
|
||||
* left to do — nothing rides without its stop.
|
||||
*/
|
||||
export class NothingToCloseError extends OrderNotPlacedError {
|
||||
readonly nothingToClose = true;
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'NothingToCloseError';
|
||||
}
|
||||
}
|
||||
|
||||
/** Duck-typed, like isOrderNotPlaced. */
|
||||
export function isNothingToClose(err: unknown): boolean {
|
||||
return !!err && typeof err === 'object' && (err as { nothingToClose?: unknown }).nothingToClose === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* What an order on `side` does on a position-based platform (Vest, DXtrade),
|
||||
* given a fresh read of the account's position in the symbol (`held`: its
|
||||
* side, null when flat) and the client's own last record of it (`recorded`):
|
||||
* open, or close (or reduce) — or it throws why nothing is sent. With no
|
||||
* intent a flat account opens and the opposite side closes. An entry ('open')
|
||||
* never closes: a hand trade the engine didn't know about would be closed in
|
||||
* its place, and the leg's flatten would then open a position with no exits.
|
||||
* A flatten ('close') never opens, and a flat read is NothingToCloseError only
|
||||
* when the record agrees — one empty read just after a fill the client saw
|
||||
* may be the gateway lagging, and a leg riding without its stop must still be
|
||||
* shouted about. Adding to a position is never supported.
|
||||
*/
|
||||
export function orderEffect(o: {
|
||||
symbol: string;
|
||||
held: 'long' | 'short' | null;
|
||||
recorded: 'long' | 'short' | null;
|
||||
side: 'long' | 'short';
|
||||
intent?: OrderIntent;
|
||||
}): 'open' | 'close' {
|
||||
const { symbol, held, recorded, side, intent } = o;
|
||||
if (held === side) {
|
||||
throw new OrderNotPlacedError(intent === 'close'
|
||||
? `the account holds a ${held} ${symbol} position, not one to close — nothing sent`
|
||||
: `adding to an open ${symbol} position is not supported`);
|
||||
}
|
||||
if (held && intent === 'open') {
|
||||
throw new OrderNotPlacedError(`a ${held} ${symbol} position is already open — an entry never closes it, order not sent`);
|
||||
}
|
||||
if (!held && intent === 'close') {
|
||||
if (recorded && recorded !== side) {
|
||||
throw new OrderNotPlacedError(`a fresh read shows no ${symbol} position, but this client last saw a ${recorded} one open — nothing sent`);
|
||||
}
|
||||
throw new NothingToCloseError(`no ${symbol} position to close — nothing sent`);
|
||||
}
|
||||
return held ? 'close' : 'open';
|
||||
}
|
||||
|
||||
/** An error's message without the rest of it. AxiosErrors carry the request
|
||||
* config — Tradovate's login body (the password) and Bearer tokens — so they
|
||||
* must never be logged whole. */
|
||||
|
||||
Reference in New Issue
Block a user