learning_ai_invt_trdg/backend/reconcileExitBackfillOnce.ts
Saravana Achu Mac 6fac10de9d refactor(backend): root scripts use legacySupabase client where possible
- 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
2026-04-04 20:44:24 -07:00

218 lines
7.2 KiB
TypeScript

import 'dotenv/config';
import { config, loadDynamicConfig } from '../src/config/index.js';
import { ConnectorFactory } from '../src/connectors/factory.js';
import { TradeExecutor } from '../src/services/TradeExecutor.js';
import { healthTracker } from '../src/services/healthTracker.js';
import { reconciliationExitBackfillService } from '../src/services/reconciliationExitBackfillService.js';
import { supabaseService } from '../src/services/SupabaseService.js';
type BackfillCliOptions = {
apply: boolean;
profileIds: Set<string>;
ignoreAllowlist: boolean;
};
type ProfileSummary = {
profileId: string;
userId: string;
attempted: boolean;
skippedReason?: string;
batchId?: string;
dryRun: boolean;
openTradeCandidates: number;
proposedRows: number;
insertedRows: number;
noGoTrades: number;
};
const parseOptions = (argv: string[]): BackfillCliOptions => {
const options: BackfillCliOptions = {
apply: false,
profileIds: new Set<string>(),
ignoreAllowlist: false
};
for (const arg of argv) {
if (arg === '--apply') {
options.apply = true;
continue;
}
if (arg === '--ignore-allowlist') {
options.ignoreAllowlist = true;
continue;
}
if (arg.startsWith('--profile=')) {
const value = String(arg.slice('--profile='.length) || '').trim();
if (value) options.profileIds.add(value);
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 originalDryRun = config.RECON_EXIT_BACKFILL_DRY_RUN;
const originalAllowlist = [...config.RECON_EXIT_BACKFILL_PROFILE_ALLOWLIST];
if (!config.ENABLE_RECON_EXIT_BACKFILL) {
throw new Error('ENABLE_RECON_EXIT_BACKFILL=false. Enable it before running reconciliation EXIT backfill.');
}
config.RECON_EXIT_BACKFILL_DRY_RUN = !options.apply;
if (options.ignoreAllowlist) {
config.RECON_EXIT_BACKFILL_PROFILE_ALLOWLIST = [];
} else if (options.profileIds.size > 0) {
config.RECON_EXIT_BACKFILL_PROFILE_ALLOWLIST = normalizeProfileIds(options.profileIds);
}
healthTracker.recordTradingControl({
mode: 'PAUSED',
lastChangedBy: 'maintenance-script',
lastChangedAt: Date.now(),
reason: 'Offline reconciliation EXIT backfill cycle'
});
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_EXIT_BACKFILL_DRY_RUN,
openTradeCandidates: 0,
proposedRows: 0,
insertedRows: 0,
noGoTrades: 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_EXIT_BACKFILL_DRY_RUN,
openTradeCandidates: 0,
proposedRows: 0,
insertedRows: 0,
noGoTrades: 0
});
continue;
}
const connector = ConnectorFactory.getCustomConnector(config.EXECUTION_PROVIDER, apiKey, apiSecret);
const executor = new TradeExecutor(connector, undefined, userId, profileId);
executor.setProfileSettings(profile);
try {
const result = await reconciliationExitBackfillService.runProfile({
profileId,
userId,
executor
});
results.push({
profileId,
userId,
attempted: result.attempted,
skippedReason: result.skippedReason,
batchId: result.batchId,
dryRun: result.dryRun,
openTradeCandidates: result.openTradeCandidates,
proposedRows: result.proposedRows,
insertedRows: result.insertedRows,
noGoTrades: result.noGoTrades
});
} finally {
executor.dispose();
}
}
config.RECON_EXIT_BACKFILL_DRY_RUN = originalDryRun;
config.RECON_EXIT_BACKFILL_PROFILE_ALLOWLIST = originalAllowlist;
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.proposedRows += row.proposedRows;
acc.insertedRows += row.insertedRows;
acc.noGoTrades += row.noGoTrades;
return acc;
},
{
attemptedProfiles: 0,
skippedProfiles: {} as Record<string, number>,
proposedRows: 0,
insertedRows: 0,
noGoTrades: 0
}
);
console.log(JSON.stringify({
mode: options.apply ? 'apply' : 'dry-run',
profileFilter: normalizeProfileIds(options.profileIds),
ignoreAllowlist: options.ignoreAllowlist,
requirePause: config.RECON_EXIT_BACKFILL_REQUIRE_PAUSE,
dryRunFlagUsed: !options.apply,
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);
});