From b43368677617869364e4d55f65e48a94edb5a454 Mon Sep 17 00:00:00 2001 From: Saravana Achu Mac Date: Sat, 4 Apr 2026 16:49:18 -0700 Subject: [PATCH] refactor: move web trading data behind backend apis --- backend/src/services/AutoTrader.ts | 11 +- backend/src/services/apiServer.ts | 99 ++++++++++ .../src/services/orderActivityRepository.ts | 56 ++++++ .../src/services/strategyPresetRepository.ts | 42 +++++ .../src/services/tradeHistoryRepository.ts | 170 ++++++++++++++++++ web/src/components/PresetMarketplace.tsx | 47 +++-- web/src/components/TradeProfileManager.tsx | 27 +-- web/src/lib/authSession.ts | 52 ++++++ web/src/lib/manualEntriesApi.ts | 9 +- web/src/lib/marketplaceApi.ts | 45 +++++ web/src/lib/positionsApi.ts | 36 ++++ web/src/lib/profileApi.ts | 9 +- web/src/lib/tradeHistoryApi.ts | 42 +++++ web/src/tabs/PositionsTab.tsx | 122 +++---------- 14 files changed, 606 insertions(+), 161 deletions(-) create mode 100644 backend/src/services/orderActivityRepository.ts create mode 100644 backend/src/services/strategyPresetRepository.ts create mode 100644 backend/src/services/tradeHistoryRepository.ts create mode 100644 web/src/lib/authSession.ts create mode 100644 web/src/lib/marketplaceApi.ts create mode 100644 web/src/lib/positionsApi.ts create mode 100644 web/src/lib/tradeHistoryApi.ts diff --git a/backend/src/services/AutoTrader.ts b/backend/src/services/AutoTrader.ts index 597c6e2..57f6682 100644 --- a/backend/src/services/AutoTrader.ts +++ b/backend/src/services/AutoTrader.ts @@ -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, diff --git a/backend/src/services/apiServer.ts b/backend/src/services/apiServer.ts index bdcaec1..5b8cf8e 100644 --- a/backend/src/services/apiServer.ts +++ b/backend/src/services/apiServer.ts @@ -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; diff --git a/backend/src/services/orderActivityRepository.ts b/backend/src/services/orderActivityRepository.ts new file mode 100644 index 0000000..50d815b --- /dev/null +++ b/backend/src/services/orderActivityRepository.ts @@ -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 { + 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 []; + } +} diff --git a/backend/src/services/strategyPresetRepository.ts b/backend/src/services/strategyPresetRepository.ts new file mode 100644 index 0000000..594d745 --- /dev/null +++ b/backend/src/services/strategyPresetRepository.ts @@ -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 { + 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, legacyService?: LegacySupabaseService): Promise { + 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); + } +} diff --git a/backend/src/services/tradeHistoryRepository.ts b/backend/src/services/tradeHistoryRepository.ts new file mode 100644 index 0000000..d0ce806 --- /dev/null +++ b/backend/src/services/tradeHistoryRepository.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 []; + } +} diff --git a/web/src/components/PresetMarketplace.tsx b/web/src/components/PresetMarketplace.tsx index 1d0c34f..e961c90 100644 --- a/web/src/components/PresetMarketplace.tsx +++ b/web/src/components/PresetMarketplace.tsx @@ -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 = ({ onSelect, const [customPresets, setCustomPresets] = useState([]); 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(); diff --git a/web/src/components/TradeProfileManager.tsx b/web/src/components/TradeProfileManager.tsx index bb395ea..2b0644a 100644 --- a/web/src/components/TradeProfileManager.tsx +++ b/web/src/components/TradeProfileManager.tsx @@ -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); }; diff --git a/web/src/lib/authSession.ts b/web/src/lib/authSession.ts new file mode 100644 index 0000000..7a2b651 --- /dev/null +++ b/web/src/lib/authSession.ts @@ -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; +} + +export interface PlatformSession { + access_token: string; + refresh_token: string; + user: PlatformSessionUser; +} + +function parseJson(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(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; +} diff --git a/web/src/lib/manualEntriesApi.ts b/web/src/lib/manualEntriesApi.ts index 51990be..5ebfce6 100644 --- a/web/src/lib/manualEntriesApi.ts +++ b/web/src/lib/manualEntriesApi.ts @@ -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 { - 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(path: string, init?: RequestInit): Promise { diff --git a/web/src/lib/marketplaceApi.ts b/web/src/lib/marketplaceApi.ts new file mode 100644 index 0000000..e912d75 --- /dev/null +++ b/web/src/lib/marketplaceApi.ts @@ -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; +} + +async function apiRequest(path: string, init?: RequestInit): Promise { + 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 { + const response = await apiRequest<{ presets: any[] }>('/api/marketplace-presets'); + return Array.isArray(response.presets) ? response.presets : []; +} + +export async function publishMarketplacePreset(payload: StrategyPresetPayload): Promise { + await apiRequest<{ success: boolean }>('/api/marketplace-presets', { + method: 'POST', + body: JSON.stringify(payload), + }); +} diff --git a/web/src/lib/positionsApi.ts b/web/src/lib/positionsApi.ts new file mode 100644 index 0000000..139be17 --- /dev/null +++ b/web/src/lib/positionsApi.ts @@ -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(path: string): Promise { + 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 { + 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(`/api/positions/bootstrap${query}`); +} diff --git a/web/src/lib/profileApi.ts b/web/src/lib/profileApi.ts index a5db9c4..2634a75 100644 --- a/web/src/lib/profileApi.ts +++ b/web/src/lib/profileApi.ts @@ -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 { - 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(path: string, init?: RequestInit): Promise { diff --git a/web/src/lib/tradeHistoryApi.ts b/web/src/lib/tradeHistoryApi.ts new file mode 100644 index 0000000..09a6b48 --- /dev/null +++ b/web/src/lib/tradeHistoryApi.ts @@ -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 { + 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 : []; +} diff --git a/web/src/tabs/PositionsTab.tsx b/web/src/tabs/PositionsTab.tsx index 55afb23..6e82eca 100644 --- a/web/src/tabs/PositionsTab.tsx +++ b/web/src/tabs/PositionsTab.tsx @@ -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' && (