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 { config } from '../config/index.js';
|
||||
import { supabaseService } from './SupabaseService.js';
|
||||
import { getTradeProfileCapital } from './profileRepository.js';
|
||||
|
||||
export interface CapitalLedgerRecord {
|
||||
profile_id: string;
|
||||
@ -39,7 +40,7 @@ export class CapitalLedger {
|
||||
logger.error(`[CapitalLedger] ensureLedger aborted for ${profileId}: Supabase client unavailable (fail-closed).`);
|
||||
return null;
|
||||
}
|
||||
const profileCapital = await supabaseService.getProfileCapital(profileId);
|
||||
const profileCapital = await getTradeProfileCapital(profileId, supabaseService);
|
||||
const allocation = toNumeric(allocatedCapital ?? profileCapital?.allocatedCapital ?? config.TOTAL_CAPITAL);
|
||||
|
||||
const { data, error } = await client
|
||||
|
||||
@ -3,6 +3,7 @@ import { SignalDirection } from '../strategies/rules/types.js';
|
||||
import logger from '../utils/logger.js';
|
||||
import { supabaseService } from './SupabaseService.js';
|
||||
import { config } from '../config/index.js';
|
||||
import { getTradeProfileCapital } from './profileRepository.js';
|
||||
|
||||
const CAPITAL_WAIT_TIMEOUT_MS = 60_000;
|
||||
const CAPITAL_WAIT_POLL_MS = 3_000;
|
||||
@ -33,7 +34,7 @@ export class ManualTrader {
|
||||
return Number(config.TOTAL_CAPITAL || 0);
|
||||
}
|
||||
|
||||
const profileCapital = await supabaseService.getProfileCapital(profileId);
|
||||
const profileCapital = await getTradeProfileCapital(profileId, supabaseService);
|
||||
if (profileCapital?.allocatedCapital && profileCapital.allocatedCapital > 0) {
|
||||
return profileCapital.allocatedCapital;
|
||||
}
|
||||
|
||||
@ -18,7 +18,9 @@ import {
|
||||
deleteTradeProfileForUser,
|
||||
ensureDefaultTradeProfileForUser,
|
||||
getCurrentUserProfile,
|
||||
getTradeProfileForUser,
|
||||
listAllTradeProfiles,
|
||||
listActiveTradeProfiles,
|
||||
listTradeProfilesForUser,
|
||||
saveCurrentUserProfile,
|
||||
saveTradeProfileForUser,
|
||||
@ -1252,13 +1254,14 @@ export class ApiServer {
|
||||
});
|
||||
|
||||
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) {
|
||||
res.status(401).json({ error: 'Unauthorized' });
|
||||
return;
|
||||
}
|
||||
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);
|
||||
res.json({
|
||||
...scopedState,
|
||||
@ -1267,14 +1270,15 @@ export class ApiServer {
|
||||
});
|
||||
|
||||
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) {
|
||||
res.status(401).json({ error: 'Unauthorized' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const isAdmin = await supabaseService.isAdmin(authUserId);
|
||||
const isAdmin = await isTradingAdmin(authUserId, authReq.authRole);
|
||||
const requestedProfileId = String(req.query.profileId || '').trim();
|
||||
const splitByProfileQuery = String(req.query.splitByProfile || '').trim().toLowerCase();
|
||||
const splitByProfile = splitByProfileQuery
|
||||
@ -1288,8 +1292,8 @@ export class ApiServer {
|
||||
);
|
||||
|
||||
const profileRows = isAdmin
|
||||
? await supabaseService.getAllProfiles()
|
||||
: await supabaseService.getProfilesForUser(authUserId);
|
||||
? await listAllTradeProfiles(supabaseService)
|
||||
: await listTradeProfilesForUser(authUserId, supabaseService);
|
||||
|
||||
if (requestedProfileId && !profileRows.some((row) => row.id === requestedProfileId)) {
|
||||
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) => {
|
||||
const authUserId = (req as AuthenticatedRequest).authUserId;
|
||||
const authReq = req as AuthenticatedRequest;
|
||||
const authUserId = authReq.authUserId;
|
||||
if (!authUserId) {
|
||||
res.status(401).json({ success: false, error: 'Unauthorized' });
|
||||
return;
|
||||
@ -1902,7 +1907,7 @@ export class ApiServer {
|
||||
return;
|
||||
}
|
||||
|
||||
const isAdmin = await supabaseService.isAdmin(authUserId);
|
||||
const isAdmin = await isTradingAdmin(authUserId, authReq.authRole);
|
||||
if (!isAdmin && !config.BACKTEST_CUSTOMER_ENABLED) {
|
||||
res.status(403).json({
|
||||
success: false,
|
||||
@ -1916,7 +1921,7 @@ export class ApiServer {
|
||||
const profileId = String(body.profileId || '').trim();
|
||||
let profileSettings: any = undefined;
|
||||
if (profileId) {
|
||||
profileSettings = await supabaseService.getProfileForBacktest(profileId, authUserId);
|
||||
profileSettings = await getTradeProfileForUser(profileId, authUserId, supabaseService);
|
||||
if (!profileSettings) {
|
||||
res.status(404).json({ success: false, error: 'Backtest profile not found for current user' });
|
||||
return;
|
||||
|
||||
@ -35,6 +35,12 @@ export interface TradeProfileRecord {
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface TradeProfileCapitalSummary {
|
||||
allocatedCapital: number;
|
||||
isActive: boolean;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
interface TradeProfileDocument extends TradeProfileRecord {
|
||||
productId: string;
|
||||
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> {
|
||||
const client = legacyService?.getClient?.();
|
||||
if (!client) return;
|
||||
@ -263,6 +320,50 @@ export async function listAllTradeProfiles(legacyService?: LegacySupabaseService
|
||||
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[]> {
|
||||
const profiles = await listTradeProfilesForUser(userId, legacyService);
|
||||
if (profiles.length > 0) {
|
||||
|
||||
@ -4,6 +4,7 @@ import logger from '../utils/logger.js';
|
||||
import { healthTracker } from './healthTracker.js';
|
||||
import { observabilityService } from './observabilityService.js';
|
||||
import { supabaseService } from './SupabaseService.js';
|
||||
import { getTradeProfileCapital } from './profileRepository.js';
|
||||
import type { TradeExecutor } from './TradeExecutor.js';
|
||||
import { buildAlpacaSubTag } from '../utils/alpacaSubTag.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 requireSubTagAttribution = Boolean(config.RECON_POSITION_PARITY_REQUIRE_SUBTAG_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));
|
||||
|
||||
let mismatchTrades = 0;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user