refactor: centralize backend profile metadata lookups
This commit is contained in:
parent
ebaabaed47
commit
50defe1890
@ -1,6 +1,7 @@
|
|||||||
import logger from '../utils/logger.js';
|
import logger from '../utils/logger.js';
|
||||||
import { config } from '../config/index.js';
|
import { config } from '../config/index.js';
|
||||||
import { supabaseService } from './SupabaseService.js';
|
import { supabaseService } from './SupabaseService.js';
|
||||||
|
import { getTradeProfileCapital } from './profileRepository.js';
|
||||||
|
|
||||||
export interface CapitalLedgerRecord {
|
export interface CapitalLedgerRecord {
|
||||||
profile_id: string;
|
profile_id: string;
|
||||||
@ -39,7 +40,7 @@ export class CapitalLedger {
|
|||||||
logger.error(`[CapitalLedger] ensureLedger aborted for ${profileId}: Supabase client unavailable (fail-closed).`);
|
logger.error(`[CapitalLedger] ensureLedger aborted for ${profileId}: Supabase client unavailable (fail-closed).`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const profileCapital = await supabaseService.getProfileCapital(profileId);
|
const profileCapital = await getTradeProfileCapital(profileId, supabaseService);
|
||||||
const allocation = toNumeric(allocatedCapital ?? profileCapital?.allocatedCapital ?? config.TOTAL_CAPITAL);
|
const allocation = toNumeric(allocatedCapital ?? profileCapital?.allocatedCapital ?? config.TOTAL_CAPITAL);
|
||||||
|
|
||||||
const { data, error } = await client
|
const { data, error } = await client
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { SignalDirection } from '../strategies/rules/types.js';
|
|||||||
import logger from '../utils/logger.js';
|
import logger from '../utils/logger.js';
|
||||||
import { supabaseService } from './SupabaseService.js';
|
import { supabaseService } from './SupabaseService.js';
|
||||||
import { config } from '../config/index.js';
|
import { config } from '../config/index.js';
|
||||||
|
import { getTradeProfileCapital } from './profileRepository.js';
|
||||||
|
|
||||||
const CAPITAL_WAIT_TIMEOUT_MS = 60_000;
|
const CAPITAL_WAIT_TIMEOUT_MS = 60_000;
|
||||||
const CAPITAL_WAIT_POLL_MS = 3_000;
|
const CAPITAL_WAIT_POLL_MS = 3_000;
|
||||||
@ -33,7 +34,7 @@ export class ManualTrader {
|
|||||||
return Number(config.TOTAL_CAPITAL || 0);
|
return Number(config.TOTAL_CAPITAL || 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const profileCapital = await supabaseService.getProfileCapital(profileId);
|
const profileCapital = await getTradeProfileCapital(profileId, supabaseService);
|
||||||
if (profileCapital?.allocatedCapital && profileCapital.allocatedCapital > 0) {
|
if (profileCapital?.allocatedCapital && profileCapital.allocatedCapital > 0) {
|
||||||
return profileCapital.allocatedCapital;
|
return profileCapital.allocatedCapital;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,7 +18,9 @@ import {
|
|||||||
deleteTradeProfileForUser,
|
deleteTradeProfileForUser,
|
||||||
ensureDefaultTradeProfileForUser,
|
ensureDefaultTradeProfileForUser,
|
||||||
getCurrentUserProfile,
|
getCurrentUserProfile,
|
||||||
|
getTradeProfileForUser,
|
||||||
listAllTradeProfiles,
|
listAllTradeProfiles,
|
||||||
|
listActiveTradeProfiles,
|
||||||
listTradeProfilesForUser,
|
listTradeProfilesForUser,
|
||||||
saveCurrentUserProfile,
|
saveCurrentUserProfile,
|
||||||
saveTradeProfileForUser,
|
saveTradeProfileForUser,
|
||||||
@ -1252,13 +1254,14 @@ export class ApiServer {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.app.get('/api/state', this.requireAuth, async (req, res) => {
|
this.app.get('/api/state', this.requireAuth, async (req, res) => {
|
||||||
const authUserId = (req as AuthenticatedRequest).authUserId;
|
const authReq = req as AuthenticatedRequest;
|
||||||
|
const authUserId = authReq.authUserId;
|
||||||
if (!authUserId) {
|
if (!authUserId) {
|
||||||
res.status(401).json({ error: 'Unauthorized' });
|
res.status(401).json({ error: 'Unauthorized' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.state.uptime = Date.now() - this.startTime;
|
this.state.uptime = Date.now() - this.startTime;
|
||||||
const isAdmin = await supabaseService.isAdmin(authUserId);
|
const isAdmin = await isTradingAdmin(authUserId, authReq.authRole);
|
||||||
const scopedState = this.getScopedState(authUserId, isAdmin);
|
const scopedState = this.getScopedState(authUserId, isAdmin);
|
||||||
res.json({
|
res.json({
|
||||||
...scopedState,
|
...scopedState,
|
||||||
@ -1267,14 +1270,15 @@ export class ApiServer {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.app.get('/api/lifecycle/canonical', this.requireAuth, async (req, res) => {
|
this.app.get('/api/lifecycle/canonical', this.requireAuth, async (req, res) => {
|
||||||
const authUserId = (req as AuthenticatedRequest).authUserId;
|
const authReq = req as AuthenticatedRequest;
|
||||||
|
const authUserId = authReq.authUserId;
|
||||||
if (!authUserId) {
|
if (!authUserId) {
|
||||||
res.status(401).json({ error: 'Unauthorized' });
|
res.status(401).json({ error: 'Unauthorized' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const isAdmin = await supabaseService.isAdmin(authUserId);
|
const isAdmin = await isTradingAdmin(authUserId, authReq.authRole);
|
||||||
const requestedProfileId = String(req.query.profileId || '').trim();
|
const requestedProfileId = String(req.query.profileId || '').trim();
|
||||||
const splitByProfileQuery = String(req.query.splitByProfile || '').trim().toLowerCase();
|
const splitByProfileQuery = String(req.query.splitByProfile || '').trim().toLowerCase();
|
||||||
const splitByProfile = splitByProfileQuery
|
const splitByProfile = splitByProfileQuery
|
||||||
@ -1288,8 +1292,8 @@ export class ApiServer {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const profileRows = isAdmin
|
const profileRows = isAdmin
|
||||||
? await supabaseService.getAllProfiles()
|
? await listAllTradeProfiles(supabaseService)
|
||||||
: await supabaseService.getProfilesForUser(authUserId);
|
: await listTradeProfilesForUser(authUserId, supabaseService);
|
||||||
|
|
||||||
if (requestedProfileId && !profileRows.some((row) => row.id === requestedProfileId)) {
|
if (requestedProfileId && !profileRows.some((row) => row.id === requestedProfileId)) {
|
||||||
res.status(403).json({ success: false, error: 'Forbidden: profile does not belong to scoped user context' });
|
res.status(403).json({ success: false, error: 'Forbidden: profile does not belong to scoped user context' });
|
||||||
@ -1889,7 +1893,8 @@ export class ApiServer {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.app.post('/api/backtest/run', this.requireAuth, async (req, res) => {
|
this.app.post('/api/backtest/run', this.requireAuth, async (req, res) => {
|
||||||
const authUserId = (req as AuthenticatedRequest).authUserId;
|
const authReq = req as AuthenticatedRequest;
|
||||||
|
const authUserId = authReq.authUserId;
|
||||||
if (!authUserId) {
|
if (!authUserId) {
|
||||||
res.status(401).json({ success: false, error: 'Unauthorized' });
|
res.status(401).json({ success: false, error: 'Unauthorized' });
|
||||||
return;
|
return;
|
||||||
@ -1902,7 +1907,7 @@ export class ApiServer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isAdmin = await supabaseService.isAdmin(authUserId);
|
const isAdmin = await isTradingAdmin(authUserId, authReq.authRole);
|
||||||
if (!isAdmin && !config.BACKTEST_CUSTOMER_ENABLED) {
|
if (!isAdmin && !config.BACKTEST_CUSTOMER_ENABLED) {
|
||||||
res.status(403).json({
|
res.status(403).json({
|
||||||
success: false,
|
success: false,
|
||||||
@ -1916,7 +1921,7 @@ export class ApiServer {
|
|||||||
const profileId = String(body.profileId || '').trim();
|
const profileId = String(body.profileId || '').trim();
|
||||||
let profileSettings: any = undefined;
|
let profileSettings: any = undefined;
|
||||||
if (profileId) {
|
if (profileId) {
|
||||||
profileSettings = await supabaseService.getProfileForBacktest(profileId, authUserId);
|
profileSettings = await getTradeProfileForUser(profileId, authUserId, supabaseService);
|
||||||
if (!profileSettings) {
|
if (!profileSettings) {
|
||||||
res.status(404).json({ success: false, error: 'Backtest profile not found for current user' });
|
res.status(404).json({ success: false, error: 'Backtest profile not found for current user' });
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -35,6 +35,12 @@ export interface TradeProfileRecord {
|
|||||||
updated_at?: string;
|
updated_at?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TradeProfileCapitalSummary {
|
||||||
|
allocatedCapital: number;
|
||||||
|
isActive: boolean;
|
||||||
|
userId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface TradeProfileDocument extends TradeProfileRecord {
|
interface TradeProfileDocument extends TradeProfileRecord {
|
||||||
productId: string;
|
productId: string;
|
||||||
type: 'trade_profile';
|
type: 'trade_profile';
|
||||||
@ -197,6 +203,57 @@ async function listAllProfilesFromSupabase(legacyService?: LegacySupabaseService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getProfileFromCosmos(profileId: string): Promise<TradeProfileRecord | null> {
|
||||||
|
if (!isCosmosConfigured() || !profileId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = getContainer(PROFILE_CONTAINER);
|
||||||
|
const { resources } = await container.items.query<TradeProfileDocument>({
|
||||||
|
query: 'SELECT TOP 1 * FROM c WHERE c.productId = @productId AND c.type = @type AND c.id = @id',
|
||||||
|
parameters: [
|
||||||
|
{ name: '@productId', value: config.PRODUCT_ID },
|
||||||
|
{ name: '@type', value: 'trade_profile' },
|
||||||
|
{ name: '@id', value: profileId },
|
||||||
|
],
|
||||||
|
}).fetchAll();
|
||||||
|
|
||||||
|
const resource = resources[0];
|
||||||
|
if (!resource) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizeProfile({
|
||||||
|
...resource,
|
||||||
|
created_at: resource.createdAt,
|
||||||
|
updated_at: resource.updatedAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getProfileFromSupabase(profileId: string, legacyService?: LegacySupabaseService): Promise<TradeProfileRecord | null> {
|
||||||
|
const client = legacyService?.getClient?.();
|
||||||
|
if (!client || !profileId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data, error } = await client
|
||||||
|
.from('trade_profiles')
|
||||||
|
.select('id,user_id,name,allocated_capital,risk_per_trade_percent,symbols,is_active,strategy_config,created_at,updated_at')
|
||||||
|
.eq('id', profileId)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (error || !data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizeProfile(data as TradeProfileRecord);
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`[Profiles] Legacy profile lookup failed: ${error instanceof Error ? error.message : 'unknown error'}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function mirrorProfileToSupabase(profile: TradeProfileRecord, legacyService?: LegacySupabaseService): Promise<void> {
|
async function mirrorProfileToSupabase(profile: TradeProfileRecord, legacyService?: LegacySupabaseService): Promise<void> {
|
||||||
const client = legacyService?.getClient?.();
|
const client = legacyService?.getClient?.();
|
||||||
if (!client) return;
|
if (!client) return;
|
||||||
@ -263,6 +320,50 @@ export async function listAllTradeProfiles(legacyService?: LegacySupabaseService
|
|||||||
return listAllProfilesFromSupabase(legacyService);
|
return listAllProfilesFromSupabase(legacyService);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function listActiveTradeProfiles(legacyService?: LegacySupabaseService): Promise<TradeProfileRecord[]> {
|
||||||
|
const profiles = await listAllTradeProfiles(legacyService);
|
||||||
|
return profiles.filter((profile) => Boolean(profile.is_active));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTradeProfileById(profileId: string, legacyService?: LegacySupabaseService): Promise<TradeProfileRecord | null> {
|
||||||
|
const normalizedId = String(profileId || '').trim();
|
||||||
|
if (!normalizedId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cosmosProfile = await getProfileFromCosmos(normalizedId);
|
||||||
|
if (cosmosProfile) {
|
||||||
|
return cosmosProfile;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`[Profiles] Cosmos profile lookup failed, falling back to legacy store: ${error instanceof Error ? error.message : 'unknown error'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return getProfileFromSupabase(normalizedId, legacyService);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTradeProfileCapital(profileId: string, legacyService?: LegacySupabaseService): Promise<TradeProfileCapitalSummary | null> {
|
||||||
|
const profile = await getTradeProfileById(profileId, legacyService);
|
||||||
|
if (!profile) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
allocatedCapital: Number(profile.allocated_capital || 0),
|
||||||
|
isActive: Boolean(profile.is_active),
|
||||||
|
userId: profile.user_id || undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTradeProfileForUser(profileId: string, userId: string, legacyService?: LegacySupabaseService): Promise<TradeProfileRecord | null> {
|
||||||
|
const profile = await getTradeProfileById(profileId, legacyService);
|
||||||
|
if (!profile || String(profile.user_id || '').trim() !== String(userId || '').trim()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
|
||||||
export async function ensureDefaultTradeProfileForUser(userId: string, legacyService?: LegacySupabaseService): Promise<TradeProfileRecord[]> {
|
export async function ensureDefaultTradeProfileForUser(userId: string, legacyService?: LegacySupabaseService): Promise<TradeProfileRecord[]> {
|
||||||
const profiles = await listTradeProfilesForUser(userId, legacyService);
|
const profiles = await listTradeProfilesForUser(userId, legacyService);
|
||||||
if (profiles.length > 0) {
|
if (profiles.length > 0) {
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import logger from '../utils/logger.js';
|
|||||||
import { healthTracker } from './healthTracker.js';
|
import { healthTracker } from './healthTracker.js';
|
||||||
import { observabilityService } from './observabilityService.js';
|
import { observabilityService } from './observabilityService.js';
|
||||||
import { supabaseService } from './SupabaseService.js';
|
import { supabaseService } from './SupabaseService.js';
|
||||||
|
import { getTradeProfileCapital } from './profileRepository.js';
|
||||||
import type { TradeExecutor } from './TradeExecutor.js';
|
import type { TradeExecutor } from './TradeExecutor.js';
|
||||||
import { buildAlpacaSubTag } from '../utils/alpacaSubTag.js';
|
import { buildAlpacaSubTag } from '../utils/alpacaSubTag.js';
|
||||||
import { normalizeBotSymbolToken } from '../utils/botSymbolScope.js';
|
import { normalizeBotSymbolToken } from '../utils/botSymbolScope.js';
|
||||||
@ -275,7 +276,7 @@ export class ReconciliationParityHeartbeatService {
|
|||||||
const throttleMs = Math.max(0, toNumber(config.RECON_INTEGRITY_WATCHDOG_THROTTLE_MS || 600_000));
|
const throttleMs = Math.max(0, toNumber(config.RECON_INTEGRITY_WATCHDOG_THROTTLE_MS || 600_000));
|
||||||
const requireSubTagAttribution = Boolean(config.RECON_POSITION_PARITY_REQUIRE_SUBTAG_ATTRIBUTION);
|
const requireSubTagAttribution = Boolean(config.RECON_POSITION_PARITY_REQUIRE_SUBTAG_ATTRIBUTION);
|
||||||
const allowLegacyEntryAttribution = Boolean(config.RECON_POSITION_PARITY_ALLOW_LEGACY_ENTRY_ATTRIBUTION);
|
const allowLegacyEntryAttribution = Boolean(config.RECON_POSITION_PARITY_ALLOW_LEGACY_ENTRY_ATTRIBUTION);
|
||||||
const profileCapital = await supabaseService.getProfileCapital(profileId);
|
const profileCapital = await getTradeProfileCapital(profileId, supabaseService);
|
||||||
const allocatedCapitalUsd = Math.max(0, toNumber(profileCapital?.allocatedCapital));
|
const allocatedCapitalUsd = Math.max(0, toNumber(profileCapital?.allocatedCapital));
|
||||||
|
|
||||||
let mismatchTrades = 0;
|
let mismatchTrades = 0;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user