- Call loadDynamicConfig() without dead supabaseService argument (Cosmos-backed). - Use getLegacySupabaseClient() for raw .from() queries in maintenance scripts. - manualOverrideCloseTrades: typed imports + legacy client for lifecycle SELECT. - verify_realtime: ESM .js imports and comment for subscribeToProfiles. - verifyTenantIsolation: comment for singleton monkey-patch. Made-with: Cursor
309 lines
12 KiB
TypeScript
309 lines
12 KiB
TypeScript
import 'dotenv/config';
|
|
import { config, loadDynamicConfig } from '../src/config/index.js';
|
|
import { AlpacaConnector } from '../src/connectors/alpaca.js';
|
|
import { TradeExecutor } from '../src/services/TradeExecutor.js';
|
|
import { reconciliationOrderCoverageService } from '../src/services/reconciliationOrderCoverageService.js';
|
|
import { supabaseService } from '../src/services/SupabaseService.js';
|
|
|
|
type CliOptions = {
|
|
apply: boolean;
|
|
profileIds: Set<string>;
|
|
lookbackHours?: number;
|
|
fetchLimitPerPage?: number;
|
|
maxFetchPages?: number;
|
|
maxInsertsPerProfile?: number;
|
|
ignoreFeatureFlag: boolean;
|
|
};
|
|
|
|
type ProfileSummary = {
|
|
profileId: string;
|
|
userId: string;
|
|
attempted: boolean;
|
|
skippedReason?: string;
|
|
dryRun: boolean;
|
|
scannedOrders: number;
|
|
filledLikeOrders: number;
|
|
botOwnedOrders: number;
|
|
eligibleOrders: number;
|
|
missingInDb: number;
|
|
insertedRows: number;
|
|
skippedNotBotOwned: number;
|
|
skippedUnmappedTrade: number;
|
|
skippedUnmappedAction: number;
|
|
skippedMissingFillData: number;
|
|
skippedMissingOrderId: number;
|
|
skippedExisting: number;
|
|
skippedMaxInsertLimit: number;
|
|
};
|
|
|
|
const parseOptions = (argv: string[]): CliOptions => {
|
|
const options: CliOptions = {
|
|
apply: false,
|
|
profileIds: new Set<string>(),
|
|
ignoreFeatureFlag: false
|
|
};
|
|
|
|
for (const arg of argv) {
|
|
if (arg === '--apply') {
|
|
options.apply = true;
|
|
continue;
|
|
}
|
|
if (arg === '--ignore-feature-flag') {
|
|
options.ignoreFeatureFlag = true;
|
|
continue;
|
|
}
|
|
if (arg.startsWith('--profile=')) {
|
|
const profileId = String(arg.slice('--profile='.length) || '').trim();
|
|
if (profileId) options.profileIds.add(profileId);
|
|
continue;
|
|
}
|
|
if (arg.startsWith('--lookback-hours=')) {
|
|
const parsed = Number(arg.slice('--lookback-hours='.length));
|
|
if (Number.isFinite(parsed) && parsed > 0) options.lookbackHours = Math.floor(parsed);
|
|
continue;
|
|
}
|
|
if (arg.startsWith('--max-inserts-per-profile=')) {
|
|
const parsed = Number(arg.slice('--max-inserts-per-profile='.length));
|
|
if (Number.isFinite(parsed) && parsed > 0) options.maxInsertsPerProfile = Math.floor(parsed);
|
|
continue;
|
|
}
|
|
if (arg.startsWith('--fetch-limit-per-page=')) {
|
|
const parsed = Number(arg.slice('--fetch-limit-per-page='.length));
|
|
if (Number.isFinite(parsed) && parsed > 0) options.fetchLimitPerPage = Math.floor(parsed);
|
|
continue;
|
|
}
|
|
if (arg.startsWith('--max-fetch-pages=')) {
|
|
const parsed = Number(arg.slice('--max-fetch-pages='.length));
|
|
if (Number.isFinite(parsed) && parsed > 0) options.maxFetchPages = Math.floor(parsed);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return options;
|
|
};
|
|
|
|
const isPlaceholder = (value: string | undefined): boolean => {
|
|
const normalized = String(value || '').trim();
|
|
if (!normalized) return true;
|
|
return normalized === 'your_key' || normalized === 'your_secret';
|
|
};
|
|
|
|
const normalizeProfileIds = (profileIds: Set<string>): string[] => {
|
|
return Array.from(profileIds)
|
|
.map((value) => String(value || '').trim())
|
|
.filter(Boolean);
|
|
};
|
|
|
|
const run = async (): Promise<void> => {
|
|
const options = parseOptions(process.argv.slice(2));
|
|
await loadDynamicConfig();
|
|
|
|
const originalEnabled = config.ENABLE_RECON_ORDER_COVERAGE_SYNC;
|
|
const originalDryRun = config.RECON_ORDER_COVERAGE_DRY_RUN;
|
|
const originalLookback = config.RECON_ORDER_COVERAGE_LOOKBACK_HOURS;
|
|
const originalFetchLimitPerPage = config.RECON_ORDER_COVERAGE_FETCH_LIMIT_PER_PAGE;
|
|
const originalMaxFetchPages = config.RECON_ORDER_COVERAGE_MAX_FETCH_PAGES;
|
|
const originalMaxInserts = config.RECON_ORDER_COVERAGE_MAX_INSERTS_PER_PROFILE;
|
|
|
|
if (!config.ENABLE_RECON_ORDER_COVERAGE_SYNC && !options.ignoreFeatureFlag) {
|
|
throw new Error('ENABLE_RECON_ORDER_COVERAGE_SYNC=false. Enable it or pass --ignore-feature-flag for one-shot run.');
|
|
}
|
|
|
|
if (options.ignoreFeatureFlag) {
|
|
config.ENABLE_RECON_ORDER_COVERAGE_SYNC = true;
|
|
}
|
|
config.RECON_ORDER_COVERAGE_DRY_RUN = !options.apply;
|
|
if (Number.isFinite(options.lookbackHours)) {
|
|
config.RECON_ORDER_COVERAGE_LOOKBACK_HOURS = Number(options.lookbackHours);
|
|
}
|
|
if (Number.isFinite(options.fetchLimitPerPage)) {
|
|
config.RECON_ORDER_COVERAGE_FETCH_LIMIT_PER_PAGE = Number(options.fetchLimitPerPage);
|
|
}
|
|
if (Number.isFinite(options.maxFetchPages)) {
|
|
config.RECON_ORDER_COVERAGE_MAX_FETCH_PAGES = Number(options.maxFetchPages);
|
|
}
|
|
if (Number.isFinite(options.maxInsertsPerProfile)) {
|
|
config.RECON_ORDER_COVERAGE_MAX_INSERTS_PER_PROFILE = Number(options.maxInsertsPerProfile);
|
|
}
|
|
const effectiveLookbackHours = config.RECON_ORDER_COVERAGE_LOOKBACK_HOURS;
|
|
const effectiveFetchLimitPerPage = config.RECON_ORDER_COVERAGE_FETCH_LIMIT_PER_PAGE;
|
|
const effectiveMaxFetchPages = config.RECON_ORDER_COVERAGE_MAX_FETCH_PAGES;
|
|
const effectiveMaxInsertsPerProfile = config.RECON_ORDER_COVERAGE_MAX_INSERTS_PER_PROFILE;
|
|
const effectiveDryRunFlag = config.RECON_ORDER_COVERAGE_DRY_RUN;
|
|
|
|
const [users, profiles] = await Promise.all([
|
|
supabaseService.getActiveUsers(),
|
|
supabaseService.getActiveProfiles()
|
|
]);
|
|
|
|
const userById = new Map<string, any>();
|
|
for (const user of users || []) {
|
|
const userId = String((user as any)?.user_id || '').trim();
|
|
if (!userId) continue;
|
|
userById.set(userId, user);
|
|
}
|
|
|
|
const selectedProfiles = (profiles || []).filter((profile: any) => {
|
|
const profileId = String(profile?.id || '').trim();
|
|
if (!profileId) return false;
|
|
if (options.profileIds.size === 0) return true;
|
|
return options.profileIds.has(profileId);
|
|
});
|
|
|
|
const results: ProfileSummary[] = [];
|
|
|
|
for (const profile of selectedProfiles) {
|
|
const profileId = String(profile?.id || '').trim();
|
|
const userId = String(profile?.user_id || '').trim();
|
|
if (!profileId || !userId) continue;
|
|
|
|
const user = userById.get(userId);
|
|
if (!user) {
|
|
results.push({
|
|
profileId,
|
|
userId,
|
|
attempted: false,
|
|
skippedReason: 'user_not_found',
|
|
dryRun: config.RECON_ORDER_COVERAGE_DRY_RUN,
|
|
scannedOrders: 0,
|
|
filledLikeOrders: 0,
|
|
botOwnedOrders: 0,
|
|
eligibleOrders: 0,
|
|
missingInDb: 0,
|
|
insertedRows: 0,
|
|
skippedNotBotOwned: 0,
|
|
skippedUnmappedTrade: 0,
|
|
skippedUnmappedAction: 0,
|
|
skippedMissingFillData: 0,
|
|
skippedMissingOrderId: 0,
|
|
skippedExisting: 0,
|
|
skippedMaxInsertLimit: 0
|
|
});
|
|
continue;
|
|
}
|
|
|
|
const apiKey = config.PAPER_TRADING ? user.ALPACA_API_KEY : user.REAL_ALPACA_API_KEY;
|
|
const apiSecret = config.PAPER_TRADING ? user.ALPACA_SECRET_KEY : user.REAL_ALPACA_SECRET_KEY;
|
|
if (isPlaceholder(apiKey) || isPlaceholder(apiSecret)) {
|
|
results.push({
|
|
profileId,
|
|
userId,
|
|
attempted: false,
|
|
skippedReason: 'missing_exchange_credentials',
|
|
dryRun: config.RECON_ORDER_COVERAGE_DRY_RUN,
|
|
scannedOrders: 0,
|
|
filledLikeOrders: 0,
|
|
botOwnedOrders: 0,
|
|
eligibleOrders: 0,
|
|
missingInDb: 0,
|
|
insertedRows: 0,
|
|
skippedNotBotOwned: 0,
|
|
skippedUnmappedTrade: 0,
|
|
skippedUnmappedAction: 0,
|
|
skippedMissingFillData: 0,
|
|
skippedMissingOrderId: 0,
|
|
skippedExisting: 0,
|
|
skippedMaxInsertLimit: 0
|
|
});
|
|
continue;
|
|
}
|
|
|
|
const connector = new AlpacaConnector(apiKey, apiSecret);
|
|
const executor = new TradeExecutor(connector, undefined, userId, profileId);
|
|
executor.setProfileSettings(profile);
|
|
|
|
try {
|
|
const result = await reconciliationOrderCoverageService.runProfile({
|
|
profileId,
|
|
userId,
|
|
executor
|
|
});
|
|
results.push({
|
|
profileId,
|
|
userId,
|
|
attempted: result.attempted,
|
|
skippedReason: result.skippedReason,
|
|
dryRun: result.dryRun,
|
|
scannedOrders: result.scannedOrders,
|
|
filledLikeOrders: result.filledLikeOrders,
|
|
botOwnedOrders: result.botOwnedOrders,
|
|
eligibleOrders: result.eligibleOrders,
|
|
missingInDb: result.missingInDb,
|
|
insertedRows: result.insertedRows,
|
|
skippedNotBotOwned: result.skippedNotBotOwned,
|
|
skippedUnmappedTrade: result.skippedUnmappedTrade,
|
|
skippedUnmappedAction: result.skippedUnmappedAction,
|
|
skippedMissingFillData: result.skippedMissingFillData,
|
|
skippedMissingOrderId: result.skippedMissingOrderId,
|
|
skippedExisting: result.skippedExisting,
|
|
skippedMaxInsertLimit: result.skippedMaxInsertLimit
|
|
});
|
|
} finally {
|
|
executor.dispose();
|
|
}
|
|
}
|
|
|
|
config.ENABLE_RECON_ORDER_COVERAGE_SYNC = originalEnabled;
|
|
config.RECON_ORDER_COVERAGE_DRY_RUN = originalDryRun;
|
|
config.RECON_ORDER_COVERAGE_LOOKBACK_HOURS = originalLookback;
|
|
config.RECON_ORDER_COVERAGE_FETCH_LIMIT_PER_PAGE = originalFetchLimitPerPage;
|
|
config.RECON_ORDER_COVERAGE_MAX_FETCH_PAGES = originalMaxFetchPages;
|
|
config.RECON_ORDER_COVERAGE_MAX_INSERTS_PER_PROFILE = originalMaxInserts;
|
|
|
|
const aggregate = results.reduce((acc, row) => {
|
|
if (row.attempted) acc.attemptedProfiles += 1;
|
|
if (!row.attempted && row.skippedReason) {
|
|
acc.skippedProfiles[row.skippedReason] = (acc.skippedProfiles[row.skippedReason] || 0) + 1;
|
|
}
|
|
acc.scannedOrders += row.scannedOrders;
|
|
acc.filledLikeOrders += row.filledLikeOrders;
|
|
acc.botOwnedOrders += row.botOwnedOrders;
|
|
acc.eligibleOrders += row.eligibleOrders;
|
|
acc.missingInDb += row.missingInDb;
|
|
acc.insertedRows += row.insertedRows;
|
|
acc.skippedNotBotOwned += row.skippedNotBotOwned;
|
|
acc.skippedUnmappedTrade += row.skippedUnmappedTrade;
|
|
acc.skippedUnmappedAction += row.skippedUnmappedAction;
|
|
acc.skippedMissingFillData += row.skippedMissingFillData;
|
|
acc.skippedMissingOrderId += row.skippedMissingOrderId;
|
|
acc.skippedExisting += row.skippedExisting;
|
|
acc.skippedMaxInsertLimit += row.skippedMaxInsertLimit;
|
|
return acc;
|
|
}, {
|
|
attemptedProfiles: 0,
|
|
skippedProfiles: {} as Record<string, number>,
|
|
scannedOrders: 0,
|
|
filledLikeOrders: 0,
|
|
botOwnedOrders: 0,
|
|
eligibleOrders: 0,
|
|
missingInDb: 0,
|
|
insertedRows: 0,
|
|
skippedNotBotOwned: 0,
|
|
skippedUnmappedTrade: 0,
|
|
skippedUnmappedAction: 0,
|
|
skippedMissingFillData: 0,
|
|
skippedMissingOrderId: 0,
|
|
skippedExisting: 0,
|
|
skippedMaxInsertLimit: 0
|
|
});
|
|
|
|
console.log(JSON.stringify({
|
|
mode: options.apply ? 'apply' : 'dry-run',
|
|
profileFilter: normalizeProfileIds(options.profileIds),
|
|
ignoreFeatureFlag: options.ignoreFeatureFlag,
|
|
configuredLookbackHours: effectiveLookbackHours,
|
|
configuredFetchLimitPerPage: effectiveFetchLimitPerPage,
|
|
configuredMaxFetchPages: effectiveMaxFetchPages,
|
|
configuredMaxInsertsPerProfile: effectiveMaxInsertsPerProfile,
|
|
dryRunFlagUsed: effectiveDryRunFlag,
|
|
aggregate,
|
|
results
|
|
}, null, 2));
|
|
};
|
|
|
|
run().catch((error) => {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
console.error(JSON.stringify({ error: message }, null, 2));
|
|
process.exit(1);
|
|
});
|