Report each firm's trading mix: open positions and today's entries by instrument
The master knew how many accounts were in a trade, not in what. Each firm's report now carries its open positions by instrument (accounts long and short, contracts held) and today's entries the same way. A position's contract is named by its instrument: DXtrade's carries the symbol; a futures one is looked up among the contracts this machine has entered (recorded at the fill and kept), then the resolver's cache and TopstepX's own. Entries are counted as they fill, per trade date, in a setting, so a restart doesn't lose the day. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ab3132182a
commit
5696f2b036
@@ -25,6 +25,7 @@ import { getSetting, setSetting } from './db';
|
|||||||
import type { LaneTick, TickReason } from './master-protocol';
|
import type { LaneTick, TickReason } from './master-protocol';
|
||||||
import { POINT_VALUES, housekeepingRoot, MAX_HOLD_HOURS } from './instrument-specs';
|
import { POINT_VALUES, housekeepingRoot, MAX_HOLD_HOURS } from './instrument-specs';
|
||||||
import { firmInstruments, drawFuturesInstrument, spreadBlock, type LegShape } from './instruments';
|
import { firmInstruments, drawFuturesInstrument, spreadBlock, type LegShape } from './instruments';
|
||||||
|
import { recordEntry } from './trade-mix';
|
||||||
import { errorMessage, isClientReady, isOrderNotPlaced, type PlatformClient, type PlatformContract, type PickContext } from './platform-client';
|
import { errorMessage, isClientReady, isOrderNotPlaced, type PlatformClient, type PlatformContract, type PickContext } from './platform-client';
|
||||||
|
|
||||||
// ── process-wide state ───────────────────────────────────────────────────────
|
// ── process-wide state ───────────────────────────────────────────────────────
|
||||||
@@ -1082,6 +1083,7 @@ async function runTradeLocked(
|
|||||||
if (!mnqContract) throw new Error(`${microRoot} contract not found for extra-day trade`);
|
if (!mnqContract) throw new Error(`${microRoot} contract not found for extra-day trade`);
|
||||||
|
|
||||||
const fill = await client.sendOrder(acc.id, mnqContract.name, 1, resolvedAction, 'Market');
|
const fill = await client.sendOrder(acc.id, mnqContract.name, 1, resolvedAction, 'Market');
|
||||||
|
recordEntry(item.firmName, microRoot, resolvedAction, 1, mnqContract.id);
|
||||||
const exitAction: 'Buy' | 'Sell' = resolvedAction === 'Buy' ? 'Sell' : 'Buy';
|
const exitAction: 'Buy' | 'Sell' = resolvedAction === 'Buy' ? 'Sell' : 'Buy';
|
||||||
const exitFill = await client.sendOrder(acc.id, mnqContract.name, 1, exitAction, 'Market');
|
const exitFill = await client.sendOrder(acc.id, mnqContract.name, 1, exitAction, 'Market');
|
||||||
|
|
||||||
@@ -1318,6 +1320,16 @@ async function fireLeg(
|
|||||||
const { sizing, obfuscation } = plan;
|
const { sizing, obfuscation } = plan;
|
||||||
|
|
||||||
const fill = await sendEntry(tag, client, acc, legContract.name, contracts, action, tradeDate, legAmount);
|
const fill = await sendEntry(tag, client, acc, legContract.name, contracts, action, tradeDate, legAmount);
|
||||||
|
// For the master's trade mix: a futures leg is its root (the drain
|
||||||
|
// micro on a drain day), a DXtrade one its symbol.
|
||||||
|
const dx = (legContract.stepNotional ?? 0) > 0;
|
||||||
|
recordEntry(
|
||||||
|
item.firmName,
|
||||||
|
dx ? String(legContract.name).split('|')[0] : (item.drain ? housekeepingFor(item.firm) : symbolLabel),
|
||||||
|
action,
|
||||||
|
dx ? contracts * (Number(String(legContract.name).split('|')[1]) || 1) : contracts,
|
||||||
|
(legContract as { id?: unknown }).id,
|
||||||
|
);
|
||||||
const exitAction: 'Buy' | 'Sell' = action === 'Buy' ? 'Sell' : 'Buy';
|
const exitAction: 'Buy' | 'Sell' = action === 'Buy' ? 'Sell' : 'Buy';
|
||||||
|
|
||||||
// The entry fee. Percent firms: straight off the fill. Otherwise wait
|
// The entry fee. Percent firms: straight off the fill. Otherwise wait
|
||||||
|
|||||||
@@ -119,6 +119,23 @@ export interface FirmStats {
|
|||||||
/** Futures firms: each CME root's entry cost, movement, expected hold and
|
/** Futures firms: each CME root's entry cost, movement, expected hold and
|
||||||
* chance in the Auto draw right now. Optional. */
|
* chance in the Auto draw right now. Optional. */
|
||||||
instrumentStats?: InstrumentStats;
|
instrumentStats?: InstrumentStats;
|
||||||
|
/** What the firm is trading, by instrument: its open positions and
|
||||||
|
* today's entries (lib/trade-mix.ts). Optional. */
|
||||||
|
mix?: TradeMix;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One instrument's share of a firm's trading: accounts (open) or entries
|
||||||
|
* (today) on each side, and the contracts in them — units on DXtrade. */
|
||||||
|
export interface InstrumentCount {
|
||||||
|
instrument: string;
|
||||||
|
long: number;
|
||||||
|
short: number;
|
||||||
|
contracts: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TradeMix {
|
||||||
|
open: InstrumentCount[];
|
||||||
|
today: InstrumentCount[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A futures firm's instruments as the Auto draw sees them. */
|
/** A futures firm's instruments as the Auto draw sees them. */
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
import { getSetting, loadEndedCycleStart } from './db';
|
import { getSetting, loadEndedCycleStart } from './db';
|
||||||
import { pushReport } from './master-link';
|
import { pushReport } from './master-link';
|
||||||
import { getBuildVersion } from './version';
|
import { getBuildVersion } from './version';
|
||||||
|
import { tradeMixFor } from './trade-mix';
|
||||||
import { updaterStatus } from './updater-status';
|
import { updaterStatus } from './updater-status';
|
||||||
import { getClients } from './clients';
|
import { getClients } from './clients';
|
||||||
import { getTradableFirms } from './firm-catalog';
|
import { getTradableFirms } from './firm-catalog';
|
||||||
@@ -392,6 +393,7 @@ function collectFirmStats() {
|
|||||||
accountTypes,
|
accountTypes,
|
||||||
...symbolStatsFor(f, client, rawToday),
|
...symbolStatsFor(f, client, rawToday),
|
||||||
...instrumentStatsBlock(f, client, rawToday),
|
...instrumentStatsBlock(f, client, rawToday),
|
||||||
|
mix: tradeMixFor(f, client),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1758,6 +1758,12 @@ export class TopstepXClient implements PlatformClient {
|
|||||||
|
|
||||||
// ── lifecycle ────────────────────────────────────────────────────────────
|
// ── lifecycle ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Contract id → the root it was resolved for, for the reporter's trade
|
||||||
|
* mix (lib/trade-mix.ts). */
|
||||||
|
public contractRoots(): Map<string, string> {
|
||||||
|
return new Map([...this.contractByProduct].map(([root, { contract }]) => [String(contract.id), root]));
|
||||||
|
}
|
||||||
|
|
||||||
/** The session token, for the live quote feed's market hub
|
/** The session token, for the live quote feed's market hub
|
||||||
* (lib/live-quotes.ts); null while logged out. */
|
* (lib/live-quotes.ts); null while logged out. */
|
||||||
public quoteToken(): string | null {
|
public quoteToken(): string | null {
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
/**
|
||||||
|
* What each firm is trading, by instrument — its open positions now and its
|
||||||
|
* entries today — for the master's fleet-wide mix.
|
||||||
|
*
|
||||||
|
* Open positions come off the client's position map, each contract named by
|
||||||
|
* the instrument it is: a DXtrade position carries its symbol; a futures one
|
||||||
|
* is looked up among the contracts this machine has entered (recorded at the
|
||||||
|
* fill, kept across restarts), then the resolver's cache and TopstepX's own.
|
||||||
|
* Entries are counted as they fill and kept in a setting per trade date, so
|
||||||
|
* a restart doesn't lose the day.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getSetting, setSetting } from './db';
|
||||||
|
import { currentTradeDateCT } from './trading-logic';
|
||||||
|
import { getAllCachedContracts } from './contract-resolver';
|
||||||
|
import { errorMessage } from './platform-client';
|
||||||
|
import type { InstrumentCount, TradeMix } from './master-protocol';
|
||||||
|
|
||||||
|
const TODAY_KEY = 'trade_mix_today';
|
||||||
|
const NAMES_KEY = 'trade_mix_contracts';
|
||||||
|
/** Contract ids remembered at most — the front months of a few roots. */
|
||||||
|
const MAX_NAMES = 200;
|
||||||
|
|
||||||
|
type Tally = Record<string, { long: number; short: number; contracts: number }>;
|
||||||
|
interface Today {
|
||||||
|
date: string;
|
||||||
|
byFirm: Record<string, Tally>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readJson<T>(key: string): T | null {
|
||||||
|
try {
|
||||||
|
return JSON.parse(getSetting(key) ?? 'null') as T | null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function today(date: string): Today {
|
||||||
|
const saved = readJson<Today>(TODAY_KEY);
|
||||||
|
return saved?.date === date && saved.byFirm && typeof saved.byFirm === 'object' ? saved : { date, byFirm: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count one filled entry toward the firm's mix for today, and remember which
|
||||||
|
* instrument its contract is. Bookkeeping: it never throws into a trade.
|
||||||
|
*/
|
||||||
|
export function recordEntry(firm: string, instrument: string, side: 'Buy' | 'Sell', contracts: number, contractId?: unknown): void {
|
||||||
|
try {
|
||||||
|
const t = today(currentTradeDateCT());
|
||||||
|
const tally = (t.byFirm[firm] ??= {});
|
||||||
|
const entry = (tally[instrument] ??= { long: 0, short: 0, contracts: 0 });
|
||||||
|
if (side === 'Buy') entry.long++;
|
||||||
|
else entry.short++;
|
||||||
|
entry.contracts += Number.isFinite(contracts) ? contracts : 0;
|
||||||
|
setSetting(TODAY_KEY, JSON.stringify(t));
|
||||||
|
|
||||||
|
if (contractId !== undefined && contractId !== null && contractId !== '') {
|
||||||
|
const names = readJson<Record<string, string>>(NAMES_KEY) ?? {};
|
||||||
|
names[String(contractId)] = instrument;
|
||||||
|
const kept = Object.entries(names).slice(-MAX_NAMES);
|
||||||
|
setSetting(NAMES_KEY, JSON.stringify(Object.fromEntries(kept)));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`[trade-mix] could not record an entry: ${errorMessage(err)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The firm's open positions and today's entries, by instrument. */
|
||||||
|
export function tradeMixFor(
|
||||||
|
firm: { name: string; platform: string },
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
client: any,
|
||||||
|
): TradeMix {
|
||||||
|
const names = new Map<string, string>();
|
||||||
|
for (const [root, contract] of Object.entries(getAllCachedContracts())) {
|
||||||
|
if (contract) names.set(String(contract.id), root);
|
||||||
|
}
|
||||||
|
const own = typeof client.contractRoots === 'function' ? (client.contractRoots() as Map<string, string>) : null;
|
||||||
|
for (const [id, root] of own ?? []) names.set(id, root);
|
||||||
|
for (const [id, instrument] of Object.entries(readJson<Record<string, string>>(NAMES_KEY) ?? {})) names.set(id, instrument);
|
||||||
|
const instrumentOf = (contractId: unknown): string => (firm.platform === 'dxtrade'
|
||||||
|
? String(contractId).split('|')[0]
|
||||||
|
: names.get(String(contractId)) ?? `#${String(contractId)}`);
|
||||||
|
|
||||||
|
const open = new Map<string, InstrumentCount>();
|
||||||
|
for (const acc of (client.accountList ?? []) as { id: number }[]) {
|
||||||
|
const pos = client.positions?.[acc.id] as { contractId: unknown; netPos: number } | undefined;
|
||||||
|
if (!pos || !pos.netPos) continue;
|
||||||
|
const instrument = instrumentOf(pos.contractId);
|
||||||
|
const entry = open.get(instrument) ?? { instrument, long: 0, short: 0, contracts: 0 };
|
||||||
|
if (pos.netPos > 0) entry.long++;
|
||||||
|
else entry.short++;
|
||||||
|
entry.contracts += Math.abs(pos.netPos);
|
||||||
|
open.set(instrument, entry);
|
||||||
|
}
|
||||||
|
const tally = today(currentTradeDateCT()).byFirm[firm.name] ?? {};
|
||||||
|
return {
|
||||||
|
open: [...open.values()],
|
||||||
|
today: Object.entries(tally).map(([instrument, t]) => ({ instrument, long: t.long, short: t.short, contracts: t.contracts })),
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user