154 lines
5.7 KiB
TypeScript
154 lines
5.7 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
|
|
vi.mock('../lib/supabaseClient', () => {
|
|
const query: any = {
|
|
select: vi.fn(() => query),
|
|
order: vi.fn(() => query),
|
|
eq: vi.fn(() => query),
|
|
limit: vi.fn(() => Promise.resolve({ data: [], error: null }))
|
|
};
|
|
|
|
return {
|
|
supabase: {
|
|
from: vi.fn(() => query)
|
|
}
|
|
};
|
|
});
|
|
|
|
vi.mock('./AuthContext', () => ({
|
|
useAuth: () => ({ user: null, profile: { role: 'admin' } })
|
|
}));
|
|
|
|
import {
|
|
filterProfilesBySearch,
|
|
normalizeSessionPresetValue,
|
|
normalizeStrategyConfig,
|
|
resolveSessionPresetSelection,
|
|
summarizePortfolioStats
|
|
} from './TradeProfileManager';
|
|
|
|
describe('normalizeStrategyConfig', () => {
|
|
it('normalizes legacy session aliases into canonical preset values', () => {
|
|
expect(normalizeSessionPresetValue('London,NY')).toBe('LDN,NY');
|
|
expect(normalizeSessionPresetValue('LDN | NY | TOK | SYD')).toBe('24/7');
|
|
expect(normalizeSessionPresetValue('24x7')).toBe('24/7');
|
|
expect(resolveSessionPresetSelection('Tokyo,Sydney')).toBe('TOK,SYD');
|
|
expect(resolveSessionPresetSelection('LDN,NY,custom')).toBe('LDN,NY');
|
|
expect(resolveSessionPresetSelection('LATAM')).toBe('custom');
|
|
});
|
|
|
|
it('returns safe defaults when config is missing', () => {
|
|
const normalized = normalizeStrategyConfig(undefined as any);
|
|
|
|
expect(normalized.rules).toEqual([]);
|
|
expect(normalized.riskLimits.maxDailyLossUsd).toBe(50);
|
|
expect(normalized.riskLimits.maxOpenTrades).toBe(3);
|
|
expect(normalized.execution.orderType).toBe('market');
|
|
expect(normalized.execution.cooldownMinutes).toBe(30);
|
|
expect(normalized.execution.entryMode).toBe('both');
|
|
});
|
|
|
|
it('normalizes legacy session and AI confidence parameters', () => {
|
|
const normalized = normalizeStrategyConfig({
|
|
rules: [
|
|
{
|
|
ruleId: 'SessionRule',
|
|
enabled: true,
|
|
params: { allowedSessions: 'London,NY' }
|
|
},
|
|
{
|
|
ruleId: 'AIAnalysisRule',
|
|
enabled: true,
|
|
params: { confidenceThreshold: 0.72 }
|
|
}
|
|
],
|
|
riskLimits: {
|
|
maxDailyLossUsd: 100,
|
|
maxOpenTrades: 4,
|
|
maxConsecutiveLosses: 3
|
|
},
|
|
execution: {
|
|
orderType: 'limit',
|
|
cooldownMinutes: 12,
|
|
entryMode: 'long_only'
|
|
}
|
|
} as any);
|
|
|
|
const sessionRule = normalized.rules.find((rule) => rule.ruleId === 'SessionRule');
|
|
const aiRule = normalized.rules.find((rule) => rule.ruleId === 'AIAnalysisRule');
|
|
|
|
expect(sessionRule?.params?.sessions).toBe('London,NY');
|
|
expect(aiRule?.params?.minConfidence).toBe(72);
|
|
expect(normalized.execution.orderType).toBe('limit');
|
|
expect(normalized.execution.entryMode).toBe('long_only');
|
|
});
|
|
|
|
it('coerces invalid numeric values back to defaults', () => {
|
|
const normalized = normalizeStrategyConfig({
|
|
rules: [],
|
|
riskLimits: {
|
|
maxDailyLossUsd: Number.NaN,
|
|
maxOpenTrades: 0,
|
|
maxConsecutiveLosses: -1
|
|
},
|
|
execution: {
|
|
orderType: 'unknown',
|
|
cooldownMinutes: -5,
|
|
longOnly: true
|
|
}
|
|
} as any);
|
|
|
|
expect(normalized.riskLimits.maxDailyLossUsd).toBe(50);
|
|
expect(normalized.riskLimits.maxOpenTrades).toBe(3);
|
|
expect(normalized.riskLimits.maxConsecutiveLosses).toBe(2);
|
|
expect(normalized.execution.orderType).toBe('market');
|
|
expect(normalized.execution.cooldownMinutes).toBe(30);
|
|
expect(normalized.execution.entryMode).toBe('long_only');
|
|
});
|
|
|
|
it('preserves maxConsecutiveLosses=0 as disabled', () => {
|
|
const normalized = normalizeStrategyConfig({
|
|
rules: [],
|
|
riskLimits: {
|
|
maxDailyLossUsd: 100,
|
|
maxOpenTrades: 3,
|
|
maxConsecutiveLosses: 0
|
|
},
|
|
execution: {
|
|
orderType: 'market',
|
|
cooldownMinutes: 10
|
|
}
|
|
} as any);
|
|
|
|
expect(normalized.riskLimits.maxConsecutiveLosses).toBe(0);
|
|
});
|
|
|
|
it('filters profiles by search term case-insensitively', () => {
|
|
const filtered = filterProfilesBySearch([
|
|
{ id: '1', user_id: 'u1', name: 'High Risk Scalper', allocated_capital: 1000, risk_per_trade_percent: 1, symbols: 'BTC/USDT', is_active: true },
|
|
{ id: '2', user_id: 'u1', name: 'Conservative Bag', allocated_capital: 2000, risk_per_trade_percent: 0.5, symbols: 'ETH/USDT', is_active: false }
|
|
] as any, 'conservative');
|
|
|
|
expect(filtered).toHaveLength(1);
|
|
expect(filtered[0].name).toBe('Conservative Bag');
|
|
});
|
|
|
|
it('summarizes capital and pnl metrics', () => {
|
|
const summary = summarizePortfolioStats(
|
|
[
|
|
{ id: '1', user_id: 'u1', name: 'A', allocated_capital: 1000, risk_per_trade_percent: 1, symbols: 'BTC/USDT', is_active: true },
|
|
{ id: '2', user_id: 'u1', name: 'B', allocated_capital: 1500, risk_per_trade_percent: 1, symbols: 'ETH/USDT', is_active: false }
|
|
] as any,
|
|
{
|
|
'1': { winRate: 60, totalPnl: 120, tradeCount: 4 },
|
|
'2': { winRate: 50, totalPnl: -20, tradeCount: 2 }
|
|
}
|
|
);
|
|
|
|
expect(summary.activeCount).toBe(1);
|
|
expect(summary.totalCapital).toBe(2500);
|
|
expect(summary.totalPnl).toBe(100);
|
|
expect(summary.totalTrades).toBe(6);
|
|
});
|
|
});
|