506 lines
22 KiB
TypeScript
506 lines
22 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { Indicators } from '../src/utils/indicators.js';
|
|
import { DirectionTracker, SignalType } from '../src/strategies/directionTracker.js';
|
|
import { TrendBiasRule } from '../src/strategies/rules/TrendBiasRule.js';
|
|
import { MomentumRule } from '../src/strategies/rules/MomentumRule.js';
|
|
import { ZoneRule } from '../src/strategies/rules/ZoneRule.js';
|
|
import { SessionRule } from '../src/strategies/rules/SessionRule.js';
|
|
import { EntryTriggerRule } from '../src/strategies/rules/EntryTriggerRule.js';
|
|
import { RiskManagementRule } from '../src/strategies/rules/RiskManagementRule.js';
|
|
import { AIAnalysisRule } from '../src/strategies/rules/AIAnalysisRule.js';
|
|
import { ProStrategyEngine } from '../src/strategies/ProStrategyEngine.js';
|
|
import { SignalDirection, type MarketContext, type RuleResult } from '../src/strategies/rules/types.js';
|
|
import { config } from '../src/config/index.js';
|
|
import { RiskEngine } from '../src/services/riskEngine.js';
|
|
import { AutoTrader } from '../src/services/AutoTrader.js';
|
|
import { ExecutionManager } from '../src/services/executionManager.js';
|
|
import { supabaseService } from '../src/services/SupabaseService.js';
|
|
import { metrics, MetricsService } from '../src/services/MetricsService.js';
|
|
|
|
const makeCandle = (timestamp: number, open: number, high: number, low: number, close: number, volume: number = 100) => ({
|
|
timestamp,
|
|
open,
|
|
high,
|
|
low,
|
|
close,
|
|
volume
|
|
});
|
|
|
|
const buildSeries = (count: number, start: number, step: number) => {
|
|
const out: Array<ReturnType<typeof makeCandle>> = [];
|
|
for (let i = 0; i < count; i++) {
|
|
const price = start + (step * i);
|
|
out.push(makeCandle(1700000000000 + (i * 3600000), price - 1, price + 2, price - 2, price, 100 + i));
|
|
}
|
|
return out;
|
|
};
|
|
|
|
const makeContext = (overrides: Partial<MarketContext> = {}): MarketContext => {
|
|
const candles1h = overrides.candles1h || buildSeries(120, 100, 1);
|
|
const candles4h = overrides.candles4h || buildSeries(320, 80, 1.5);
|
|
const candles15m = overrides.candles15m || buildSeries(120, 102, 0.4);
|
|
|
|
return {
|
|
symbol: overrides.symbol || 'BTC/USD',
|
|
candles4h,
|
|
candles1h,
|
|
candles15m,
|
|
currentPrice: overrides.currentPrice ?? candles15m[candles15m.length - 1].close,
|
|
change24h: overrides.change24h ?? 1.2,
|
|
changeToday: overrides.changeToday ?? 0.5,
|
|
session: overrides.session || 'LDN|NY',
|
|
isMajorSession: overrides.isMajorSession ?? true,
|
|
volatility: overrides.volatility || 'High',
|
|
latestSignal: overrides.latestSignal || SignalDirection.NONE,
|
|
ema20_1h: overrides.ema20_1h ?? 150,
|
|
ema20_15m: overrides.ema20_15m ?? 149,
|
|
ema50_4h: overrides.ema50_4h ?? 140,
|
|
ema200_4h: overrides.ema200_4h ?? 120,
|
|
rsi_1h: overrides.rsi_1h ?? 58,
|
|
rsi_15m: overrides.rsi_15m ?? 56
|
|
};
|
|
};
|
|
|
|
async function testIndicatorsAndDirectionTracker() {
|
|
assert.equal(Indicators.calculateEMA([], 10), 0, 'EMA should handle empty arrays');
|
|
assert.equal(Indicators.calculateRSI([1, 2, 3], 14), 50, 'RSI should return fallback for short series');
|
|
assert.equal(Indicators.calculateATR([], 14), 0, 'ATR should return 0 when candles are insufficient');
|
|
|
|
const atrCandles = buildSeries(20, 100, 1);
|
|
const atr = Indicators.calculateATR(atrCandles, 14);
|
|
assert(atr > 0, 'ATR should compute positive value for valid candles');
|
|
|
|
const tracker = new DirectionTracker();
|
|
const shortSeries = tracker.calculateDirection(buildSeries(10, 100, 0.2));
|
|
assert.equal(shortSeries.signal, SignalType.NONE);
|
|
assert.equal(shortSeries.changed, false);
|
|
|
|
const fromCloses = (closes: number[]) => closes.map((close, idx) =>
|
|
makeCandle(1700000000000 + (idx * 60000), close - 1, close + 1, close - 2, close, 100)
|
|
);
|
|
|
|
const buySeries = tracker.calculateDirection(fromCloses([
|
|
100, 99, 101, 100, 102, 101, 103, 102, 104, 103,
|
|
105, 104, 106, 105, 107, 106, 108, 107, 109, 108,
|
|
110, 109, 111, 110, 112, 111, 113, 112, 114, 113
|
|
]));
|
|
assert.equal(buySeries.signal, SignalType.BUY);
|
|
assert.equal(buySeries.changed, true);
|
|
|
|
const duplicateBuy = tracker.calculateDirection(fromCloses([
|
|
102, 101, 103, 102, 104, 103, 105, 104, 106, 105,
|
|
107, 106, 108, 107, 109, 108, 110, 109, 111, 110,
|
|
112, 111, 113, 112, 114, 113, 115, 114, 116, 115
|
|
]));
|
|
assert.equal(duplicateBuy.signal, SignalType.BUY);
|
|
assert.equal(duplicateBuy.changed, false, 'State machine should suppress duplicate signal transitions');
|
|
|
|
const sellSeries = tracker.calculateDirection(fromCloses([
|
|
200, 201, 199, 200, 198, 199, 197, 198, 196, 197,
|
|
195, 196, 194, 195, 193, 194, 192, 193, 191, 192,
|
|
190, 191, 189, 190, 188, 189, 187, 188, 186, 187
|
|
]));
|
|
assert.equal(sellSeries.signal, SignalType.SELL);
|
|
assert.equal(sellSeries.changed, true);
|
|
}
|
|
|
|
async function testRules() {
|
|
const trendRule = new TrendBiasRule();
|
|
const trendInvalid = await trendRule.check(makeContext(), { fastPeriod: 200, slowPeriod: 50 });
|
|
assert.equal(trendInvalid.passed, false);
|
|
|
|
const trendInsufficient = await trendRule.check(makeContext({ candles4h: buildSeries(10, 80, 1) }));
|
|
assert.equal(trendInsufficient.passed, false);
|
|
|
|
const trendBuy = await trendRule.check(makeContext({ currentPrice: 600, ema50_4h: 500, ema200_4h: 400 }));
|
|
assert.equal(trendBuy.signal, SignalDirection.BUY);
|
|
|
|
const trendSell = await trendRule.check(makeContext({ currentPrice: 60, candles4h: buildSeries(320, 500, -1) }));
|
|
assert.equal(trendSell.signal, SignalDirection.SELL);
|
|
|
|
const momentumRule = new MomentumRule();
|
|
const momentumShort = await momentumRule.check(
|
|
makeContext({ candles1h: buildSeries(5, 100, 1), candles15m: buildSeries(5, 100, 1) }),
|
|
{ timeframe: '1h', rsiPeriod: 20 }
|
|
);
|
|
assert.equal(momentumShort.passed, false);
|
|
|
|
const momentumBuySeries = [
|
|
100, 99, 101, 100, 102, 101, 103, 102, 104, 103,
|
|
105, 104, 106, 105, 107, 106, 108, 107, 109, 108,
|
|
110, 109, 111, 110, 112, 111, 113, 112, 114, 113,
|
|
115, 114, 116, 115, 117, 116, 118, 117, 119, 118
|
|
].map((close, idx) => makeCandle(1700000000000 + (idx * 60000), close - 1, close + 1, close - 2, close, 100));
|
|
const momentumBuy = await momentumRule.check(makeContext({ isMajorSession: true, candles1h: momentumBuySeries }), { timeframe: '1h' });
|
|
assert.equal(momentumBuy.signal, SignalDirection.BUY);
|
|
|
|
const momentumSellSeries = [
|
|
200, 201, 199, 200, 198, 199, 197, 198, 196, 197,
|
|
195, 196, 194, 195, 193, 194, 192, 193, 191, 192,
|
|
190, 191, 189, 190, 188, 189, 187, 188, 186, 187,
|
|
185, 186, 184, 185, 183, 184, 182, 183, 181, 182
|
|
].map((close, idx) => makeCandle(1700000000000 + (idx * 60000), close - 1, close + 1, close - 2, close, 100));
|
|
const momentumSell = await momentumRule.check(makeContext({ isMajorSession: false, candles1h: momentumSellSeries }), { timeframe: '1h' });
|
|
assert.equal(momentumSell.signal, SignalDirection.SELL);
|
|
|
|
const zoneRule = new ZoneRule();
|
|
const zoneNoEma = await zoneRule.check(makeContext({ ema20_15m: 0 }), { timeframe: '15m' });
|
|
assert.equal(zoneNoEma.passed, false);
|
|
|
|
const zonePass = await zoneRule.check(makeContext({ currentPrice: 101, ema20_1h: 100 }), { timeframe: '1h', zonePercent: 2 });
|
|
assert.equal(zonePass.passed, true);
|
|
|
|
const zoneFail = await zoneRule.check(makeContext({ currentPrice: 110, ema20_1h: 100 }), { timeframe: '1h', zonePercent: 1 });
|
|
assert.equal(zoneFail.passed, false);
|
|
|
|
const sessionRule = new SessionRule();
|
|
const sessionPass = await sessionRule.check(makeContext({ session: 'LDN|TOK', isMajorSession: true }), { sessions: 'LDN,NY' });
|
|
assert.equal(sessionPass.passed, true);
|
|
|
|
const sessionFail = await sessionRule.check(makeContext({ session: 'TOK', isMajorSession: false }), { sessions: 'NY' });
|
|
assert.equal(sessionFail.passed, false);
|
|
|
|
const sessionOff = await sessionRule.check(makeContext({ session: 'OFF', isMajorSession: false }));
|
|
assert.equal(sessionOff.passed, false);
|
|
|
|
const entryRule = new EntryTriggerRule();
|
|
const entryShort = await entryRule.check(makeContext({ candles1h: buildSeries(2, 100, 1) }));
|
|
assert.equal(entryShort.passed, false);
|
|
|
|
const entryNoEma = await entryRule.check(makeContext({ ema20_1h: 0 }), { timeframe: '1h' });
|
|
assert.equal(entryNoEma.passed, false);
|
|
|
|
const bullishReclaimCtx = makeContext({
|
|
candles1h: [
|
|
makeCandle(1, 100, 101, 99, 99),
|
|
makeCandle(2, 99, 101, 98, 98),
|
|
makeCandle(3, 98, 105, 97, 104)
|
|
],
|
|
ema20_1h: 100
|
|
});
|
|
const bullishReclaim = await entryRule.check(bullishReclaimCtx, { timeframe: '1h' });
|
|
assert.equal(bullishReclaim.signal, SignalDirection.BUY);
|
|
|
|
const bearishReclaimCtx = makeContext({
|
|
candles1h: [
|
|
makeCandle(1, 100, 105, 99, 104),
|
|
makeCandle(2, 104, 106, 102, 105),
|
|
makeCandle(3, 105, 106, 95, 96)
|
|
],
|
|
ema20_1h: 100
|
|
});
|
|
const bearishReclaim = await entryRule.check(bearishReclaimCtx, { timeframe: '1h', enableEmaReclaim: true, enableWickRejection: false });
|
|
assert.equal(bearishReclaim.signal, SignalDirection.SELL);
|
|
|
|
const riskRule = new RiskManagementRule();
|
|
const riskShort = await riskRule.check(makeContext({ candles1h: buildSeries(10, 100, 1) }));
|
|
assert.equal(riskShort.passed, false);
|
|
|
|
const riskPass = await riskRule.check(makeContext(), { maxRisk: 10 });
|
|
assert.equal(riskPass.passed, true);
|
|
|
|
const riskFail = await riskRule.check(makeContext({ currentPrice: 1, candles1h: buildSeries(120, 1, 1) }), { maxRisk: 0.1 });
|
|
assert.equal(riskFail.passed, false);
|
|
}
|
|
|
|
async function testAIAnalysisRule() {
|
|
const aiRule = new AIAnalysisRule();
|
|
const aiRuleAsAny = aiRule as any;
|
|
|
|
const originalFailOpen = config.AI.FAIL_OPEN;
|
|
try {
|
|
aiRuleAsAny.aiClient.generateAnalysis = async () => null;
|
|
|
|
config.AI.FAIL_OPEN = true;
|
|
const failOpenRes = await aiRule.check(makeContext());
|
|
assert.equal(failOpenRes.passed, true);
|
|
assert.equal(failOpenRes.metadata.ai_fail_open, true);
|
|
|
|
config.AI.FAIL_OPEN = false;
|
|
const failClosedRes = await aiRule.check(makeContext());
|
|
assert.equal(failClosedRes.passed, false);
|
|
|
|
aiRuleAsAny.aiClient.generateAnalysis = async () => '{"action":"BUY","confidence":0.82,"reasoning":"good"}';
|
|
config.AI.FAIL_OPEN = true;
|
|
const parseOk = await aiRule.check(makeContext(), { minConfidence: 70 });
|
|
assert.equal(parseOk.passed, true);
|
|
assert.equal(parseOk.signal, SignalDirection.BUY);
|
|
|
|
const parsedInvalid = aiRuleAsAny.parseResponse('not-json');
|
|
assert.equal(parsedInvalid.action, 'HOLD');
|
|
|
|
const normalizedPct = aiRuleAsAny.normalizeConfidenceToPct(0.77);
|
|
assert.equal(normalizedPct, 77);
|
|
const normalizedClamped = aiRuleAsAny.normalizeConfidenceToPct(220);
|
|
assert.equal(normalizedClamped, 100);
|
|
|
|
const prompt = aiRuleAsAny.buildPrompt(makeContext());
|
|
assert(prompt.includes('Analyze the following Crypto Market Data'));
|
|
} finally {
|
|
config.AI.FAIL_OPEN = originalFailOpen;
|
|
}
|
|
}
|
|
|
|
async function testRiskEngine() {
|
|
const engine = new RiskEngine();
|
|
const noSignal = await engine.calculateRiskProfile('BTC/USD', SignalDirection.NONE, makeContext());
|
|
assert.equal(noSignal, null);
|
|
|
|
const risk = await engine.calculateRiskProfile(
|
|
'BTC/USD',
|
|
SignalDirection.BUY,
|
|
makeContext(),
|
|
{
|
|
allocated_capital: 1000,
|
|
risk_per_trade_percent: 1,
|
|
strategy_config: {
|
|
execution: {
|
|
minQty: 0.001,
|
|
maxQty: 5,
|
|
qtyPrecision: 3,
|
|
minNotionalUsd: 10,
|
|
maxNotionalUsd: 10000
|
|
}
|
|
}
|
|
},
|
|
500
|
|
);
|
|
|
|
assert(risk !== null);
|
|
assert(risk!.positionSize > 0);
|
|
|
|
const rejectMinQty = await engine.calculateRiskProfile(
|
|
'BTC/USD',
|
|
SignalDirection.BUY,
|
|
makeContext({ currentPrice: 50000 }),
|
|
{
|
|
allocated_capital: 100,
|
|
risk_per_trade_percent: 0.1,
|
|
strategy_config: {
|
|
execution: {
|
|
minQty: 5,
|
|
maxQty: 10,
|
|
qtyPrecision: 2,
|
|
minNotionalUsd: 10,
|
|
maxNotionalUsd: 1000
|
|
}
|
|
}
|
|
},
|
|
5
|
|
);
|
|
assert.equal(rejectMinQty, null);
|
|
}
|
|
|
|
async function testProStrategyEngine() {
|
|
const fakeExchange = {
|
|
async fetchOHLCV(_symbol: string, timeframe: string) {
|
|
if (timeframe === '4h') return buildSeries(320, 100, 1.2);
|
|
if (timeframe === '1h') return buildSeries(140, 120, 0.8);
|
|
return buildSeries(140, 121, 0.2);
|
|
}
|
|
} as any;
|
|
|
|
const engine = new ProStrategyEngine(fakeExchange);
|
|
|
|
const context = await engine.buildMarketContext('BTC/USD');
|
|
assert(context, 'buildMarketContext should return context with enough candles');
|
|
|
|
const fail = await engine.evaluateContext(makeContext(), [
|
|
{ ruleId: 'TrendBiasRule', enabled: true, params: { fastPeriod: 200, slowPeriod: 50 } }
|
|
]);
|
|
assert.equal(fail?.passed, false);
|
|
|
|
const pass = await engine.evaluateContext(makeContext({
|
|
currentPrice: 600,
|
|
session: 'LDN|NY',
|
|
isMajorSession: true
|
|
}), [
|
|
{ ruleId: 'TrendBiasRule', enabled: true, params: { fastPeriod: 50, slowPeriod: 200 } },
|
|
{ ruleId: 'SessionRule', enabled: true, params: { sessions: 'LDN,NY' } },
|
|
{ ruleId: 'AIAnalysisRule', enabled: false, params: {} }
|
|
]);
|
|
|
|
assert(pass, 'evaluateContext should return a result object');
|
|
assert.equal(pass?.passed, true);
|
|
assert(pass?.metadata?.ruleStatuses, 'Rule statuses should be included in pass response');
|
|
}
|
|
|
|
async function testAutoTrader() {
|
|
const originalEnableTrading = config.ENABLE_TRADING;
|
|
const originalMaxOpen = config.MAX_OPEN_TRADES;
|
|
|
|
const originalDailyLoss = (supabaseService as any).getProfileDailyLossUsd;
|
|
const originalConsecutive = (supabaseService as any).getProfileConsecutiveLosses;
|
|
|
|
const closeCalls: Array<{ symbol: string; reason: string; tradeId?: string }> = [];
|
|
const openCalls: Array<any> = [];
|
|
|
|
const executor = {
|
|
profileId: 'profile-1',
|
|
positionsBySymbol: new Map<string, any[]>(),
|
|
checkCooldown: () => false,
|
|
getOpenPositionCount: () => 0,
|
|
getProfileId() { return this.profileId; },
|
|
getAllPositions: () => new Map<string, any>(),
|
|
getActivePositions(symbol: string) { return this.positionsBySymbol.get(symbol) || []; },
|
|
async closePosition(symbol: string, reason: string, tradeId?: string) {
|
|
closeCalls.push({ symbol, reason, tradeId });
|
|
},
|
|
async openPosition(...args: any[]) {
|
|
openCalls.push(args);
|
|
return { success: true };
|
|
}
|
|
} as any;
|
|
|
|
const exchange = {
|
|
async getPosition() { return null; }
|
|
} as any;
|
|
|
|
const trader = new AutoTrader(executor, exchange, async () => ({ allowed: true }));
|
|
const resultBuy: RuleResult = { ruleName: 'x', passed: true, signal: SignalDirection.BUY };
|
|
const resultSell: RuleResult = { ruleName: 'x', passed: true, signal: SignalDirection.SELL };
|
|
|
|
try {
|
|
(supabaseService as any).getProfileDailyLossUsd = async () => 0;
|
|
(supabaseService as any).getProfileConsecutiveLosses = async () => 0;
|
|
|
|
config.ENABLE_TRADING = false;
|
|
await trader.handleSignal('BTC/USD', resultBuy, makeContext());
|
|
assert.equal(openCalls.length, 0);
|
|
|
|
config.ENABLE_TRADING = true;
|
|
|
|
await trader.handleSignal('BTC/USD', resultBuy, makeContext(), { symbols: 'ETH/USD' });
|
|
assert.equal(openCalls.length, 0, 'Symbol filtering should skip symbols not in profile list');
|
|
|
|
executor.positionsBySymbol.set('BTC/USD', [{ side: SignalDirection.BUY, entryPrice: 100, peakPrice: 100, tradeId: 't1' }]);
|
|
await trader.handleSignal('BTC/USD', resultSell, makeContext({ currentPrice: 95 }), { strategy_config: { execution: { allowPyramiding: false } } });
|
|
assert.equal(closeCalls.length, 1, 'Opposite signal should close existing position');
|
|
|
|
executor.positionsBySymbol.clear();
|
|
|
|
await trader.handleSignal('BTC/USD', resultSell, makeContext(), { strategy_config: { execution: { entryMode: 'long_only' } } });
|
|
assert.equal(openCalls.length, 0, 'long_only should block SELL entries');
|
|
|
|
const blockedTrader = new AutoTrader(executor, exchange, async () => ({ allowed: false, reason: 'blocked' }));
|
|
await blockedTrader.handleSignal('BTC/USD', resultBuy, makeContext());
|
|
assert.equal(openCalls.length, 0, 'Portfolio guard block should stop entries');
|
|
|
|
const maxOpenExecutor = { ...executor, getOpenPositionCount: () => 999 } as any;
|
|
const maxOpenTrader = new AutoTrader(maxOpenExecutor, exchange);
|
|
config.MAX_OPEN_TRADES = 1;
|
|
await maxOpenTrader.handleSignal('BTC/USD', resultBuy, makeContext());
|
|
assert.equal(openCalls.length, 0, 'Max open trade guard should block entries');
|
|
|
|
const cooldownExecutor = { ...executor, checkCooldown: () => true } as any;
|
|
const cooldownTrader = new AutoTrader(cooldownExecutor, exchange);
|
|
await cooldownTrader.handleSignal('BTC/USD', resultBuy, makeContext());
|
|
assert.equal(openCalls.length, 0, 'Cooldown guard should block entries');
|
|
|
|
(supabaseService as any).getProfileDailyLossUsd = async () => 500;
|
|
const riskBlocked = new AutoTrader(executor, exchange);
|
|
await riskBlocked.handleSignal('BTC/USD', resultBuy, makeContext(), {
|
|
strategy_config: { riskLimits: { maxDailyLossUsd: 100, maxConsecutiveLosses: 5 } }
|
|
});
|
|
assert.equal(openCalls.length, 0, 'Runtime risk guard should block entries when daily loss breached');
|
|
|
|
(supabaseService as any).getProfileDailyLossUsd = async () => 0;
|
|
(supabaseService as any).getProfileConsecutiveLosses = async () => 0;
|
|
|
|
await trader.handleSignal('BTC/USD', resultBuy, makeContext(), { user_id: 'user-1', allocated_capital: 1000, risk_per_trade_percent: 1 });
|
|
assert.equal(openCalls.length, 1, 'Happy path should open new position');
|
|
} finally {
|
|
config.ENABLE_TRADING = originalEnableTrading;
|
|
config.MAX_OPEN_TRADES = originalMaxOpen;
|
|
(supabaseService as any).getProfileDailyLossUsd = originalDailyLoss;
|
|
(supabaseService as any).getProfileConsecutiveLosses = originalConsecutive;
|
|
}
|
|
}
|
|
|
|
async function testExecutionManagerAndMetrics() {
|
|
const originalEnableTrading = config.ENABLE_TRADING;
|
|
const originalAllowLegacy = process.env.ALLOW_LEGACY_EXECUTION_MANAGER;
|
|
|
|
const fakeExchange = {
|
|
async getPosition() { return null; },
|
|
async placeOrder(_symbol: string, _side: 'buy' | 'sell', qty: number) {
|
|
return { id: `ord-${qty}`, filled_avg_price: 123.45 };
|
|
}
|
|
} as any;
|
|
|
|
const apiServerMock = {
|
|
updateOrders: (_orders: any[]) => undefined,
|
|
addHistory: (_trade: any) => undefined
|
|
} as any;
|
|
|
|
try {
|
|
process.env.ALLOW_LEGACY_EXECUTION_MANAGER = 'true';
|
|
config.ENABLE_TRADING = true;
|
|
|
|
const manager = new ExecutionManager(fakeExchange, apiServerMock, 'global');
|
|
const ctx = makeContext({ currentPrice: 120 });
|
|
|
|
await manager.handleSignal('BTC/USD', { ruleName: 'r', passed: true, signal: SignalDirection.BUY }, ctx);
|
|
assert.equal(manager.isSymbolLocked('BTC/USD'), true);
|
|
|
|
await manager.handleSignal('BTC/USD', { ruleName: 'r', passed: true, signal: SignalDirection.SELL }, makeContext({ currentPrice: 130 }));
|
|
assert.equal(manager.isSymbolLocked('BTC/USD'), false, 'Opposite signal should close and unlock symbol');
|
|
|
|
await manager.executeManualTrade('ETH/USD', 'buy', 1, 'market', 125);
|
|
assert.equal(manager.getActivePosition('ETH/USD')?.side, SignalDirection.BUY);
|
|
|
|
manager.markTradeComplete('ETH/USD', 130, 'manual test close');
|
|
assert.equal(manager.getActivePosition('ETH/USD'), null);
|
|
|
|
const syncExchange = {
|
|
async getPosition(symbol: string) {
|
|
if (symbol.includes('SOL')) {
|
|
return { side: 'long', avg_entry_price: '50', qty: '2' };
|
|
}
|
|
return null;
|
|
},
|
|
async placeOrder() {
|
|
return { id: 'dummy' };
|
|
}
|
|
} as any;
|
|
|
|
const manager2 = new ExecutionManager(syncExchange, apiServerMock, 'global');
|
|
await manager2.syncPositions(['SOL/USD', 'BTC/USD']);
|
|
assert.equal(manager2.isSymbolLocked('SOL/USD'), true);
|
|
assert.equal(manager2.isSymbolLocked('BTC/USD'), false);
|
|
|
|
// Metrics singleton calls
|
|
metrics.operationalEventsTotal.labels({
|
|
severity: 'INFO',
|
|
type: 'COVERAGE_TEST',
|
|
profile_id: 'p1',
|
|
symbol: 'BTC/USD'
|
|
}).inc();
|
|
const svc = MetricsService.getInstance();
|
|
const exposition = await svc.getMetrics();
|
|
assert(exposition.includes('bytelyst_bot_operational_events_total'));
|
|
assert.equal(typeof svc.getContentType(), 'string');
|
|
} finally {
|
|
if (originalAllowLegacy === undefined) {
|
|
delete process.env.ALLOW_LEGACY_EXECUTION_MANAGER;
|
|
} else {
|
|
process.env.ALLOW_LEGACY_EXECUTION_MANAGER = originalAllowLegacy;
|
|
}
|
|
config.ENABLE_TRADING = originalEnableTrading;
|
|
}
|
|
}
|
|
|
|
async function run() {
|
|
await testIndicatorsAndDirectionTracker();
|
|
await testRules();
|
|
await testAIAnalysisRule();
|
|
await testRiskEngine();
|
|
await testProStrategyEngine();
|
|
await testAutoTrader();
|
|
await testExecutionManagerAndMetrics();
|
|
|
|
console.log('[core-module-coverage] OK: core strategy, risk, trader, and legacy execution paths validated');
|
|
}
|
|
|
|
await run();
|