refactor: move web trading data behind backend apis

This commit is contained in:
Saravana Achu Mac 2026-04-04 16:49:18 -07:00
parent 560c95a599
commit b433686776
14 changed files with 606 additions and 161 deletions

View File

@ -7,6 +7,11 @@ import { SymbolMapper } from '../utils/symbolMapper.js';
import { IExchangeConnector } from '../connectors/types.js';
import { supabaseService } from './SupabaseService.js';
import { healthTracker } from './healthTracker.js';
import {
getProfileConsecutiveLosses,
getProfileDailyLossUsd,
getProfileDailyNetPnlUsd,
} from './tradeHistoryRepository.js';
export interface PortfolioGuardInput {
symbol: string;
@ -329,7 +334,7 @@ export class AutoTrader {
const maxDailyLossUsd = Number(riskLimits.maxDailyLossUsd);
if (Number.isFinite(maxDailyLossUsd) && maxDailyLossUsd > 0) {
const dailyLossUsd = await supabaseService.getProfileDailyLossUsd(profileId);
const dailyLossUsd = await getProfileDailyLossUsd(profileId, supabaseService);
if (dailyLossUsd >= maxDailyLossUsd) {
return {
allowed: false,
@ -340,7 +345,7 @@ export class AutoTrader {
const dailyProfitTargetUsd = Number(riskLimits.dailyProfitTargetUsd);
if (Number.isFinite(dailyProfitTargetUsd) && dailyProfitTargetUsd > 0) {
const dailyNetPnl = await supabaseService.getProfileDailyNetPnlUsd(profileId);
const dailyNetPnl = await getProfileDailyNetPnlUsd(profileId, supabaseService);
if (dailyNetPnl >= dailyProfitTargetUsd) {
return {
allowed: false,
@ -351,7 +356,7 @@ export class AutoTrader {
const maxConsecutiveLosses = Number(riskLimits.maxConsecutiveLosses);
if (Number.isFinite(maxConsecutiveLosses) && maxConsecutiveLosses > 0) {
const consecutiveLosses = await supabaseService.getProfileConsecutiveLosses(profileId);
const consecutiveLosses = await getProfileConsecutiveLosses(profileId, 100, supabaseService);
if (consecutiveLosses >= maxConsecutiveLosses) {
return {
allowed: false,

View File

@ -35,6 +35,9 @@ import {
listManualEntriesForUser,
saveManualEntryForUser
} from './manualEntryRepository.js';
import { listRecentOrders } from './orderActivityRepository.js';
import { createStrategyPreset, listStrategyPresets } from './strategyPresetRepository.js';
import { listRecentTradeHistory, listRecentTradeHistoryKeys } from './tradeHistoryRepository.js';
import * as runtimeOrderRepository from './runtimeOrderRepository.js';
import { mergeOrderSnapshots, mergePositionSnapshots } from './stateMerge.js';
import { OperationalEvent } from '../domain/operationalEvents.js';
@ -1829,6 +1832,74 @@ export class ApiServer {
}
});
this.app.get('/api/positions/bootstrap', this.requireAuth, async (req, res) => {
const authReq = req as AuthenticatedRequest;
const authUserId = authReq.authUserId;
if (!authUserId) {
res.status(401).json({ error: 'Unauthorized' });
return;
}
try {
const isAdmin = await isTradingAdmin(authUserId, authReq.authRole);
const scope = String(req.query.scope || 'user').toLowerCase();
const wantsAll = scope === 'all' && isAdmin;
const orderLimit = Math.max(1, Math.min(5000, parseInt(String(req.query.limit || '5000'), 10) || 5000));
const [entries, orders, historyTradeKeys, profiles] = await Promise.all([
listManualEntriesForUser(authUserId, supabaseService),
listRecentOrders({
userId: wantsAll ? undefined : authUserId,
limit: orderLimit,
legacyService: supabaseService
}),
listRecentTradeHistoryKeys({
userId: wantsAll ? undefined : authUserId,
limit: orderLimit,
legacyService: supabaseService
}),
wantsAll
? listAllTradeProfiles(supabaseService)
: listTradeProfilesForUser(authUserId, supabaseService)
]);
res.json({
entries,
orders,
historyTradeKeys,
profiles
});
} catch (error: any) {
res.status(500).json({ error: `Failed to load positions bootstrap: ${error.message}` });
}
});
this.app.get('/api/trade-history', this.requireAuth, async (req, res) => {
const authReq = req as AuthenticatedRequest;
const authUserId = authReq.authUserId;
if (!authUserId) {
res.status(401).json({ error: 'Unauthorized' });
return;
}
try {
const isAdmin = await isTradingAdmin(authUserId, authReq.authRole);
const scope = String(req.query.scope || 'user').toLowerCase();
const wantsAll = scope === 'all' && isAdmin;
const limit = Math.max(1, Math.min(5000, parseInt(String(req.query.limit || '5000'), 10) || 5000));
const rows = await listRecentTradeHistory({
userId: wantsAll ? undefined : authUserId,
limit,
legacyService: supabaseService
});
res.json({ rows });
} catch (error: any) {
res.status(500).json({ error: `Failed to load trade history: ${error.message}` });
}
});
this.app.post('/api/manual-entries', this.requireAuth, async (req, res) => {
const authUserId = (req as AuthenticatedRequest).authUserId;
if (!authUserId) {
@ -1898,6 +1969,34 @@ export class ApiServer {
}
});
this.app.get('/api/marketplace-presets', this.requireAuth, async (_req, res) => {
try {
const presets = await listStrategyPresets(supabaseService);
res.json({ presets });
} catch (error: any) {
res.status(500).json({ error: `Failed to load marketplace presets: ${error.message}` });
}
});
this.app.post('/api/marketplace-presets', this.requireAuth, this.requireAdmin, async (req, res) => {
const authUserId = (req as AuthenticatedRequest).authUserId;
if (!authUserId) {
res.status(401).json({ error: 'Unauthorized' });
return;
}
try {
const payload = {
...(req.body || {}),
created_by: authUserId
};
await createStrategyPreset(payload, supabaseService);
res.status(201).json({ success: true });
} catch (error: any) {
res.status(400).json({ error: `Failed to publish preset: ${error.message}` });
}
});
this.app.post('/api/backtest/run', this.requireAuth, async (req, res) => {
const authReq = req as AuthenticatedRequest;
const authUserId = authReq.authUserId;

View File

@ -0,0 +1,56 @@
import logger from '../utils/logger.js';
import type { supabaseService } from './SupabaseService.js';
type LegacySupabaseService = typeof supabaseService;
function getClient(legacyService?: LegacySupabaseService) {
return legacyService?.getClient?.() ?? null;
}
export async function listRecentOrders(options: {
userId?: string;
limit?: number;
legacyService?: LegacySupabaseService;
}): Promise<any[]> {
const client = getClient(options.legacyService);
if (!client) return [];
const orderColumnsV3 = 'id,order_id,user_id,profile_id,symbol,type,side,qty,quantity,price,status,timestamp,filled_at,created_at,trade_id,action,source,stop_loss,take_profit,sub_tag';
const orderColumnsV2 = 'id,order_id,user_id,profile_id,symbol,type,side,qty,quantity,price,status,timestamp,filled_at,created_at,trade_id,action,source,stop_loss,take_profit';
const orderColumnsLegacy = 'id,order_id,user_id,profile_id,symbol,type,side,qty,price,status,timestamp,created_at';
const limit = Math.max(1, Math.min(5000, Math.floor(Number(options.limit || 5000))));
const runQuery = async (columns: string) => {
let query = client
.from('orders')
.select(columns)
.order('created_at', { ascending: false })
.limit(limit);
if (options.userId) {
query = query.eq('user_id', options.userId);
}
return query;
};
try {
const { data: v3Data, error: v3Error } = await runQuery(orderColumnsV3);
if (!v3Error) return v3Data || [];
logger.warn(`[OrderActivityRepo] V3 order query failed, falling back to v2 columns: ${v3Error.message}`);
const { data: v2Data, error: v2Error } = await runQuery(orderColumnsV2);
if (!v2Error) return v2Data || [];
logger.warn(`[OrderActivityRepo] V2 order query failed, falling back to legacy columns: ${v2Error.message}`);
const { data: legacyData, error: legacyError } = await runQuery(orderColumnsLegacy);
if (legacyError) {
logger.error(`[OrderActivityRepo] Legacy order query failed: ${legacyError.message}`);
return [];
}
return legacyData || [];
} catch (error: any) {
logger.error(`[OrderActivityRepo] Recent order lookup unexpected error: ${error.message}`);
return [];
}
}

View File

@ -0,0 +1,42 @@
import logger from '../utils/logger.js';
import type { supabaseService } from './SupabaseService.js';
type LegacySupabaseService = typeof supabaseService;
function getClient(legacyService?: LegacySupabaseService) {
return legacyService?.getClient?.() ?? null;
}
export async function listStrategyPresets(legacyService?: LegacySupabaseService): Promise<any[]> {
const client = getClient(legacyService);
if (!client) return [];
try {
const { data, error } = await client
.from('strategy_presets')
.select('*')
.order('created_at', { ascending: false });
if (error) {
logger.error(`[StrategyPresetRepo] Preset lookup failed: ${error.message}`);
return [];
}
return data || [];
} catch (error: any) {
logger.error(`[StrategyPresetRepo] Preset lookup unexpected error: ${error.message}`);
return [];
}
}
export async function createStrategyPreset(payload: Record<string, unknown>, legacyService?: LegacySupabaseService): Promise<void> {
const client = getClient(legacyService);
if (!client) {
throw new Error('Strategy preset store is unavailable');
}
const { error } = await client.from('strategy_presets').insert([payload]);
if (error) {
throw new Error(error.message);
}
}

View File

@ -0,0 +1,170 @@
import logger from '../utils/logger.js';
import type { supabaseService } from './SupabaseService.js';
type LegacySupabaseService = typeof supabaseService;
export interface TradeHistoryKeyRecord {
trade_id: string;
profile_id: string | null;
}
export interface TradeHistoryRow {
id?: string;
timestamp?: number | string;
symbol?: string;
side?: string;
size?: number;
entry_price?: number;
exit_price?: number;
pnl?: number;
pnl_percent?: number;
reason?: string;
profile_id?: string | null;
created_at?: string;
trade_id?: string;
source?: string;
}
const startOfCurrentDayUtc = (): string => {
const now = new Date();
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())).toISOString();
};
function getClient(legacyService?: LegacySupabaseService) {
return legacyService?.getClient?.() ?? null;
}
export async function getProfileDailyNetPnlUsd(profileId: string, legacyService?: LegacySupabaseService): Promise<number> {
const client = getClient(legacyService);
if (!client || !profileId) return 0;
try {
const { data, error } = await client
.from('trade_history')
.select('pnl, created_at')
.eq('profile_id', profileId)
.gte('created_at', startOfCurrentDayUtc())
.order('created_at', { ascending: false })
.limit(5000);
if (error) {
logger.error(`[TradeHistoryRepo] Daily net PnL lookup failed for ${profileId}: ${error.message}`);
return 0;
}
return (data || []).reduce((sum: number, row: any) => {
const pnl = Number(row?.pnl || 0);
return Number.isFinite(pnl) ? sum + pnl : sum;
}, 0);
} catch (error: any) {
logger.error(`[TradeHistoryRepo] Daily net PnL unexpected error for ${profileId}: ${error.message}`);
return 0;
}
}
export async function getProfileDailyLossUsd(profileId: string, legacyService?: LegacySupabaseService): Promise<number> {
const netPnl = await getProfileDailyNetPnlUsd(profileId, legacyService);
return netPnl < 0 ? Math.abs(netPnl) : 0;
}
export async function getProfileConsecutiveLosses(
profileId: string,
lookback: number = 100,
legacyService?: LegacySupabaseService
): Promise<number> {
const client = getClient(legacyService);
if (!client || !profileId) return 0;
try {
const cappedLookback = Math.max(1, Math.min(500, Math.floor(lookback)));
const { data, error } = await client
.from('trade_history')
.select('pnl, created_at')
.eq('profile_id', profileId)
.order('created_at', { ascending: false })
.limit(cappedLookback);
if (error) {
logger.error(`[TradeHistoryRepo] Consecutive loss lookup failed for ${profileId}: ${error.message}`);
return 0;
}
let consecutiveLosses = 0;
for (const row of data || []) {
const pnl = Number((row as any)?.pnl || 0);
if (!Number.isFinite(pnl) || pnl >= 0) break;
consecutiveLosses += 1;
}
return consecutiveLosses;
} catch (error: any) {
logger.error(`[TradeHistoryRepo] Consecutive loss unexpected error for ${profileId}: ${error.message}`);
return 0;
}
}
export async function listRecentTradeHistoryKeys(options: {
userId?: string;
limit?: number;
legacyService?: LegacySupabaseService;
}): Promise<TradeHistoryKeyRecord[]> {
const client = getClient(options.legacyService);
if (!client) return [];
try {
let query = client
.from('trade_history')
.select('trade_id,profile_id')
.order('created_at', { ascending: false })
.limit(Math.max(1, Math.min(5000, Math.floor(Number(options.limit || 5000)))));
if (options.userId) {
query = query.eq('user_id', options.userId);
}
const { data, error } = await query;
if (error) {
logger.error(`[TradeHistoryRepo] Recent trade key lookup failed: ${error.message}`);
return [];
}
return (data || []).map((row: any) => ({
trade_id: String(row?.trade_id || '').trim(),
profile_id: row?.profile_id ? String(row.profile_id) : null
})).filter((row) => Boolean(row.trade_id));
} catch (error: any) {
logger.error(`[TradeHistoryRepo] Recent trade key unexpected error: ${error.message}`);
return [];
}
}
export async function listRecentTradeHistory(options: {
userId?: string;
limit?: number;
legacyService?: LegacySupabaseService;
}): Promise<TradeHistoryRow[]> {
const client = getClient(options.legacyService);
if (!client) return [];
try {
let query = client
.from('trade_history')
.select('id,timestamp,symbol,side,size,entry_price,exit_price,pnl,pnl_percent,reason,profile_id,created_at,trade_id,source')
.order('created_at', { ascending: false })
.limit(Math.max(1, Math.min(5000, Math.floor(Number(options.limit || 5000)))));
if (options.userId) {
query = query.eq('user_id', options.userId);
}
const { data, error } = await query;
if (error) {
logger.error(`[TradeHistoryRepo] Recent trade history lookup failed: ${error.message}`);
return [];
}
return (data || []) as TradeHistoryRow[];
} catch (error: any) {
logger.error(`[TradeHistoryRepo] Recent trade history unexpected error: ${error.message}`);
return [];
}
}

View File

@ -1,6 +1,5 @@
import React, { useState, useEffect } from 'react';
import { supabase } from '../lib/supabaseClient';
import { tableNameMarketplace } from '../lib/const';
import React, { useState, useEffect } from 'react';
import { fetchMarketplacePresets } from '../lib/marketplaceApi';
import {
Activity,
ArrowUpRight,
@ -244,30 +243,24 @@ export const PresetMarketplace: React.FC<PresetMarketplaceProps> = ({ onSelect,
const [customPresets, setCustomPresets] = useState<StrategyPreset[]>([]);
useEffect(() => {
const fetchCustomPresets = async () => {
try {
const { data, error } = await supabase
.from(tableNameMarketplace)
.select('*')
.order('created_at', { ascending: false });
if (!error && data) {
const mappedData = data.map((d: any) => ({
id: d.id,
name: d.name,
description: d.description,
riskStyleId: d.risk_style_id,
recommendedAssets: d.recommended_assets,
typicalTradesPerDay: d.typical_trades_per_day,
performanceTag: d.performance_tag,
isPopular: d.is_popular,
strategy_config: d.strategy_config
}));
setCustomPresets(mappedData as any);
}
} catch (e) {
console.error('Error fetching marketplace presets:', e);
}
const fetchCustomPresets = async () => {
try {
const data = await fetchMarketplacePresets();
const mappedData = data.map((d: any) => ({
id: d.id,
name: d.name,
description: d.description,
riskStyleId: d.risk_style_id,
recommendedAssets: d.recommended_assets,
typicalTradesPerDay: d.typical_trades_per_day,
performanceTag: d.performance_tag,
isPopular: d.is_popular,
strategy_config: d.strategy_config
}));
setCustomPresets(mappedData as any);
} catch (e) {
console.error('Error fetching marketplace presets:', e);
}
};
fetchCustomPresets();

View File

@ -1,8 +1,8 @@
import { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { supabase } from '../lib/supabaseClient';
import { tableNameTransactions } from '../lib/const';
import { aggregateHistoryLedger, buildHistoryLedger } from '../lib/tradeHistoryLedger';
import { publishMarketplacePreset } from '../lib/marketplaceApi';
import { fetchTradeHistory } from '../lib/tradeHistoryApi';
import {
createTradeProfile,
deleteTradeProfile,
@ -324,18 +324,10 @@ export const TradeProfileManager = ({ botState = DEFAULT_BOT_STATE }: TradeProfi
const fetchData = async () => {
setLoading(true);
try {
let historyQuery = supabase
.from(tableNameTransactions)
.select('id,timestamp,symbol,side,size,entry_price,exit_price,pnl,pnl_percent,reason,profile_id,created_at,trade_id,source');
if (profile?.role !== 'admin' && authUser?.id) {
historyQuery = historyQuery.eq('user_id', authUser.id);
}
const [profilesData, meProfile, hRes] = await Promise.all([
fetchTradeProfiles({ scope: profile?.role === 'admin' ? 'all' : 'user' }),
fetchCurrentUserProfile().catch(() => null),
historyQuery
fetchTradeHistory({ scope: profile?.role === 'admin' ? 'all' : 'user' })
]);
const normalizedProfiles = (profilesData || []).map((profile: any) => ({
...profile,
@ -351,7 +343,7 @@ export const TradeProfileManager = ({ botState = DEFAULT_BOT_STATE }: TradeProfi
} : null));
const historyLedger = buildHistoryLedger({
dbRows: hRes.data || [],
dbRows: hRes || [],
includeRealtime: false
});
const historyAggregate = aggregateHistoryLedger(historyLedger);
@ -506,15 +498,14 @@ export const TradeProfileManager = ({ botState = DEFAULT_BOT_STATE }: TradeProfi
is_popular: true,
created_by: authUser?.id,
original_profile_id: p.id,
strategy_config: p.strategy_config
strategy_config: p.strategy_config || {}
};
const { error } = await supabase.from('strategy_presets').insert([payload]);
if (error) {
addToast(`Publish failed: ${error.message}`, 'error');
} else {
try {
await publishMarketplacePreset(payload);
addToast('Strategy published to Marketplace!', 'success');
} catch (error: any) {
addToast(`Publish failed: ${error?.message || 'Unknown error'}`, 'error');
}
setLoading(false);
};

View File

@ -0,0 +1,52 @@
const AUTH_STORAGE_PREFIX = 'invttrdg_web';
const ACCESS_TOKEN_KEY = `${AUTH_STORAGE_PREFIX}_access_token`;
const REFRESH_TOKEN_KEY = `${AUTH_STORAGE_PREFIX}_refresh_token`;
const USER_KEY = `${AUTH_STORAGE_PREFIX}_auth_user`;
export interface PlatformSessionUser {
id: string;
email?: string;
role?: string;
plan?: string;
display_name?: string;
user_metadata?: Record<string, unknown>;
}
export interface PlatformSession {
access_token: string;
refresh_token: string;
user: PlatformSessionUser;
}
function parseJson<T>(value: string | null): T | null {
if (!value) return null;
try {
return JSON.parse(value) as T;
} catch {
return null;
}
}
export function getStoredPlatformSession(): PlatformSession | null {
if (typeof window === 'undefined') return null;
const accessToken = window.localStorage.getItem(ACCESS_TOKEN_KEY);
const refreshToken = window.localStorage.getItem(REFRESH_TOKEN_KEY);
const user = parseJson<PlatformSessionUser>(window.localStorage.getItem(USER_KEY));
if (!accessToken || !refreshToken || !user?.id) {
return null;
}
return {
access_token: accessToken,
refresh_token: refreshToken,
user,
};
}
export function getPlatformAccessToken(): string {
const session = getStoredPlatformSession();
const accessToken = session?.access_token;
if (!accessToken) {
throw new Error('Not authenticated');
}
return accessToken;
}

View File

@ -1,4 +1,4 @@
import { supabase } from './supabaseClient';
import { getPlatformAccessToken } from './authSession';
import { tradingRuntime } from './runtime';
export interface ManualEntryPayload {
@ -23,12 +23,7 @@ export interface ManualEntryPayload {
}
async function getAccessToken(): Promise<string> {
const { data: sessionData } = await supabase.auth.getSession();
const accessToken = sessionData.session?.access_token;
if (!accessToken) {
throw new Error('Not authenticated');
}
return accessToken;
return getPlatformAccessToken();
}
async function apiRequest<T>(path: string, init?: RequestInit): Promise<T> {

View File

@ -0,0 +1,45 @@
import { getPlatformAccessToken } from './authSession';
import { tradingRuntime } from './runtime';
export interface StrategyPresetPayload {
id: string;
name: string;
description: string;
risk_style_id: string;
recommended_assets: string[];
typical_trades_per_day: string;
performance_tag: string;
is_popular: boolean;
created_by?: string;
original_profile_id?: string;
strategy_config: Record<string, unknown>;
}
async function apiRequest<T>(path: string, init?: RequestInit): Promise<T> {
const response = await fetch(`${tradingRuntime.tradingApiUrl}${path}`, {
...init,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${getPlatformAccessToken()}`,
...(init?.headers || {}),
},
});
const body = await response.json().catch(() => ({} as any));
if (!response.ok) {
throw new Error(body?.error || `Request failed (${response.status})`);
}
return body as T;
}
export async function fetchMarketplacePresets(): Promise<any[]> {
const response = await apiRequest<{ presets: any[] }>('/api/marketplace-presets');
return Array.isArray(response.presets) ? response.presets : [];
}
export async function publishMarketplacePreset(payload: StrategyPresetPayload): Promise<void> {
await apiRequest<{ success: boolean }>('/api/marketplace-presets', {
method: 'POST',
body: JSON.stringify(payload),
});
}

View File

@ -0,0 +1,36 @@
import { getPlatformAccessToken } from './authSession';
import { tradingRuntime } from './runtime';
export interface PositionsBootstrapResponse {
entries: any[];
orders: any[];
historyTradeKeys: Array<{ trade_id: string; profile_id: string | null }>;
profiles: any[];
}
async function apiRequest<T>(path: string): Promise<T> {
const response = await fetch(`${tradingRuntime.tradingApiUrl}${path}`, {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${getPlatformAccessToken()}`,
},
});
const body = await response.json().catch(() => ({} as any));
if (!response.ok) {
throw new Error(body?.error || `Request failed (${response.status})`);
}
return body as T;
}
export async function fetchPositionsBootstrap(options?: { scope?: 'user' | 'all'; limit?: number }): Promise<PositionsBootstrapResponse> {
const params = new URLSearchParams();
if (options?.scope === 'all') {
params.set('scope', 'all');
}
if (options?.limit) {
params.set('limit', String(options.limit));
}
const query = params.toString() ? `?${params.toString()}` : '';
return apiRequest<PositionsBootstrapResponse>(`/api/positions/bootstrap${query}`);
}

View File

@ -1,4 +1,4 @@
import { supabase } from './supabaseClient';
import { getPlatformAccessToken } from './authSession';
import { tradingRuntime } from './runtime';
export interface TradeProfilePayload {
@ -31,12 +31,7 @@ export interface CurrentUserProfile {
}
async function getAccessToken(): Promise<string> {
const { data: sessionData } = await supabase.auth.getSession();
const accessToken = sessionData.session?.access_token;
if (!accessToken) {
throw new Error('Not authenticated');
}
return accessToken;
return getPlatformAccessToken();
}
async function apiRequest<T>(path: string, init?: RequestInit): Promise<T> {

View File

@ -0,0 +1,42 @@
import { getPlatformAccessToken } from './authSession';
import { tradingRuntime } from './runtime';
export interface TradeHistoryApiRow {
id?: string;
timestamp?: number | string;
symbol?: string;
side?: string;
size?: number;
entry_price?: number;
exit_price?: number;
pnl?: number;
pnl_percent?: number;
reason?: string;
profile_id?: string | null;
created_at?: string;
trade_id?: string;
source?: string;
}
export async function fetchTradeHistory(options?: { scope?: 'user' | 'all'; limit?: number }): Promise<TradeHistoryApiRow[]> {
const params = new URLSearchParams();
if (options?.scope === 'all') {
params.set('scope', 'all');
}
if (options?.limit) {
params.set('limit', String(options.limit));
}
const response = await fetch(`${tradingRuntime.tradingApiUrl}/api/trade-history${params.toString() ? `?${params.toString()}` : ''}`, {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${getPlatformAccessToken()}`,
},
});
const body = await response.json().catch(() => ({} as any));
if (!response.ok) {
throw new Error(body?.error || `Request failed (${response.status})`);
}
return Array.isArray(body?.rows) ? body.rows : [];
}

View File

@ -1,12 +1,11 @@
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import type { BotState } from '../hooks/useWebSocket';
import { supabase } from '../lib/supabaseClient';
import { getPlatformAccessToken } from '../lib/authSession';
import { tradingRuntime } from '../lib/runtime';
import { useAuth } from '../components/AuthContext';
import { tableNameStocks, tableNameOrders } from '../lib/const';
import { Layers, ListFilter, Link2, GitBranch, AlertTriangle, Lock, RefreshCw, CheckCircle, XCircle } from 'lucide-react';
import { useCanonicalLifecycle } from '../hooks/useCanonicalLifecycle';
import { fetchTradeProfiles } from '../lib/profileApi';
import { fetchPositionsBootstrap } from '../lib/positionsApi';
interface PositionsTabProps {
botState: BotState;
@ -448,100 +447,29 @@ export const PositionsTab = ({ botState }: PositionsTabProps) => {
const fetchData = async () => {
if (cancelled) return;
const positionQuery = supabase
.from(tableNameStocks)
.select('stock_instance_id,symbol,quantity,buy_price,drop_threshold_for_buy,gain_threshold_for_sell,active,status')
.eq('active', true)
.eq('status', 'active');
// Manual entries remain user-scoped even for admins.
const { data: posData, error: posError } = await positionQuery.eq('user_id', user.id);
const orderColumnsV3 = 'id,order_id,profile_id,symbol,type,side,qty,quantity,price,status,timestamp,filled_at,created_at,trade_id,action,source,stop_loss,take_profit,sub_tag';
const orderColumnsV2 = 'id,order_id,profile_id,symbol,type,side,qty,quantity,price,status,timestamp,filled_at,created_at,trade_id,action,source,stop_loss,take_profit';
const orderColumnsLegacy = 'id,order_id,profile_id,symbol,type,side,qty,price,status,timestamp,created_at';
let ordersQuery = supabase
.from(tableNameOrders)
.select(orderColumnsV3)
.order('created_at', { ascending: false })
.limit(ORDER_FETCH_LIMIT);
if (profile?.role !== 'admin') {
ordersQuery = ordersQuery.eq('user_id', user.id);
}
const { data: ordDataV3, error: ordErrorV3 } = await ordersQuery;
let ordData = ordDataV3 as RawOrderRecord[] | null;
if (ordErrorV3) {
console.warn('[PositionsTab] V3 order query failed, falling back to v2 columns:', ordErrorV3.message);
let v2Query = supabase
.from(tableNameOrders)
.select(orderColumnsV2)
.order('created_at', { ascending: false })
.limit(ORDER_FETCH_LIMIT);
if (profile?.role !== 'admin') {
v2Query = v2Query.eq('user_id', user.id);
}
const { data: ordDataV2, error: ordErrorV2 } = await v2Query;
ordData = ordDataV2 as RawOrderRecord[] | null;
if (ordErrorV2) {
console.warn('[PositionsTab] V2 order query failed, falling back to legacy columns:', ordErrorV2.message);
let legacyQuery = supabase
.from(tableNameOrders)
.select(orderColumnsLegacy)
.order('created_at', { ascending: false })
.limit(ORDER_FETCH_LIMIT);
if (profile?.role !== 'admin') {
legacyQuery = legacyQuery.eq('user_id', user.id);
}
const { data: ordDataLegacy, error: ordErrorLegacy } = await legacyQuery;
if (ordErrorLegacy) {
console.error('[PositionsTab] Legacy order query failed:', ordErrorLegacy.message);
}
ordData = ordDataLegacy as RawOrderRecord[] | null;
}
}
let historyQuery = supabase
.from('trade_history')
.select('trade_id,profile_id')
.order('created_at', { ascending: false })
.limit(ORDER_FETCH_LIMIT);
if (profile?.role !== 'admin') {
historyQuery = historyQuery.eq('user_id', user.id);
}
const { data: histData, error: histError } = await historyQuery;
if (histError) {
console.warn('[PositionsTab] History trade lookup failed:', histError.message);
}
let posData: any[] = [];
let ordData: RawOrderRecord[] | null = [];
let histData: RawHistoryRecord[] | null = [];
let profData: Array<{ id: string; name: string }> = [];
let profError: Error | null = null;
let bootstrapError: Error | null = null;
try {
const profiles = await fetchTradeProfiles({ scope: profile?.role === 'admin' ? 'all' : 'user' });
profData = profiles.map((item: any) => ({
const bootstrap = await fetchPositionsBootstrap({
scope: profile?.role === 'admin' ? 'all' : 'user',
limit: ORDER_FETCH_LIMIT
});
posData = Array.isArray(bootstrap.entries) ? bootstrap.entries : [];
ordData = Array.isArray(bootstrap.orders) ? (bootstrap.orders as RawOrderRecord[]) : [];
histData = Array.isArray(bootstrap.historyTradeKeys) ? (bootstrap.historyTradeKeys as RawHistoryRecord[]) : [];
profData = (Array.isArray(bootstrap.profiles) ? bootstrap.profiles : []).map((item: any) => ({
id: String(item.id),
name: String(item.name || item.id || 'Unnamed Profile')
}));
} catch (error) {
profError = error as Error;
bootstrapError = error as Error;
}
if (bootstrapError) {
console.error('[PositionsTab] Failed loading positions bootstrap:', bootstrapError.message);
}
if (posError) {
console.error('[PositionsTab] Failed loading manual positions:', posError.message);
}
if (profError) {
console.error('[PositionsTab] Failed loading profiles:', profError.message);
}
const tradeKeys = Array.from(new Set(
((histData as RawHistoryRecord[] | null) || [])
@ -1436,15 +1364,11 @@ export const PositionsTab = ({ botState }: PositionsTabProps) => {
{pos.source === 'BOT' && (
<button
onClick={async () => {
if (!confirm(`Are you sure you want to CLOSE ${pos.symbol}?`)) return;
try {
const { data: sessionData } = await supabase.auth.getSession();
const accessToken = sessionData.session?.access_token;
if (!accessToken) {
throw new Error('Not authenticated');
}
if (!confirm(`Are you sure you want to CLOSE ${pos.symbol}?`)) return;
try {
const accessToken = getPlatformAccessToken();
const response = await fetch(`${tradingRuntime.tradingApiUrl}/api/close`, {
method: 'POST',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`