Spaces:
Running
Running
Commit ·
7a6768e
1
Parent(s): 2d6848e
Remove unprofitable 15m timeframe symbols (ETHUSDT & EURUSD) from the entire application
Browse files- scratch/analyzeETH.js +115 -0
- scratch/backtestSignals.js +234 -0
- scratch/checkAllTimeframes.js +71 -0
- scratch/checkForexVolume.js +42 -0
- scratch/findWicks.js +76 -0
- scratch/highFidelityBacktest.js +251 -0
- scratch/optimizeParams.js +195 -0
- scratch/simulateSignals.js +66 -0
- scratch/testMarketDataEndpoint.js +40 -0
- scratch/testTelegram.js +49 -0
- scratch/testUSDCAD.js +42 -0
- server.js +3 -6
- src/analysis/exitManager.js +1 -5
- src/analysis/signalGenerator.js +1 -13
- src/chart/chartManager.js +3 -3
- src/components/analysisPanel.js +2 -8
- src/components/header.js +0 -1
- src/components/tradeManager.js +1 -1
- src/data/dataManager.js +0 -2
- src/data/twelveDataAPI.js +0 -2
- src/data/yahooFinanceAPI.js +1 -3
- src/main.js +0 -2
scratch/analyzeETH.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// scratch/analyzeETH.js
|
| 2 |
+
import { fetchCandles } from '../src/data/yahooFinanceAPI.js';
|
| 3 |
+
import { analyzeTrend } from '../src/analysis/trendDetector.js';
|
| 4 |
+
import { detectStructure } from '../src/analysis/marketStructure.js';
|
| 5 |
+
import { detectOrderBlocks, updateMitigation } from '../src/analysis/orderBlocks.js';
|
| 6 |
+
import { detectZones, updateZoneStatus } from '../src/analysis/supplyDemand.js';
|
| 7 |
+
import { rsi, ema, macd } from '../src/analysis/indicators.js';
|
| 8 |
+
import { analyzeExit } from '../src/analysis/exitManager.js';
|
| 9 |
+
|
| 10 |
+
// Polyfill fetch exactly like server.js
|
| 11 |
+
const originalFetch = globalThis.fetch;
|
| 12 |
+
globalThis.fetch = async (input, init) => {
|
| 13 |
+
let url = typeof input === 'string' ? input : input.url;
|
| 14 |
+
|
| 15 |
+
if (url.startsWith('/api/swissquote')) {
|
| 16 |
+
url = 'https://forex-data-feed.swissquote.com/public-quotes/bboquotes/instrument/XAU/USD';
|
| 17 |
+
} else if (url.startsWith('/api/yahoo')) {
|
| 18 |
+
url = 'https://query1.finance.yahoo.com' + url.replace(/^\/api\/yahoo/, '');
|
| 19 |
+
init = init || {};
|
| 20 |
+
init.headers = {
|
| 21 |
+
...init.headers,
|
| 22 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
| 23 |
+
'Origin': 'https://finance.yahoo.com',
|
| 24 |
+
'Referer': 'https://finance.yahoo.com',
|
| 25 |
+
};
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
return originalFetch(url, init);
|
| 29 |
+
};
|
| 30 |
+
|
| 31 |
+
async function run() {
|
| 32 |
+
console.log('=== ETHUSDT Live Analysis Dashboard ===\n');
|
| 33 |
+
try {
|
| 34 |
+
const symbol = 'ETHUSDT';
|
| 35 |
+
const candles = await fetchCandles(symbol, '15m');
|
| 36 |
+
console.log(`[Market Data] Loaded ${candles.length} candles from Yahoo Finance.`);
|
| 37 |
+
|
| 38 |
+
if (candles.length === 0) {
|
| 39 |
+
console.error('No market data loaded.');
|
| 40 |
+
return;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
const latest = candles[candles.length - 1];
|
| 44 |
+
console.log(`Current Price: $${latest.close.toFixed(2)} | Time: ${new Date(latest.time * 1000).toLocaleString()}`);
|
| 45 |
+
|
| 46 |
+
// 1. Indicators
|
| 47 |
+
const closes = candles.map(c => c.close);
|
| 48 |
+
const rsiVals = rsi(closes);
|
| 49 |
+
const ema9 = ema(closes, 9);
|
| 50 |
+
const ema21 = ema(closes, 21);
|
| 51 |
+
const ema50 = ema(closes, 50);
|
| 52 |
+
const ema200 = ema(closes, 200);
|
| 53 |
+
const macdData = macd(closes);
|
| 54 |
+
|
| 55 |
+
const lastRSI = rsiVals[rsiVals.length - 1];
|
| 56 |
+
const lastE9 = ema9[ema9.length - 1];
|
| 57 |
+
const lastE21 = ema21[ema21.length - 1];
|
| 58 |
+
const lastE50 = ema50[ema50.length - 1];
|
| 59 |
+
const lastE200 = ema200[ema200.length - 1];
|
| 60 |
+
|
| 61 |
+
console.log('\n📊 Indicators:');
|
| 62 |
+
console.log(`- RSI (14): ${lastRSI.toFixed(2)} (${lastRSI > 70 ? 'Overbought' : lastRSI < 30 ? 'Oversold' : 'Neutral'})`);
|
| 63 |
+
console.log(`- EMA 9: $${lastE9.toFixed(2)}`);
|
| 64 |
+
console.log(`- EMA 21: $${lastE21.toFixed(2)}`);
|
| 65 |
+
console.log(`- EMA 50: $${lastE50.toFixed(2)}`);
|
| 66 |
+
console.log(`- EMA 200: $${lastE200.toFixed(2)}`);
|
| 67 |
+
|
| 68 |
+
// 2. Trend & Structure
|
| 69 |
+
const trend = analyzeTrend(candles);
|
| 70 |
+
const structure = detectStructure(candles);
|
| 71 |
+
console.log('\n📈 Trend & Market Structure:');
|
| 72 |
+
console.log(`- Direction: ${trend.direction.toUpperCase()} (Strength: ${trend.strength}/100)`);
|
| 73 |
+
console.log(`- Structure Trend: ${structure.trend}`);
|
| 74 |
+
console.log(`- EMA Alignment: ${trend.emaAlignment}`);
|
| 75 |
+
console.log(`- Momentum: ${trend.momentum}`);
|
| 76 |
+
|
| 77 |
+
// 3. SMC Areas
|
| 78 |
+
let obs = detectOrderBlocks(candles);
|
| 79 |
+
obs = updateMitigation(obs, candles);
|
| 80 |
+
const activeOBs = obs.filter(o => !o.mitigated);
|
| 81 |
+
console.log(`- Active Order Blocks: ${activeOBs.length} detected.`);
|
| 82 |
+
if (activeOBs.length > 0) {
|
| 83 |
+
console.log(` * Recent OB: Type: ${activeOBs[activeOBs.length - 1].type.toUpperCase()}, Top: $${activeOBs[activeOBs.length - 1].top.toFixed(2)}, Bottom: $${activeOBs[activeOBs.length - 1].bottom.toFixed(2)}`);
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
// 4. Exit Manager for User's trade
|
| 87 |
+
const userTrade = {
|
| 88 |
+
type: 'SHORT',
|
| 89 |
+
entry: 1979.05,
|
| 90 |
+
sl: 1996.46,
|
| 91 |
+
tp1: 1979.05 - (1996.46 - 1979.05) * 2, // approximate TP1 = entry - 2R
|
| 92 |
+
tp2: 1926.81,
|
| 93 |
+
lotSize: 2.87,
|
| 94 |
+
symbol: 'ETHUSDT'
|
| 95 |
+
};
|
| 96 |
+
|
| 97 |
+
const exitAnalysis = analyzeExit(candles, userTrade);
|
| 98 |
+
console.log('\n🛡️ Active Trade Analysis (SHORT ETHUSDT):');
|
| 99 |
+
console.log(`- Current Unrealized P&L: $${exitAnalysis.currentPnL.toFixed(2)}`);
|
| 100 |
+
console.log(`- Current R:R: ${exitAnalysis.currentRR.toFixed(2)}`);
|
| 101 |
+
console.log(`- Suggestion: ${exitAnalysis.suggestion}`);
|
| 102 |
+
console.log(`- Recommendation: ${exitAnalysis.reason}`);
|
| 103 |
+
if (exitAnalysis.warnings && exitAnalysis.warnings.length > 0) {
|
| 104 |
+
console.log('- Warnings:');
|
| 105 |
+
exitAnalysis.warnings.forEach(w => console.log(` * ⚠️ ${w}`));
|
| 106 |
+
} else {
|
| 107 |
+
console.log('- Warnings: None (normal parameters)');
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
} catch (err) {
|
| 111 |
+
console.error('Analysis failed:', err);
|
| 112 |
+
}
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
run();
|
scratch/backtestSignals.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// backtestSignals.js
|
| 2 |
+
import { fetchCandles } from '../src/data/yahooFinanceAPI.js';
|
| 3 |
+
import { generateSignals } from '../src/analysis/signalGenerator.js';
|
| 4 |
+
|
| 5 |
+
// Polyfill fetch exactly like server.js
|
| 6 |
+
const originalFetch = globalThis.fetch;
|
| 7 |
+
globalThis.fetch = async (input, init) => {
|
| 8 |
+
let url = typeof input === 'string' ? input : input.url;
|
| 9 |
+
|
| 10 |
+
if (url.startsWith('/api/swissquote')) {
|
| 11 |
+
url = 'https://forex-data-feed.swissquote.com/public-quotes/bboquotes/instrument/XAU/USD';
|
| 12 |
+
} else if (url.startsWith('/api/yahoo')) {
|
| 13 |
+
url = 'https://query1.finance.yahoo.com' + url.replace(/^\/api\/yahoo/, '');
|
| 14 |
+
init = init || {};
|
| 15 |
+
init.headers = {
|
| 16 |
+
...init.headers,
|
| 17 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
| 18 |
+
'Origin': 'https://finance.yahoo.com',
|
| 19 |
+
'Referer': 'https://finance.yahoo.com',
|
| 20 |
+
};
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
return originalFetch(url, init);
|
| 24 |
+
};
|
| 25 |
+
|
| 26 |
+
function calculatePnL(symbol, type, entry, exit, lots) {
|
| 27 |
+
const direction = type === 'LONG' ? 1 : -1;
|
| 28 |
+
const diff = (exit - entry) * direction;
|
| 29 |
+
if (symbol === 'BTCUSDT' || symbol === 'ETHUSDT') {
|
| 30 |
+
return diff * lots;
|
| 31 |
+
} else if (symbol === 'XAUUSD') {
|
| 32 |
+
return diff * lots * 100;
|
| 33 |
+
} else if (symbol === 'EURUSD' || symbol === 'GBPUSD' || symbol === 'USDCAD') {
|
| 34 |
+
const rate = symbol === 'USDCAD' ? 1.38 : 1.0; // approximation for USDCAD USD-conversion
|
| 35 |
+
const pipScale = symbol === 'USDCAD' ? (10 / rate) : 10;
|
| 36 |
+
return (diff / 0.0001) * lots * pipScale;
|
| 37 |
+
}
|
| 38 |
+
return 0;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
function calculateCommission(symbol, entryPrice, lots) {
|
| 42 |
+
if (symbol === 'BTCUSDT' || symbol === 'ETHUSDT') {
|
| 43 |
+
return 0.0008 * entryPrice * lots;
|
| 44 |
+
} else {
|
| 45 |
+
return 5.0 * lots;
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
function getSpread(symbol) {
|
| 50 |
+
switch (symbol) {
|
| 51 |
+
case 'BTCUSDT': return 25.0;
|
| 52 |
+
case 'ETHUSDT': return 5.0;
|
| 53 |
+
case 'XAUUSD': return 0.7;
|
| 54 |
+
case 'EURUSD':
|
| 55 |
+
case 'GBPUSD':
|
| 56 |
+
case 'USDCAD':
|
| 57 |
+
return 0.00007;
|
| 58 |
+
default:
|
| 59 |
+
return 0;
|
| 60 |
+
}
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
function backtestTrade(symbol, signal, candles, startIndex) {
|
| 64 |
+
let partialClosed = false;
|
| 65 |
+
let sl = signal.sl;
|
| 66 |
+
let entry = signal.entry;
|
| 67 |
+
let lots = signal.lotSize;
|
| 68 |
+
let commission = calculateCommission(symbol, entry, lots);
|
| 69 |
+
const spread = getSpread(symbol);
|
| 70 |
+
|
| 71 |
+
for (let c = startIndex; c < candles.length; c++) {
|
| 72 |
+
const candle = candles[c];
|
| 73 |
+
|
| 74 |
+
if (signal.type === 'LONG') {
|
| 75 |
+
// Check SL hit
|
| 76 |
+
if (candle.low <= sl) {
|
| 77 |
+
if (!partialClosed) {
|
| 78 |
+
// Full SL hit
|
| 79 |
+
const pnl = calculatePnL(symbol, 'LONG', entry, sl, lots) - commission;
|
| 80 |
+
return { status: 'LOSS', pnl, exitTime: candle.time, exitPrice: sl };
|
| 81 |
+
} else {
|
| 82 |
+
// Stopped out at breakeven (entry) on remaining 30%
|
| 83 |
+
const tp1PnL = calculatePnL(symbol, 'LONG', entry, signal.tp1, lots * 0.7);
|
| 84 |
+
const pnl = tp1PnL - commission;
|
| 85 |
+
return { status: 'PARTIAL_STOP', pnl, exitTime: candle.time, exitPrice: sl };
|
| 86 |
+
}
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
// Check TP1 hit
|
| 90 |
+
if (!partialClosed && candle.high >= signal.tp1) {
|
| 91 |
+
partialClosed = true;
|
| 92 |
+
sl = entry; // Trail SL to breakeven
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
// Check TP2 hit
|
| 96 |
+
if (partialClosed && candle.high >= signal.tp2) {
|
| 97 |
+
const tp1PnL = calculatePnL(symbol, 'LONG', entry, signal.tp1, lots * 0.7);
|
| 98 |
+
const tp2PnL = calculatePnL(symbol, 'LONG', entry, signal.tp2, lots * 0.3);
|
| 99 |
+
const pnl = tp1PnL + tp2PnL - commission;
|
| 100 |
+
return { status: 'WIN', pnl, exitTime: candle.time, exitPrice: signal.tp2 };
|
| 101 |
+
}
|
| 102 |
+
} else {
|
| 103 |
+
// SHORT: exits occur at Ask price = chart price + spread
|
| 104 |
+
// Check SL hit
|
| 105 |
+
if (candle.high + spread >= sl) {
|
| 106 |
+
if (!partialClosed) {
|
| 107 |
+
const pnl = calculatePnL(symbol, 'SHORT', entry, sl, lots) - commission;
|
| 108 |
+
return { status: 'LOSS', pnl, exitTime: candle.time, exitPrice: sl };
|
| 109 |
+
} else {
|
| 110 |
+
const tp1PnL = calculatePnL(symbol, 'SHORT', entry, signal.tp1, lots * 0.7);
|
| 111 |
+
const pnl = tp1PnL - commission;
|
| 112 |
+
return { status: 'PARTIAL_STOP', pnl, exitTime: candle.time, exitPrice: sl };
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
// Check TP1 hit
|
| 117 |
+
if (!partialClosed && candle.low + spread <= signal.tp1) {
|
| 118 |
+
partialClosed = true;
|
| 119 |
+
sl = entry; // Trail SL to breakeven
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
// Check TP2 hit
|
| 123 |
+
if (partialClosed && candle.low + spread <= signal.tp2) {
|
| 124 |
+
const tp1PnL = calculatePnL(symbol, 'SHORT', entry, signal.tp1, lots * 0.7);
|
| 125 |
+
const tp2PnL = calculatePnL(symbol, 'SHORT', entry, signal.tp2, lots * 0.3);
|
| 126 |
+
const pnl = tp1PnL + tp2PnL - commission;
|
| 127 |
+
return { status: 'WIN', pnl, exitTime: candle.time, exitPrice: signal.tp2 };
|
| 128 |
+
}
|
| 129 |
+
}
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
// Trade still open at end of history
|
| 133 |
+
const lastCandle = candles[candles.length - 1];
|
| 134 |
+
const valuation = lastCandle.close;
|
| 135 |
+
if (!partialClosed) {
|
| 136 |
+
const valuationPrice = signal.type === 'SHORT' ? valuation + spread : valuation;
|
| 137 |
+
const pnl = calculatePnL(symbol, signal.type, entry, valuationPrice, lots) - commission;
|
| 138 |
+
return { status: 'OPEN', pnl, exitTime: lastCandle.time, exitPrice: valuationPrice };
|
| 139 |
+
} else {
|
| 140 |
+
const valuationPrice = signal.type === 'SHORT' ? valuation + spread : valuation;
|
| 141 |
+
const tp1PnL = calculatePnL(symbol, signal.type, entry, signal.tp1, lots * 0.7);
|
| 142 |
+
const tp2PnL = calculatePnL(symbol, signal.type, entry, valuationPrice, lots * 0.3);
|
| 143 |
+
const pnl = tp1PnL + tp2PnL - commission;
|
| 144 |
+
return { status: 'OPEN_PARTIAL', pnl, exitTime: lastCandle.time, exitPrice: valuationPrice };
|
| 145 |
+
}
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
const SYMBOLS = ['BTCUSDT', 'ETHUSDT', 'XAUUSD', 'GBPUSD', 'EURUSD', 'USDCAD'];
|
| 149 |
+
|
| 150 |
+
async function runBacktest() {
|
| 151 |
+
console.log('=== RUNNING HISTORICAL BACKTEST & QUALITY METRICS ===');
|
| 152 |
+
console.log('Parameters: 1H Timeframe, Spread & Commission Enabled, TP1 (70%) + BE SL Trailing\n');
|
| 153 |
+
|
| 154 |
+
for (const symbol of SYMBOLS) {
|
| 155 |
+
try {
|
| 156 |
+
const candles = await fetchCandles(symbol, '1H');
|
| 157 |
+
if (!candles || candles.length < 100) {
|
| 158 |
+
console.log(`[${symbol}] Insufficient data for backtest.`);
|
| 159 |
+
continue;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
let totalTrades = 0;
|
| 163 |
+
let fullWins = 0; // Hit TP2
|
| 164 |
+
let partialWins = 0; // Hit TP1, then stopped at BE
|
| 165 |
+
let fullLosses = 0; // Hit SL before TP1
|
| 166 |
+
let openTrades = 0;
|
| 167 |
+
let totalPnL = 0;
|
| 168 |
+
|
| 169 |
+
// Let's slide a window through historical candles
|
| 170 |
+
// To simulate real execution, we keep a cooldown timer per symbol
|
| 171 |
+
let lastClosedTime = 0;
|
| 172 |
+
let isCooldown = false;
|
| 173 |
+
let activeTradeEndIndex = -1;
|
| 174 |
+
|
| 175 |
+
for (let i = 50; i < candles.length; i++) {
|
| 176 |
+
// Check if there is an active trade running
|
| 177 |
+
if (i <= activeTradeEndIndex) {
|
| 178 |
+
continue; // skip scanning if in a trade
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
// Check cooldown (10 minutes = 1 candle on 15m TF)
|
| 182 |
+
if (lastClosedTime > 0 && (candles[i].time - lastClosedTime) < 600) {
|
| 183 |
+
continue; // skip scanning if cooling down
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
const window = candles.slice(0, i);
|
| 187 |
+
const signals = generateSignals(window, symbol, 0);
|
| 188 |
+
|
| 189 |
+
// Filter signals to Grade A and B
|
| 190 |
+
const validSignals = signals.filter(sig => ['A', 'B'].includes(sig.quality));
|
| 191 |
+
if (validSignals.length === 0) continue;
|
| 192 |
+
|
| 193 |
+
const signal = validSignals[0];
|
| 194 |
+
// We evaluate the trade outcome starting from candle index `i` (entry is at the close of candle `i-1`)
|
| 195 |
+
const result = backtestTrade(symbol, signal, candles, i);
|
| 196 |
+
|
| 197 |
+
totalTrades++;
|
| 198 |
+
totalPnL += result.pnl;
|
| 199 |
+
|
| 200 |
+
// Estimate index of trade exit
|
| 201 |
+
let exitIndex = candles.findIndex(c => c.time === result.exitTime);
|
| 202 |
+
if (exitIndex === -1) exitIndex = candles.length - 1;
|
| 203 |
+
|
| 204 |
+
activeTradeEndIndex = exitIndex;
|
| 205 |
+
lastClosedTime = result.exitTime;
|
| 206 |
+
|
| 207 |
+
if (result.status === 'WIN') fullWins++;
|
| 208 |
+
else if (result.status === 'PARTIAL_STOP') partialWins++;
|
| 209 |
+
else if (result.status === 'LOSS') fullLosses++;
|
| 210 |
+
else openTrades++;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
const completedTrades = fullWins + partialWins + fullLosses;
|
| 214 |
+
const winRate = completedTrades > 0 ? ((fullWins + partialWins) / completedTrades) * 100 : 0;
|
| 215 |
+
const fullWinRate = completedTrades > 0 ? (fullWins / completedTrades) * 100 : 0;
|
| 216 |
+
const avgPnL = completedTrades > 0 ? totalPnL / completedTrades : 0;
|
| 217 |
+
|
| 218 |
+
console.log(`[${symbol}] Backtest results over ${candles.length} candles (~${Math.round(candles.length / 96)} days):`);
|
| 219 |
+
console.log(` Total Scanned Signals Executed: ${totalTrades}`);
|
| 220 |
+
console.log(` Completed Outcomes: ${completedTrades} (Open: ${openTrades})`);
|
| 221 |
+
console.log(` - Hit TP2 (Full Win): ${fullWins} (${fullWinRate.toFixed(1)}%)`);
|
| 222 |
+
console.log(` - Hit TP1, then BE Stop (Partial Profit): ${partialWins}`);
|
| 223 |
+
console.log(` - Hit SL (Full Loss): ${fullLosses}`);
|
| 224 |
+
console.log(` - Net Win Rate (Profit/BE Trades): ${winRate.toFixed(1)}%`);
|
| 225 |
+
console.log(` - Total Cumulative Net P&L: $${totalPnL.toFixed(2)}`);
|
| 226 |
+
console.log(` - Average P&L per completed trade: $${avgPnL.toFixed(2)}\n`);
|
| 227 |
+
|
| 228 |
+
} catch (e) {
|
| 229 |
+
console.error(`Error backtesting ${symbol}:`, e.message);
|
| 230 |
+
}
|
| 231 |
+
}
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
runBacktest();
|
scratch/checkAllTimeframes.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// checkAllTimeframes.js
|
| 2 |
+
const originalFetch = globalThis.fetch;
|
| 3 |
+
globalThis.fetch = async (input, init) => {
|
| 4 |
+
let url = typeof input === 'string' ? input : input.url;
|
| 5 |
+
if (url.startsWith('/api/yahoo')) {
|
| 6 |
+
url = 'https://query1.finance.yahoo.com' + url.replace(/^\/api\/yahoo/, '');
|
| 7 |
+
init = init || {};
|
| 8 |
+
init.headers = {
|
| 9 |
+
...init.headers,
|
| 10 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
| 11 |
+
'Origin': 'https://finance.yahoo.com',
|
| 12 |
+
'Referer': 'https://finance.yahoo.com',
|
| 13 |
+
};
|
| 14 |
+
}
|
| 15 |
+
return originalFetch(url, init);
|
| 16 |
+
};
|
| 17 |
+
|
| 18 |
+
const TIMEFRAMES = {
|
| 19 |
+
'1m': { interval: '1m', range: '1d' },
|
| 20 |
+
'5m': { interval: '5m', range: '5d' },
|
| 21 |
+
'15m': { interval: '15m', range: '15d' },
|
| 22 |
+
'1H': { interval: '60m', range: '30d' },
|
| 23 |
+
'1D': { interval: '1d', range: '1y' }
|
| 24 |
+
};
|
| 25 |
+
|
| 26 |
+
async function test() {
|
| 27 |
+
for (const [tf, config] of Object.entries(TIMEFRAMES)) {
|
| 28 |
+
const url = `https://query1.finance.yahoo.com/v8/finance/chart/USDCAD=X?interval=${config.interval}&range=${config.range}`;
|
| 29 |
+
try {
|
| 30 |
+
const res = await fetch(url);
|
| 31 |
+
const data = await res.json();
|
| 32 |
+
const result = data.chart.result[0];
|
| 33 |
+
if (!result || !result.timestamp) {
|
| 34 |
+
console.log(`Timeframe ${tf}: No data`);
|
| 35 |
+
continue;
|
| 36 |
+
}
|
| 37 |
+
const timestamps = result.timestamp;
|
| 38 |
+
const quotes = result.indicators.quote[0];
|
| 39 |
+
|
| 40 |
+
let maxWick = 0;
|
| 41 |
+
let maxWickTime = null;
|
| 42 |
+
let count = 0;
|
| 43 |
+
|
| 44 |
+
for (let i = 0; i < timestamps.length; i++) {
|
| 45 |
+
const o = quotes.open[i];
|
| 46 |
+
const h = quotes.high[i];
|
| 47 |
+
const l = quotes.low[i];
|
| 48 |
+
const c = quotes.close[i];
|
| 49 |
+
if (o === null || h === null || l === null || c === null) continue;
|
| 50 |
+
|
| 51 |
+
count++;
|
| 52 |
+
const maxBody = Math.max(o, c);
|
| 53 |
+
const minBody = Math.min(o, c);
|
| 54 |
+
const upper = h - maxBody;
|
| 55 |
+
const lower = minBody - l;
|
| 56 |
+
const wick = Math.max(upper, lower);
|
| 57 |
+
|
| 58 |
+
if (wick > maxWick) {
|
| 59 |
+
maxWick = wick;
|
| 60 |
+
maxWickTime = new Date(timestamps[i] * 1000).toLocaleString();
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
console.log(`Timeframe ${tf}: Total candles=${count}, Max Wick=${maxWick.toFixed(5)} (${Math.round(maxWick * 10000)} pips) at ${maxWickTime}`);
|
| 65 |
+
} catch (e) {
|
| 66 |
+
console.log(`Timeframe ${tf}: Failed with error: ${e.message}`);
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
test();
|
scratch/checkForexVolume.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// checkForexVolume.js
|
| 2 |
+
import { fetchCandles } from '../src/data/yahooFinanceAPI.js';
|
| 3 |
+
|
| 4 |
+
// Polyfill fetch exactly like server.js
|
| 5 |
+
const originalFetch = globalThis.fetch;
|
| 6 |
+
globalThis.fetch = async (input, init) => {
|
| 7 |
+
let url = typeof input === 'string' ? input : input.url;
|
| 8 |
+
|
| 9 |
+
if (url.startsWith('/api/swissquote')) {
|
| 10 |
+
url = 'https://forex-data-feed.swissquote.com/public-quotes/bboquotes/instrument/XAU/USD';
|
| 11 |
+
} else if (url.startsWith('/api/yahoo')) {
|
| 12 |
+
url = 'https://query1.finance.yahoo.com' + url.replace(/^\/api\/yahoo/, '');
|
| 13 |
+
init = init || {};
|
| 14 |
+
init.headers = {
|
| 15 |
+
...init.headers,
|
| 16 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
| 17 |
+
'Origin': 'https://finance.yahoo.com',
|
| 18 |
+
'Referer': 'https://finance.yahoo.com',
|
| 19 |
+
};
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
return originalFetch(url, init);
|
| 23 |
+
};
|
| 24 |
+
|
| 25 |
+
async function test() {
|
| 26 |
+
const symbols = ['EURUSD', 'GBPUSD', 'USDCAD', 'XAUUSD', 'BTCUSDT', 'ETHUSDT'];
|
| 27 |
+
for (const sym of symbols) {
|
| 28 |
+
try {
|
| 29 |
+
const candles = await fetchCandles(sym, '15m');
|
| 30 |
+
const sample = candles.slice(-5);
|
| 31 |
+
console.log(`\n=== Symbol: ${sym} ===`);
|
| 32 |
+
console.log(`Total candles loaded: ${candles.length}`);
|
| 33 |
+
sample.forEach(c => {
|
| 34 |
+
console.log(`Candle Time: ${new Date(c.time * 1000).toLocaleString()} | Close: ${c.close} | Volume: ${c.volume}`);
|
| 35 |
+
});
|
| 36 |
+
} catch (e) {
|
| 37 |
+
console.error(`Error for ${sym}:`, e.message);
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
test();
|
scratch/findWicks.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { fetchCandles } from '../src/data/yahooFinanceAPI.js';
|
| 2 |
+
|
| 3 |
+
// Polyfill fetch
|
| 4 |
+
const originalFetch = globalThis.fetch;
|
| 5 |
+
globalThis.fetch = async (input, init) => {
|
| 6 |
+
let url = typeof input === 'string' ? input : input.url;
|
| 7 |
+
if (url.startsWith('/api/yahoo')) {
|
| 8 |
+
url = 'https://query1.finance.yahoo.com' + url.replace(/^\/api\/yahoo/, '');
|
| 9 |
+
init = init || {};
|
| 10 |
+
init.headers = {
|
| 11 |
+
...init.headers,
|
| 12 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
| 13 |
+
'Origin': 'https://finance.yahoo.com',
|
| 14 |
+
'Referer': 'https://finance.yahoo.com',
|
| 15 |
+
};
|
| 16 |
+
}
|
| 17 |
+
return originalFetch(url, init);
|
| 18 |
+
};
|
| 19 |
+
|
| 20 |
+
async function check() {
|
| 21 |
+
const symbol = 'USDCAD';
|
| 22 |
+
console.log(`--- Fetching ${symbol} from Yahoo Finance ---`);
|
| 23 |
+
const url = 'https://query1.finance.yahoo.com/v8/finance/chart/USDCAD=X?interval=15m&range=15d';
|
| 24 |
+
const res = await fetch(url);
|
| 25 |
+
const data = await res.json();
|
| 26 |
+
const result = data.chart.result[0];
|
| 27 |
+
const timestamps = result.timestamp;
|
| 28 |
+
const quotes = result.indicators.quote[0];
|
| 29 |
+
|
| 30 |
+
console.log(`Total timestamps: ${timestamps.length}`);
|
| 31 |
+
|
| 32 |
+
let nonNullCount = 0;
|
| 33 |
+
let minLow = Infinity;
|
| 34 |
+
let maxHigh = -Infinity;
|
| 35 |
+
let maxWick = 0;
|
| 36 |
+
let maxWickIdx = -1;
|
| 37 |
+
|
| 38 |
+
for (let i = 0; i < timestamps.length; i++) {
|
| 39 |
+
const o = quotes.open[i];
|
| 40 |
+
const h = quotes.high[i];
|
| 41 |
+
const l = quotes.low[i];
|
| 42 |
+
const c = quotes.close[i];
|
| 43 |
+
if (o === null || h === null || l === null || c === null) continue;
|
| 44 |
+
|
| 45 |
+
nonNullCount++;
|
| 46 |
+
if (l < minLow) minLow = l;
|
| 47 |
+
if (h > maxHigh) maxHigh = h;
|
| 48 |
+
|
| 49 |
+
const maxBody = Math.max(o, c);
|
| 50 |
+
const minBody = Math.min(o, c);
|
| 51 |
+
const upperWick = h - maxBody;
|
| 52 |
+
const lowerWick = minBody - l;
|
| 53 |
+
const wick = Math.max(upperWick, lowerWick);
|
| 54 |
+
|
| 55 |
+
if (wick > maxWick) {
|
| 56 |
+
maxWick = wick;
|
| 57 |
+
maxWickIdx = i;
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
console.log(`Non-null quotes: ${nonNullCount}`);
|
| 62 |
+
console.log(`Absolute Min Low: ${minLow}`);
|
| 63 |
+
console.log(`Absolute Max High: ${maxHigh}`);
|
| 64 |
+
console.log(`Max Wick size: ${maxWick} at index ${maxWickIdx}`);
|
| 65 |
+
if (maxWickIdx !== -1) {
|
| 66 |
+
console.log('Candle with max wick:', {
|
| 67 |
+
time: new Date(timestamps[maxWickIdx] * 1000).toLocaleString(),
|
| 68 |
+
open: quotes.open[maxWickIdx],
|
| 69 |
+
high: quotes.high[maxWickIdx],
|
| 70 |
+
low: quotes.low[maxWickIdx],
|
| 71 |
+
close: quotes.close[maxWickIdx]
|
| 72 |
+
});
|
| 73 |
+
}
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
check();
|
scratch/highFidelityBacktest.js
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { fetchCandles } from '../src/data/yahooFinanceAPI.js';
|
| 2 |
+
import { generateSignals } from '../src/analysis/signalGenerator.js';
|
| 3 |
+
|
| 4 |
+
// Polyfill fetch exactly like server.js
|
| 5 |
+
const originalFetch = globalThis.fetch;
|
| 6 |
+
globalThis.fetch = async (input, init) => {
|
| 7 |
+
let url = typeof input === 'string' ? input : input.url;
|
| 8 |
+
if (url.startsWith('/api/swissquote')) {
|
| 9 |
+
url = 'https://forex-data-feed.swissquote.com/public-quotes/bboquotes/instrument/XAU/USD';
|
| 10 |
+
} else if (url.startsWith('/api/yahoo')) {
|
| 11 |
+
url = 'https://query1.finance.yahoo.com' + url.replace(/^\/api\/yahoo/, '');
|
| 12 |
+
init = init || {};
|
| 13 |
+
init.headers = {
|
| 14 |
+
...init.headers,
|
| 15 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
| 16 |
+
'Origin': 'https://finance.yahoo.com',
|
| 17 |
+
'Referer': 'https://finance.yahoo.com',
|
| 18 |
+
};
|
| 19 |
+
}
|
| 20 |
+
return originalFetch(url, init);
|
| 21 |
+
};
|
| 22 |
+
|
| 23 |
+
function getSpread(symbol) {
|
| 24 |
+
switch (symbol) {
|
| 25 |
+
case 'BTCUSDT': return 25.0;
|
| 26 |
+
case 'ETHUSDT': return 5.0;
|
| 27 |
+
case 'XAUUSD': return 0.7;
|
| 28 |
+
case 'EURUSD':
|
| 29 |
+
case 'GBPUSD':
|
| 30 |
+
case 'USDCAD':
|
| 31 |
+
return 0.00007;
|
| 32 |
+
default:
|
| 33 |
+
return 0;
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
function calculatePnL(symbol, type, entry, exit, lots) {
|
| 38 |
+
const direction = type === 'LONG' ? 1 : -1;
|
| 39 |
+
const diff = (exit - entry) * direction;
|
| 40 |
+
if (symbol === 'BTCUSDT' || symbol === 'ETHUSDT') {
|
| 41 |
+
return diff * lots;
|
| 42 |
+
} else if (symbol === 'XAUUSD') {
|
| 43 |
+
return diff * lots * 100;
|
| 44 |
+
} else if (symbol === 'EURUSD' || symbol === 'GBPUSD' || symbol === 'USDCAD') {
|
| 45 |
+
const rate = symbol === 'USDCAD' ? 1.38 : 1.0;
|
| 46 |
+
const pipScale = symbol === 'USDCAD' ? (10 / rate) : 10;
|
| 47 |
+
return (diff / 0.0001) * lots * pipScale;
|
| 48 |
+
}
|
| 49 |
+
return 0;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
function calculateCommission(symbol, entryPrice, lots) {
|
| 53 |
+
if (symbol === 'BTCUSDT' || symbol === 'ETHUSDT') {
|
| 54 |
+
return 0.0008 * entryPrice * lots;
|
| 55 |
+
} else {
|
| 56 |
+
return 5.0 * lots;
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
/**
|
| 61 |
+
* Runs high-fidelity exit check using lower timeframe candles.
|
| 62 |
+
*/
|
| 63 |
+
function checkExitHighFidelity(symbol, signal, lowerCandles, startLtIndex) {
|
| 64 |
+
let partialClosed = false;
|
| 65 |
+
let sl = signal.sl;
|
| 66 |
+
let entry = signal.entry;
|
| 67 |
+
let lots = signal.lotSize;
|
| 68 |
+
let commission = calculateCommission(symbol, entry, lots);
|
| 69 |
+
const spread = getSpread(symbol);
|
| 70 |
+
|
| 71 |
+
for (let c = startLtIndex; c < lowerCandles.length; c++) {
|
| 72 |
+
const candle = lowerCandles[c];
|
| 73 |
+
|
| 74 |
+
if (signal.type === 'LONG') {
|
| 75 |
+
// Check SL hit
|
| 76 |
+
if (candle.low <= sl) {
|
| 77 |
+
if (!partialClosed) {
|
| 78 |
+
const pnl = calculatePnL(symbol, 'LONG', entry, sl, lots) - commission;
|
| 79 |
+
return { status: 'LOSS', pnl, exitTime: candle.time, exitPrice: sl };
|
| 80 |
+
} else {
|
| 81 |
+
const tp1PnL = calculatePnL(symbol, 'LONG', entry, signal.tp1, lots * 0.7);
|
| 82 |
+
const pnl = tp1PnL - commission;
|
| 83 |
+
return { status: 'PARTIAL_STOP', pnl, exitTime: candle.time, exitPrice: sl };
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
// Check TP1 hit
|
| 88 |
+
if (!partialClosed && candle.high >= signal.tp1) {
|
| 89 |
+
partialClosed = true;
|
| 90 |
+
sl = entry;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
// Check TP2 hit
|
| 94 |
+
if (partialClosed && candle.high >= signal.tp2) {
|
| 95 |
+
const tp1PnL = calculatePnL(symbol, 'LONG', entry, signal.tp1, lots * 0.7);
|
| 96 |
+
const tp2PnL = calculatePnL(symbol, 'LONG', entry, signal.tp2, lots * 0.3);
|
| 97 |
+
const pnl = tp1PnL + tp2PnL - commission;
|
| 98 |
+
return { status: 'WIN', pnl, exitTime: candle.time, exitPrice: signal.tp2 };
|
| 99 |
+
}
|
| 100 |
+
} else {
|
| 101 |
+
// SHORT
|
| 102 |
+
if (candle.high + spread >= sl) {
|
| 103 |
+
if (!partialClosed) {
|
| 104 |
+
const pnl = calculatePnL(symbol, 'SHORT', entry, sl, lots) - commission;
|
| 105 |
+
return { status: 'LOSS', pnl, exitTime: candle.time, exitPrice: sl };
|
| 106 |
+
} else {
|
| 107 |
+
const tp1PnL = calculatePnL(symbol, 'SHORT', entry, signal.tp1, lots * 0.7);
|
| 108 |
+
const pnl = tp1PnL - commission;
|
| 109 |
+
return { status: 'PARTIAL_STOP', pnl, exitTime: candle.time, exitPrice: sl };
|
| 110 |
+
}
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
if (!partialClosed && candle.low + spread <= signal.tp1) {
|
| 114 |
+
partialClosed = true;
|
| 115 |
+
sl = entry;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
if (partialClosed && candle.low + spread <= signal.tp2) {
|
| 119 |
+
const tp1PnL = calculatePnL(symbol, 'SHORT', entry, signal.tp1, lots * 0.7);
|
| 120 |
+
const tp2PnL = calculatePnL(symbol, 'SHORT', entry, signal.tp2, lots * 0.3);
|
| 121 |
+
const pnl = tp1PnL + tp2PnL - commission;
|
| 122 |
+
return { status: 'WIN', pnl, exitTime: candle.time, exitPrice: signal.tp2 };
|
| 123 |
+
}
|
| 124 |
+
}
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
// Still open
|
| 128 |
+
const lastCandle = lowerCandles[lowerCandles.length - 1];
|
| 129 |
+
const valuation = lastCandle.close;
|
| 130 |
+
if (!partialClosed) {
|
| 131 |
+
const valuationPrice = signal.type === 'SHORT' ? valuation + spread : valuation;
|
| 132 |
+
const pnl = calculatePnL(symbol, signal.type, entry, valuationPrice, lots) - commission;
|
| 133 |
+
return { status: 'OPEN', pnl, exitTime: lastCandle.time, exitPrice: valuationPrice };
|
| 134 |
+
} else {
|
| 135 |
+
const valuationPrice = signal.type === 'SHORT' ? valuation + spread : valuation;
|
| 136 |
+
const tp1PnL = calculatePnL(symbol, signal.type, entry, signal.tp1, lots * 0.7);
|
| 137 |
+
const tp2PnL = calculatePnL(symbol, signal.type, entry, valuationPrice, lots * 0.3);
|
| 138 |
+
const pnl = tp1PnL + tp2PnL - commission;
|
| 139 |
+
return { status: 'OPEN_PARTIAL', pnl, exitTime: lastCandle.time, exitPrice: valuationPrice };
|
| 140 |
+
}
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
const SYMBOLS = ['BTCUSDT', 'ETHUSDT', 'XAUUSD', 'GBPUSD', 'EURUSD', 'USDCAD'];
|
| 144 |
+
|
| 145 |
+
async function runHighFidelityBacktest(signalTimeframe) {
|
| 146 |
+
console.log(`\n=== RUNNING HIGH-FIDELITY BACKTEST (Signals: ${signalTimeframe}, Exits checked on: 5m candles) ===`);
|
| 147 |
+
|
| 148 |
+
// Fetch lower timeframe candles for exit checking (range 30d for 5m is supported by Yahoo)
|
| 149 |
+
// Let's modify TIMEFRAME_CONFIG dynamically inside yahooFinanceAPI to make sure GC=F and others return 30d of 5m
|
| 150 |
+
// We can just query directly or fetch it.
|
| 151 |
+
|
| 152 |
+
for (const symbol of SYMBOLS) {
|
| 153 |
+
try {
|
| 154 |
+
const sigCandles = await fetchCandles(symbol, signalTimeframe);
|
| 155 |
+
// Fetch 5m candles to track entries/exits at fine resolution
|
| 156 |
+
// We temporarily fetch with '5m' timeframe. Let's make sure it returns enough candles.
|
| 157 |
+
const ltCandles = await fetchCandles(symbol, '5m');
|
| 158 |
+
|
| 159 |
+
if (!sigCandles || sigCandles.length < 100 || !ltCandles || ltCandles.length < 100) {
|
| 160 |
+
console.log(`[${symbol}] Insufficient candles. Signal Count: ${sigCandles?.length}, 5m Count: ${ltCandles?.length}`);
|
| 161 |
+
continue;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
let totalTrades = 0;
|
| 165 |
+
let fullWins = 0;
|
| 166 |
+
let partialWins = 0;
|
| 167 |
+
let fullLosses = 0;
|
| 168 |
+
let openTrades = 0;
|
| 169 |
+
let totalPnL = 0;
|
| 170 |
+
|
| 171 |
+
let lastClosedTime = 0;
|
| 172 |
+
let activeTradeEndIndex = -1; // index in sigCandles
|
| 173 |
+
let activeTradeEndLtTime = 0;
|
| 174 |
+
|
| 175 |
+
for (let i = 50; i < sigCandles.length; i++) {
|
| 176 |
+
// Skip scanning if we are currently inside an active trade (using time constraint)
|
| 177 |
+
if (sigCandles[i].time <= activeTradeEndLtTime) {
|
| 178 |
+
continue;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
// Cooldown check (10 minutes)
|
| 182 |
+
if (lastClosedTime > 0 && (sigCandles[i].time - lastClosedTime) < 600) {
|
| 183 |
+
continue;
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
const window = sigCandles.slice(0, i);
|
| 187 |
+
const signals = generateSignals(window, symbol, 0);
|
| 188 |
+
const validSignals = signals.filter(sig => ['A', 'B'].includes(sig.quality));
|
| 189 |
+
if (validSignals.length === 0) continue;
|
| 190 |
+
|
| 191 |
+
const signal = validSignals[0];
|
| 192 |
+
|
| 193 |
+
// Find corresponding index in the lower-timeframe 5m candles
|
| 194 |
+
// Entry is at the close of candle i-1, which is the start of candle i
|
| 195 |
+
const entryTime = sigCandles[i].time;
|
| 196 |
+
let ltIndex = ltCandles.findIndex(c => c.time >= entryTime);
|
| 197 |
+
if (ltIndex === -1) continue;
|
| 198 |
+
|
| 199 |
+
const result = checkExitHighFidelity(symbol, signal, ltCandles, ltIndex);
|
| 200 |
+
|
| 201 |
+
totalTrades++;
|
| 202 |
+
totalPnL += result.pnl;
|
| 203 |
+
|
| 204 |
+
if (result.status === 'WIN') fullWins++;
|
| 205 |
+
else if (result.status === 'PARTIAL_STOP') partialWins++;
|
| 206 |
+
else if (result.status === 'LOSS') fullLosses++;
|
| 207 |
+
else openTrades++;
|
| 208 |
+
|
| 209 |
+
activeTradeEndLtTime = result.exitTime;
|
| 210 |
+
lastClosedTime = result.exitTime;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
const completedTrades = fullWins + partialWins + fullLosses;
|
| 214 |
+
const winRate = completedTrades > 0 ? ((fullWins + partialWins) / completedTrades) * 100 : 0;
|
| 215 |
+
const fullWinRate = completedTrades > 0 ? (fullWins / completedTrades) * 100 : 0;
|
| 216 |
+
const avgPnL = completedTrades > 0 ? totalPnL / completedTrades : 0;
|
| 217 |
+
|
| 218 |
+
console.log(`[${symbol}] Results over ~10-15 days of overlap:`);
|
| 219 |
+
console.log(` Total Scanned Signals Executed: ${totalTrades}`);
|
| 220 |
+
console.log(` Completed Outcomes: ${completedTrades} (Open: ${openTrades})`);
|
| 221 |
+
console.log(` - Hit TP2 (Full Win): ${fullWins} (${fullWinRate.toFixed(1)}%)`);
|
| 222 |
+
console.log(` - Hit TP1, then BE Stop (Partial Profit): ${partialWins}`);
|
| 223 |
+
console.log(` - Hit SL (Full Loss): ${fullLosses}`);
|
| 224 |
+
console.log(` - Net Win Rate (Profit/BE Trades): ${winRate.toFixed(1)}%`);
|
| 225 |
+
console.log(` - Total Cumulative Net P&L: $${totalPnL.toFixed(2)}`);
|
| 226 |
+
console.log(` - Average P&L per completed trade: $${avgPnL.toFixed(2)}\n`);
|
| 227 |
+
|
| 228 |
+
} catch (e) {
|
| 229 |
+
console.error(`Error backtesting ${symbol}:`, e.message);
|
| 230 |
+
}
|
| 231 |
+
}
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
async function run() {
|
| 235 |
+
// Let's modify TIMEFRAME_CONFIG range for 5m to 30d so we have enough candles to overlap with 1H's 60d
|
| 236 |
+
// We will patch the config dynamically
|
| 237 |
+
try {
|
| 238 |
+
const { TIMEFRAME_CONFIG } = await import('../src/data/yahooFinanceAPI.js');
|
| 239 |
+
if (TIMEFRAME_CONFIG && TIMEFRAME_CONFIG['5m']) {
|
| 240 |
+
TIMEFRAME_CONFIG['5m'].range = '30d';
|
| 241 |
+
console.log('[Info] Dynamically expanded 5m Yahoo range to 30d.');
|
| 242 |
+
}
|
| 243 |
+
} catch (err) {
|
| 244 |
+
// Ignore
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
await runHighFidelityBacktest('15m');
|
| 248 |
+
await runHighFidelityBacktest('1H');
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
run();
|
scratch/optimizeParams.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { fetchCandles } from '../src/data/yahooFinanceAPI.js';
|
| 2 |
+
import { generateSignals, ATR_MULTIPLIERS, ATR_CONFIG } from '../src/analysis/signalGenerator.js';
|
| 3 |
+
|
| 4 |
+
// Polyfill fetch exactly like server.js
|
| 5 |
+
const originalFetch = globalThis.fetch;
|
| 6 |
+
globalThis.fetch = async (input, init) => {
|
| 7 |
+
let url = typeof input === 'string' ? input : input.url;
|
| 8 |
+
if (url.startsWith('/api/swissquote')) {
|
| 9 |
+
url = 'https://forex-data-feed.swissquote.com/public-quotes/bboquotes/instrument/XAU/USD';
|
| 10 |
+
} else if (url.startsWith('/api/yahoo')) {
|
| 11 |
+
url = 'https://query1.finance.yahoo.com' + url.replace(/^\/api\/yahoo/, '');
|
| 12 |
+
init = init || {};
|
| 13 |
+
init.headers = {
|
| 14 |
+
...init.headers,
|
| 15 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
| 16 |
+
'Origin': 'https://finance.yahoo.com',
|
| 17 |
+
'Referer': 'https://finance.yahoo.com',
|
| 18 |
+
};
|
| 19 |
+
}
|
| 20 |
+
return originalFetch(url, init);
|
| 21 |
+
};
|
| 22 |
+
|
| 23 |
+
function getSpread(symbol) {
|
| 24 |
+
switch (symbol) {
|
| 25 |
+
case 'BTCUSDT': return 25.0;
|
| 26 |
+
case 'ETHUSDT': return 5.0;
|
| 27 |
+
case 'XAUUSD': return 0.7;
|
| 28 |
+
case 'EURUSD':
|
| 29 |
+
case 'GBPUSD':
|
| 30 |
+
case 'USDCAD':
|
| 31 |
+
return 0.00007;
|
| 32 |
+
default:
|
| 33 |
+
return 0;
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
function calculatePnL(symbol, type, entry, exit, lots) {
|
| 38 |
+
const direction = type === 'LONG' ? 1 : -1;
|
| 39 |
+
const diff = (exit - entry) * direction;
|
| 40 |
+
if (symbol === 'BTCUSDT' || symbol === 'ETHUSDT') {
|
| 41 |
+
return diff * lots;
|
| 42 |
+
} else if (symbol === 'XAUUSD') {
|
| 43 |
+
return diff * lots * 100;
|
| 44 |
+
} else if (symbol === 'EURUSD' || symbol === 'GBPUSD' || symbol === 'USDCAD') {
|
| 45 |
+
const rate = symbol === 'USDCAD' ? 1.38 : 1.0;
|
| 46 |
+
const pipScale = symbol === 'USDCAD' ? (10 / rate) : 10;
|
| 47 |
+
return (diff / 0.0001) * lots * pipScale;
|
| 48 |
+
}
|
| 49 |
+
return 0;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
function calculateCommission(symbol, entryPrice, lots) {
|
| 53 |
+
if (symbol === 'BTCUSDT' || symbol === 'ETHUSDT') {
|
| 54 |
+
return 0.0008 * entryPrice * lots;
|
| 55 |
+
} else {
|
| 56 |
+
return 5.0 * lots;
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
function backtestTrade(symbol, signal, candles, startIndex) {
|
| 61 |
+
let partialClosed = false;
|
| 62 |
+
let sl = signal.sl;
|
| 63 |
+
let entry = signal.entry;
|
| 64 |
+
let lots = signal.lotSize;
|
| 65 |
+
let commission = calculateCommission(symbol, entry, lots);
|
| 66 |
+
const spread = getSpread(symbol);
|
| 67 |
+
|
| 68 |
+
for (let c = startIndex; c < candles.length; c++) {
|
| 69 |
+
const candle = candles[c];
|
| 70 |
+
if (signal.type === 'LONG') {
|
| 71 |
+
if (candle.low <= sl) {
|
| 72 |
+
if (!partialClosed) {
|
| 73 |
+
const pnl = calculatePnL(symbol, 'LONG', entry, sl, lots) - commission;
|
| 74 |
+
return { status: 'LOSS', pnl, exitTime: candle.time };
|
| 75 |
+
} else {
|
| 76 |
+
const tp1PnL = calculatePnL(symbol, 'LONG', entry, signal.tp1, lots * 0.7);
|
| 77 |
+
const pnl = tp1PnL - commission;
|
| 78 |
+
return { status: 'PARTIAL_STOP', pnl, exitTime: candle.time };
|
| 79 |
+
}
|
| 80 |
+
}
|
| 81 |
+
if (!partialClosed && candle.high >= signal.tp1) {
|
| 82 |
+
partialClosed = true;
|
| 83 |
+
sl = entry;
|
| 84 |
+
}
|
| 85 |
+
if (partialClosed && candle.high >= signal.tp2) {
|
| 86 |
+
const tp1PnL = calculatePnL(symbol, 'LONG', entry, signal.tp1, lots * 0.7);
|
| 87 |
+
const tp2PnL = calculatePnL(symbol, 'LONG', entry, signal.tp2, lots * 0.3);
|
| 88 |
+
const pnl = tp1PnL + tp2PnL - commission;
|
| 89 |
+
return { status: 'WIN', pnl, exitTime: candle.time };
|
| 90 |
+
}
|
| 91 |
+
} else {
|
| 92 |
+
if (candle.high + spread >= sl) {
|
| 93 |
+
if (!partialClosed) {
|
| 94 |
+
const pnl = calculatePnL(symbol, 'SHORT', entry, sl, lots) - commission;
|
| 95 |
+
return { status: 'LOSS', pnl, exitTime: candle.time };
|
| 96 |
+
} else {
|
| 97 |
+
const tp1PnL = calculatePnL(symbol, 'SHORT', entry, signal.tp1, lots * 0.7);
|
| 98 |
+
const pnl = tp1PnL - commission;
|
| 99 |
+
return { status: 'PARTIAL_STOP', pnl, exitTime: candle.time };
|
| 100 |
+
}
|
| 101 |
+
}
|
| 102 |
+
if (!partialClosed && candle.low + spread <= signal.tp1) {
|
| 103 |
+
partialClosed = true;
|
| 104 |
+
sl = entry;
|
| 105 |
+
}
|
| 106 |
+
if (partialClosed && candle.low + spread <= signal.tp2) {
|
| 107 |
+
const tp1PnL = calculatePnL(symbol, 'SHORT', entry, signal.tp1, lots * 0.7);
|
| 108 |
+
const tp2PnL = calculatePnL(symbol, 'SHORT', entry, signal.tp2, lots * 0.3);
|
| 109 |
+
const pnl = tp1PnL + tp2PnL - commission;
|
| 110 |
+
return { status: 'WIN', pnl, exitTime: candle.time };
|
| 111 |
+
}
|
| 112 |
+
}
|
| 113 |
+
}
|
| 114 |
+
const lastCandle = candles[candles.length - 1];
|
| 115 |
+
const valuation = lastCandle.close;
|
| 116 |
+
if (!partialClosed) {
|
| 117 |
+
const valuationPrice = signal.type === 'SHORT' ? valuation + spread : valuation;
|
| 118 |
+
const pnl = calculatePnL(symbol, signal.type, entry, valuationPrice, lots) - commission;
|
| 119 |
+
return { status: 'OPEN', pnl, exitTime: lastCandle.time };
|
| 120 |
+
} else {
|
| 121 |
+
const valuationPrice = signal.type === 'SHORT' ? valuation + spread : valuation;
|
| 122 |
+
const tp1PnL = calculatePnL(symbol, signal.type, entry, signal.tp1, lots * 0.7);
|
| 123 |
+
const tp2PnL = calculatePnL(symbol, signal.type, entry, valuationPrice, lots * 0.3);
|
| 124 |
+
const pnl = tp1PnL + tp2PnL - commission;
|
| 125 |
+
return { status: 'OPEN_PARTIAL', pnl, exitTime: lastCandle.time };
|
| 126 |
+
}
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
const SYMBOLS = ['BTCUSDT', 'ETHUSDT', 'XAUUSD', 'GBPUSD', 'EURUSD', 'USDCAD'];
|
| 130 |
+
const ATR_MULTIPLIERS_TO_TEST = [0.5, 1.0, 1.5, 2.0, 2.5];
|
| 131 |
+
|
| 132 |
+
async function runOptimization() {
|
| 133 |
+
ATR_CONFIG.useDynamic = false;
|
| 134 |
+
const timeframes = ['15m', '1H'];
|
| 135 |
+
const data = {};
|
| 136 |
+
|
| 137 |
+
// Pre-load candles
|
| 138 |
+
for (const tf of timeframes) {
|
| 139 |
+
data[tf] = {};
|
| 140 |
+
for (const sym of SYMBOLS) {
|
| 141 |
+
data[tf][sym] = await fetchCandles(sym, tf);
|
| 142 |
+
}
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
for (const tf of timeframes) {
|
| 146 |
+
console.log(`\n======================= TIMEFRAME: ${tf} =======================`);
|
| 147 |
+
for (const symbol of SYMBOLS) {
|
| 148 |
+
console.log(`\n--- Symbol: ${symbol} ---`);
|
| 149 |
+
const candles = data[tf][symbol];
|
| 150 |
+
if (!candles || candles.length < 100) continue;
|
| 151 |
+
|
| 152 |
+
for (const multiplier of ATR_MULTIPLIERS_TO_TEST) {
|
| 153 |
+
// Mutate the global config object
|
| 154 |
+
ATR_MULTIPLIERS[symbol] = multiplier;
|
| 155 |
+
|
| 156 |
+
let totalPnL = 0;
|
| 157 |
+
let totalTrades = 0;
|
| 158 |
+
let wins = 0;
|
| 159 |
+
let activeTradeEndIndex = -1;
|
| 160 |
+
let lastClosedTime = 0;
|
| 161 |
+
|
| 162 |
+
for (let i = 50; i < candles.length; i++) {
|
| 163 |
+
if (i <= activeTradeEndIndex) continue;
|
| 164 |
+
if (lastClosedTime > 0 && (candles[i].time - lastClosedTime) < (tf === '15m' ? 600 : 3600)) {
|
| 165 |
+
continue;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
const window = candles.slice(0, i);
|
| 169 |
+
const signals = generateSignals(window, symbol, 0);
|
| 170 |
+
const validSignals = signals.filter(sig => ['A', 'B'].includes(sig.quality));
|
| 171 |
+
if (validSignals.length === 0) continue;
|
| 172 |
+
|
| 173 |
+
const signal = validSignals[0];
|
| 174 |
+
const result = backtestTrade(symbol, signal, candles, i);
|
| 175 |
+
|
| 176 |
+
totalTrades++;
|
| 177 |
+
totalPnL += result.pnl;
|
| 178 |
+
if (result.status === 'WIN' || result.status === 'PARTIAL_STOP') {
|
| 179 |
+
wins++;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
let exitIndex = candles.findIndex(c => c.time === result.exitTime);
|
| 183 |
+
if (exitIndex === -1) exitIndex = candles.length - 1;
|
| 184 |
+
activeTradeEndIndex = exitIndex;
|
| 185 |
+
lastClosedTime = result.exitTime;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
const winRate = totalTrades > 0 ? (wins / totalTrades) * 100 : 0;
|
| 189 |
+
console.log(`ATR Mult: ${multiplier.toFixed(1)} | Trades: ${totalTrades} | WinRate: ${winRate.toFixed(1)}% | Net P&L: $${totalPnL.toFixed(2)}`);
|
| 190 |
+
}
|
| 191 |
+
}
|
| 192 |
+
}
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
runOptimization();
|
scratch/simulateSignals.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// simulateSignals.js
|
| 2 |
+
import { fetchCandles } from '../src/data/yahooFinanceAPI.js';
|
| 3 |
+
import { generateSignals } from '../src/analysis/signalGenerator.js';
|
| 4 |
+
|
| 5 |
+
// Polyfill fetch exactly like server.js
|
| 6 |
+
const originalFetch = globalThis.fetch;
|
| 7 |
+
globalThis.fetch = async (input, init) => {
|
| 8 |
+
let url = typeof input === 'string' ? input : input.url;
|
| 9 |
+
|
| 10 |
+
if (url.startsWith('/api/swissquote')) {
|
| 11 |
+
url = 'https://forex-data-feed.swissquote.com/public-quotes/bboquotes/instrument/XAU/USD';
|
| 12 |
+
} else if (url.startsWith('/api/yahoo')) {
|
| 13 |
+
url = 'https://query1.finance.yahoo.com' + url.replace(/^\/api\/yahoo/, '');
|
| 14 |
+
init = init || {};
|
| 15 |
+
init.headers = {
|
| 16 |
+
...init.headers,
|
| 17 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
| 18 |
+
'Origin': 'https://finance.yahoo.com',
|
| 19 |
+
'Referer': 'https://finance.yahoo.com',
|
| 20 |
+
};
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
return originalFetch(url, init);
|
| 24 |
+
};
|
| 25 |
+
|
| 26 |
+
const SYMBOLS = ['BTCUSDT', 'ETHUSDT', 'XAUUSD', 'GBPUSD', 'EURUSD', 'USDCAD'];
|
| 27 |
+
|
| 28 |
+
async function simulate() {
|
| 29 |
+
console.log('=== Simulating Historical Signal Scans (Last 500 candles) ===\n');
|
| 30 |
+
|
| 31 |
+
for (const symbol of SYMBOLS) {
|
| 32 |
+
try {
|
| 33 |
+
const candles = await fetchCandles(symbol, '15m');
|
| 34 |
+
console.log(`[${symbol}] Loaded ${candles.length} historical candles.`);
|
| 35 |
+
|
| 36 |
+
// Let's run a backtest on the history. We'll feed a sliding window of candles to generateSignals
|
| 37 |
+
let gradeACount = 0;
|
| 38 |
+
let gradeBCount = 0;
|
| 39 |
+
|
| 40 |
+
// Minimum history size needed is 50
|
| 41 |
+
for (let i = 50; i <= candles.length; i++) {
|
| 42 |
+
const window = candles.slice(0, i);
|
| 43 |
+
const signals = generateSignals(window, symbol, 0);
|
| 44 |
+
|
| 45 |
+
for (const sig of signals) {
|
| 46 |
+
// If the signal is on the latest candle in the window
|
| 47 |
+
if (sig.time === window[window.length - 1].time) {
|
| 48 |
+
if (sig.quality === 'A') {
|
| 49 |
+
gradeACount++;
|
| 50 |
+
console.log(` 🎯 [Grade A] ${symbol} ${sig.type} at ${new Date(sig.time * 1000).toLocaleString()} (Score: ${sig.score})`);
|
| 51 |
+
} else if (sig.quality === 'B') {
|
| 52 |
+
gradeBCount++;
|
| 53 |
+
}
|
| 54 |
+
}
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
console.log(` -> Total Grade A (Score >= 5): ${gradeACount}`);
|
| 59 |
+
console.log(` -> Total Grade B (Score == 4): ${gradeBCount}\n`);
|
| 60 |
+
} catch (err) {
|
| 61 |
+
console.error(`Error simulating for ${symbol}:`, err.message);
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
simulate();
|
scratch/testMarketDataEndpoint.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// testMarketDataEndpoint.js
|
| 2 |
+
import { fetchCandles } from '../src/data/yahooFinanceAPI.js';
|
| 3 |
+
import { filterForexOutlierWicks } from '../src/data/yahooFinanceAPI.js';
|
| 4 |
+
|
| 5 |
+
// Polyfill fetch
|
| 6 |
+
const originalFetch = globalThis.fetch;
|
| 7 |
+
globalThis.fetch = async (input, init) => {
|
| 8 |
+
let url = typeof input === 'string' ? input : input.url;
|
| 9 |
+
if (url.startsWith('/api/yahoo')) {
|
| 10 |
+
url = 'https://query1.finance.yahoo.com' + url.replace(/^\/api\/yahoo/, '');
|
| 11 |
+
init = init || {};
|
| 12 |
+
init.headers = {
|
| 13 |
+
...init.headers,
|
| 14 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
| 15 |
+
'Origin': 'https://finance.yahoo.com',
|
| 16 |
+
'Referer': 'https://finance.yahoo.com',
|
| 17 |
+
};
|
| 18 |
+
}
|
| 19 |
+
return originalFetch(url, init);
|
| 20 |
+
};
|
| 21 |
+
|
| 22 |
+
const SYMBOLS = ['BTCUSDT', 'ETHUSDT', 'XAUUSD', 'GBPUSD', 'EURUSD', 'USDCAD'];
|
| 23 |
+
|
| 24 |
+
async function test() {
|
| 25 |
+
console.log('--- Testing /api/market-data filter logic for all symbols ---');
|
| 26 |
+
for (const symbol of SYMBOLS) {
|
| 27 |
+
try {
|
| 28 |
+
const rawCandles = await fetchCandles(symbol, '15m');
|
| 29 |
+
console.log(`[${symbol}] Loaded ${rawCandles.length} raw candles.`);
|
| 30 |
+
|
| 31 |
+
// Let's run it through the filterForexOutlierWicks function
|
| 32 |
+
const filtered = filterForexOutlierWicks(rawCandles, symbol);
|
| 33 |
+
console.log(`[${symbol}] Filtered successfully. Count: ${filtered.length}`);
|
| 34 |
+
} catch (err) {
|
| 35 |
+
console.error(`[${symbol}] FAILED with error:`, err);
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
test();
|
scratch/testTelegram.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// scratch/testTelegram.js
|
| 2 |
+
import dotenv from 'dotenv';
|
| 3 |
+
dotenv.config();
|
| 4 |
+
|
| 5 |
+
const token = process.env.TELEGRAM_BOT_TOKEN;
|
| 6 |
+
const chatId = process.env.TELEGRAM_CHAT_ID;
|
| 7 |
+
|
| 8 |
+
console.log('=== Telegram Bot Diagnostics ===');
|
| 9 |
+
console.log('Loaded bot token:', token ? `${token.substring(0, 10)}... (configured)` : 'MISSING');
|
| 10 |
+
console.log('Loaded chat ID:', chatId ? `${chatId}... (configured)` : 'MISSING');
|
| 11 |
+
|
| 12 |
+
if (!token || !chatId) {
|
| 13 |
+
console.error('\nCRITICAL: TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID must be set in environment variables or your HF secrets.');
|
| 14 |
+
process.exit(1);
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
async function sendTestMessage() {
|
| 18 |
+
const url = `https://api.telegram.org/bot${token}/sendMessage`;
|
| 19 |
+
const msg = `🔔 <b>Trade Analyzer Connection Test</b>\n` +
|
| 20 |
+
`Status: 🟢 Connected successfully!\n` +
|
| 21 |
+
`Time: <code>${new Date().toLocaleString()}</code>\n` +
|
| 22 |
+
`This confirms your Telegram configuration is working.`;
|
| 23 |
+
|
| 24 |
+
console.log('\nSending test message to Telegram...');
|
| 25 |
+
try {
|
| 26 |
+
const res = await fetch(url, {
|
| 27 |
+
method: 'POST',
|
| 28 |
+
headers: {
|
| 29 |
+
'Content-Type': 'application/json',
|
| 30 |
+
},
|
| 31 |
+
body: JSON.stringify({
|
| 32 |
+
chat_id: chatId,
|
| 33 |
+
text: msg,
|
| 34 |
+
parse_mode: 'HTML',
|
| 35 |
+
}),
|
| 36 |
+
});
|
| 37 |
+
|
| 38 |
+
const data = await res.json();
|
| 39 |
+
if (res.ok && data.ok) {
|
| 40 |
+
console.log('✅ Success! Message sent. Check your Telegram chat/channel.');
|
| 41 |
+
} else {
|
| 42 |
+
console.error('❌ Telegram API Error:', data.description || 'Unknown error');
|
| 43 |
+
}
|
| 44 |
+
} catch (err) {
|
| 45 |
+
console.error('❌ Network/Connection Error:', err.message);
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
sendTestMessage();
|
scratch/testUSDCAD.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import WebSocket from 'ws';
|
| 2 |
+
globalThis.WebSocket = WebSocket;
|
| 3 |
+
|
| 4 |
+
// Polyfill fetch exactly like server.js
|
| 5 |
+
const originalFetch = globalThis.fetch;
|
| 6 |
+
globalThis.fetch = async (input, init) => {
|
| 7 |
+
let url = typeof input === 'string' ? input : input.url;
|
| 8 |
+
|
| 9 |
+
if (url.startsWith('/api/swissquote')) {
|
| 10 |
+
url = 'https://forex-data-feed.swissquote.com/public-quotes/bboquotes/instrument/XAU/USD';
|
| 11 |
+
} else if (url.startsWith('/api/yahoo')) {
|
| 12 |
+
url = 'https://query1.finance.yahoo.com' + url.replace(/^\/api\/yahoo/, '');
|
| 13 |
+
init = init || {};
|
| 14 |
+
init.headers = {
|
| 15 |
+
...init.headers,
|
| 16 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
| 17 |
+
'Origin': 'https://finance.yahoo.com',
|
| 18 |
+
'Referer': 'https://finance.yahoo.com',
|
| 19 |
+
};
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
return originalFetch(url, init);
|
| 23 |
+
};
|
| 24 |
+
|
| 25 |
+
// Import our API
|
| 26 |
+
import { fetchCandles } from '../src/data/yahooFinanceAPI.js';
|
| 27 |
+
|
| 28 |
+
async function test() {
|
| 29 |
+
console.log('--- Printing Last 10 USDCAD Candles ---');
|
| 30 |
+
try {
|
| 31 |
+
const candles = await fetchCandles('USDCAD', '15m');
|
| 32 |
+
console.log(`Fetched ${candles.length} candles.`);
|
| 33 |
+
const last10 = candles.slice(-10);
|
| 34 |
+
last10.forEach((c, idx) => {
|
| 35 |
+
console.log(`[${idx}] Time: ${new Date(c.time * 1000).toLocaleString()}, Open: ${c.open.toFixed(5)}, High: ${c.high.toFixed(5)}, Low: ${c.low.toFixed(5)}, Close: ${c.close.toFixed(5)}, Volume: ${c.volume}`);
|
| 36 |
+
});
|
| 37 |
+
} catch (err) {
|
| 38 |
+
console.error('Test failed:', err);
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
test();
|
server.js
CHANGED
|
@@ -66,12 +66,10 @@ app.use(express.json());
|
|
| 66 |
|
| 67 |
const PORT = process.env.PORT || 7860;
|
| 68 |
const TIMEFRAME = process.env.AUTOPILOT_TIMEFRAME || '15m';
|
| 69 |
-
const SYMBOLS = ['BTCUSDT', '
|
| 70 |
|
| 71 |
-
// Filter active autopilot symbols
|
| 72 |
-
const AUTOPILOT_SYMBOLS =
|
| 73 |
-
? ['BTCUSDT', 'XAUUSD', 'GBPUSD', 'EURUSD', 'USDCAD']
|
| 74 |
-
: ['BTCUSDT', 'XAUUSD', 'GBPUSD', 'USDCAD'];
|
| 75 |
|
| 76 |
// Per-symbol Twelve Data API keys (OANDA-grade forex/metals that match TradingView).
|
| 77 |
// Read from environment so keys stay server-side and out of the public repo/bundle.
|
|
@@ -80,7 +78,6 @@ const TWELVEDATA_KEYS = {
|
|
| 80 |
XAUUSD: process.env.TWELVEDATA_KEY_XAUUSD,
|
| 81 |
GBPUSD: process.env.TWELVEDATA_KEY_GBPUSD,
|
| 82 |
USDCAD: process.env.TWELVEDATA_KEY_USDCAD,
|
| 83 |
-
EURUSD: process.env.TWELVEDATA_KEY_EURUSD,
|
| 84 |
};
|
| 85 |
const getTdKey = (symbol) => TWELVEDATA_KEYS[symbol] || null;
|
| 86 |
|
|
|
|
| 66 |
|
| 67 |
const PORT = process.env.PORT || 7860;
|
| 68 |
const TIMEFRAME = process.env.AUTOPILOT_TIMEFRAME || '15m';
|
| 69 |
+
const SYMBOLS = ['BTCUSDT', 'XAUUSD', 'GBPUSD', 'USDCAD'];
|
| 70 |
|
| 71 |
+
// Filter active autopilot symbols to maximize returns and eliminate noise
|
| 72 |
+
const AUTOPILOT_SYMBOLS = ['BTCUSDT', 'XAUUSD', 'GBPUSD', 'USDCAD'];
|
|
|
|
|
|
|
| 73 |
|
| 74 |
// Per-symbol Twelve Data API keys (OANDA-grade forex/metals that match TradingView).
|
| 75 |
// Read from environment so keys stay server-side and out of the public repo/bundle.
|
|
|
|
| 78 |
XAUUSD: process.env.TWELVEDATA_KEY_XAUUSD,
|
| 79 |
GBPUSD: process.env.TWELVEDATA_KEY_GBPUSD,
|
| 80 |
USDCAD: process.env.TWELVEDATA_KEY_USDCAD,
|
|
|
|
| 81 |
};
|
| 82 |
const getTdKey = (symbol) => TWELVEDATA_KEYS[symbol] || null;
|
| 83 |
|
src/analysis/exitManager.js
CHANGED
|
@@ -13,9 +13,7 @@ import { rsi, ema, macd } from './indicators.js';
|
|
| 13 |
*/
|
| 14 |
const CONTRACT_SPECS = {
|
| 15 |
BTCUSDT: { type: 'crypto', pipValue: 1, pipSize: 1 },
|
| 16 |
-
ETHUSDT: { type: 'crypto', pipValue: 1, pipSize: 1 },
|
| 17 |
XAUUSD: { type: 'commodity', pipValue: 100, pipSize: 1 },
|
| 18 |
-
EURUSD: { type: 'forex', pipValue: 10, pipSize: 0.0001 },
|
| 19 |
GBPUSD: { type: 'forex', pipValue: 10, pipSize: 0.0001 },
|
| 20 |
USDCAD: { type: 'forex_quote', pipValue: null, pipSize: 0.0001 },
|
| 21 |
};
|
|
@@ -54,9 +52,7 @@ const CONTRACT_SPECS = {
|
|
| 54 |
function getSpread(symbol) {
|
| 55 |
switch (symbol) {
|
| 56 |
case 'BTCUSDT': return 25.0;
|
| 57 |
-
case 'ETHUSDT': return 5.0;
|
| 58 |
case 'XAUUSD': return 0.7;
|
| 59 |
-
case 'EURUSD':
|
| 60 |
case 'GBPUSD':
|
| 61 |
case 'USDCAD':
|
| 62 |
return 0.00007; // 0.7 pips
|
|
@@ -93,7 +89,7 @@ export function analyzeExit(candles, trade) {
|
|
| 93 |
|
| 94 |
// Guard against micro-stop division anomalies in older trades
|
| 95 |
let minThreshold = 0.00005; // half a pip for Forex
|
| 96 |
-
if (trade.symbol === 'BTCUSDT'
|
| 97 |
minThreshold = 0.1; // 10 cents for crypto
|
| 98 |
} else if (trade.symbol === 'XAUUSD') {
|
| 99 |
minThreshold = 0.05; // 5 cents for gold
|
|
|
|
| 13 |
*/
|
| 14 |
const CONTRACT_SPECS = {
|
| 15 |
BTCUSDT: { type: 'crypto', pipValue: 1, pipSize: 1 },
|
|
|
|
| 16 |
XAUUSD: { type: 'commodity', pipValue: 100, pipSize: 1 },
|
|
|
|
| 17 |
GBPUSD: { type: 'forex', pipValue: 10, pipSize: 0.0001 },
|
| 18 |
USDCAD: { type: 'forex_quote', pipValue: null, pipSize: 0.0001 },
|
| 19 |
};
|
|
|
|
| 52 |
function getSpread(symbol) {
|
| 53 |
switch (symbol) {
|
| 54 |
case 'BTCUSDT': return 25.0;
|
|
|
|
| 55 |
case 'XAUUSD': return 0.7;
|
|
|
|
| 56 |
case 'GBPUSD':
|
| 57 |
case 'USDCAD':
|
| 58 |
return 0.00007; // 0.7 pips
|
|
|
|
| 89 |
|
| 90 |
// Guard against micro-stop division anomalies in older trades
|
| 91 |
let minThreshold = 0.00005; // half a pip for Forex
|
| 92 |
+
if (trade.symbol === 'BTCUSDT') {
|
| 93 |
minThreshold = 0.1; // 10 cents for crypto
|
| 94 |
} else if (trade.symbol === 'XAUUSD') {
|
| 95 |
minThreshold = 0.05; // 5 cents for gold
|
src/analysis/signalGenerator.js
CHANGED
|
@@ -17,9 +17,7 @@ import { rsi, ema, atr, macd } from './indicators.js';
|
|
| 17 |
/** @type {Record<string, { type: string, pipValue: number|null, pipSize: number, label: string }>} */
|
| 18 |
export const CONTRACT_SPECS = {
|
| 19 |
BTCUSDT: { type: 'crypto', pipValue: 1, pipSize: 1, label: '$/coin' },
|
| 20 |
-
ETHUSDT: { type: 'crypto', pipValue: 1, pipSize: 1, label: '$/coin' },
|
| 21 |
XAUUSD: { type: 'commodity', pipValue: 100, pipSize: 1, label: '$/point' },
|
| 22 |
-
EURUSD: { type: 'forex', pipValue: 10, pipSize: 0.0001, label: '$/pip' },
|
| 23 |
GBPUSD: { type: 'forex', pipValue: 10, pipSize: 0.0001, label: '$/pip' },
|
| 24 |
USDCAD: { type: 'forex_quote', pipValue: null, pipSize: 0.0001, label: '$/pip (dynamic)' },
|
| 25 |
};
|
|
@@ -33,9 +31,7 @@ export const MAX_DAILY_LOSSES = 3;
|
|
| 33 |
/** ATR multipliers config for Stop Loss buffers. */
|
| 34 |
export const ATR_MULTIPLIERS = {
|
| 35 |
BTCUSDT: 1.5,
|
| 36 |
-
ETHUSDT: 1.5,
|
| 37 |
XAUUSD: 0.5,
|
| 38 |
-
EURUSD: 0.5,
|
| 39 |
GBPUSD: 0.5,
|
| 40 |
USDCAD: 0.5,
|
| 41 |
};
|
|
@@ -53,9 +49,7 @@ export const ATR_CONFIG = {
|
|
| 53 |
function getSpread(symbol) {
|
| 54 |
switch (symbol) {
|
| 55 |
case 'BTCUSDT': return 25.0;
|
| 56 |
-
case 'ETHUSDT': return 5.0;
|
| 57 |
case 'XAUUSD': return 0.7;
|
| 58 |
-
case 'EURUSD':
|
| 59 |
case 'GBPUSD':
|
| 60 |
case 'USDCAD':
|
| 61 |
return 0.00007; // 0.7 pips
|
|
@@ -137,8 +131,6 @@ export function calculateLotSize(symbol, entryPrice, slPrice, currentRate = null
|
|
| 137 |
maxLots = 0.14;
|
| 138 |
} else if (symbol === 'XAUUSD') {
|
| 139 |
maxLots = 0.3;
|
| 140 |
-
} else if (symbol === 'ETHUSDT') {
|
| 141 |
-
maxLots = 3.0;
|
| 142 |
} else if (spec.type === 'forex' || spec.type === 'forex_quote') {
|
| 143 |
maxLots = 3.0;
|
| 144 |
}
|
|
@@ -205,7 +197,7 @@ export function generateSignals(candles, symbol, dailyLossCount = 0) {
|
|
| 205 |
const lastCandle = candles[candles.length - 1];
|
| 206 |
|
| 207 |
// Option B: Session-based Kill Zones for Forex and Gold
|
| 208 |
-
const isCrypto = symbol === 'BTCUSDT'
|
| 209 |
if (!isCrypto) {
|
| 210 |
const lastCandleDate = new Date(lastCandle.time * 1000);
|
| 211 |
const utcHour = lastCandleDate.getUTCHours();
|
|
@@ -379,10 +371,8 @@ function getAtrMultiplier(symbol, timeframe) {
|
|
| 379 |
if (isHigherTimeframe) {
|
| 380 |
switch (symbol) {
|
| 381 |
case 'BTCUSDT': return 2.5;
|
| 382 |
-
case 'ETHUSDT': return 1.5;
|
| 383 |
case 'XAUUSD': return 0.5;
|
| 384 |
case 'GBPUSD': return 2.0;
|
| 385 |
-
case 'EURUSD': return 1.0;
|
| 386 |
case 'USDCAD': return 2.5;
|
| 387 |
default: return 0.5;
|
| 388 |
}
|
|
@@ -390,10 +380,8 @@ function getAtrMultiplier(symbol, timeframe) {
|
|
| 390 |
// 15m, 5m, 1m
|
| 391 |
switch (symbol) {
|
| 392 |
case 'BTCUSDT': return 1.5;
|
| 393 |
-
case 'ETHUSDT': return 1.5;
|
| 394 |
case 'XAUUSD': return 1.5;
|
| 395 |
case 'GBPUSD': return 2.0;
|
| 396 |
-
case 'EURUSD': return 0.5;
|
| 397 |
case 'USDCAD': return 1.0;
|
| 398 |
default: return 0.5;
|
| 399 |
}
|
|
|
|
| 17 |
/** @type {Record<string, { type: string, pipValue: number|null, pipSize: number, label: string }>} */
|
| 18 |
export const CONTRACT_SPECS = {
|
| 19 |
BTCUSDT: { type: 'crypto', pipValue: 1, pipSize: 1, label: '$/coin' },
|
|
|
|
| 20 |
XAUUSD: { type: 'commodity', pipValue: 100, pipSize: 1, label: '$/point' },
|
|
|
|
| 21 |
GBPUSD: { type: 'forex', pipValue: 10, pipSize: 0.0001, label: '$/pip' },
|
| 22 |
USDCAD: { type: 'forex_quote', pipValue: null, pipSize: 0.0001, label: '$/pip (dynamic)' },
|
| 23 |
};
|
|
|
|
| 31 |
/** ATR multipliers config for Stop Loss buffers. */
|
| 32 |
export const ATR_MULTIPLIERS = {
|
| 33 |
BTCUSDT: 1.5,
|
|
|
|
| 34 |
XAUUSD: 0.5,
|
|
|
|
| 35 |
GBPUSD: 0.5,
|
| 36 |
USDCAD: 0.5,
|
| 37 |
};
|
|
|
|
| 49 |
function getSpread(symbol) {
|
| 50 |
switch (symbol) {
|
| 51 |
case 'BTCUSDT': return 25.0;
|
|
|
|
| 52 |
case 'XAUUSD': return 0.7;
|
|
|
|
| 53 |
case 'GBPUSD':
|
| 54 |
case 'USDCAD':
|
| 55 |
return 0.00007; // 0.7 pips
|
|
|
|
| 131 |
maxLots = 0.14;
|
| 132 |
} else if (symbol === 'XAUUSD') {
|
| 133 |
maxLots = 0.3;
|
|
|
|
|
|
|
| 134 |
} else if (spec.type === 'forex' || spec.type === 'forex_quote') {
|
| 135 |
maxLots = 3.0;
|
| 136 |
}
|
|
|
|
| 197 |
const lastCandle = candles[candles.length - 1];
|
| 198 |
|
| 199 |
// Option B: Session-based Kill Zones for Forex and Gold
|
| 200 |
+
const isCrypto = symbol === 'BTCUSDT';
|
| 201 |
if (!isCrypto) {
|
| 202 |
const lastCandleDate = new Date(lastCandle.time * 1000);
|
| 203 |
const utcHour = lastCandleDate.getUTCHours();
|
|
|
|
| 371 |
if (isHigherTimeframe) {
|
| 372 |
switch (symbol) {
|
| 373 |
case 'BTCUSDT': return 2.5;
|
|
|
|
| 374 |
case 'XAUUSD': return 0.5;
|
| 375 |
case 'GBPUSD': return 2.0;
|
|
|
|
| 376 |
case 'USDCAD': return 2.5;
|
| 377 |
default: return 0.5;
|
| 378 |
}
|
|
|
|
| 380 |
// 15m, 5m, 1m
|
| 381 |
switch (symbol) {
|
| 382 |
case 'BTCUSDT': return 1.5;
|
|
|
|
| 383 |
case 'XAUUSD': return 1.5;
|
| 384 |
case 'GBPUSD': return 2.0;
|
|
|
|
| 385 |
case 'USDCAD': return 1.0;
|
| 386 |
default: return 0.5;
|
| 387 |
}
|
src/chart/chartManager.js
CHANGED
|
@@ -137,7 +137,7 @@ export class ChartManager {
|
|
| 137 |
this.lastClose = candles[candles.length - 1].close;
|
| 138 |
|
| 139 |
// Configure exact precision based on asset class
|
| 140 |
-
const isForex = ['
|
| 141 |
const precision = isForex ? 5 : 2;
|
| 142 |
const minMove = isForex ? 0.00001 : 0.01;
|
| 143 |
|
|
@@ -260,7 +260,7 @@ export class ChartManager {
|
|
| 260 |
if (!orderBlocks || orderBlocks.length === 0 || !candles || candles.length === 0) return;
|
| 261 |
|
| 262 |
const lastClose = candles[candles.length - 1].close;
|
| 263 |
-
const isCrypto = this.symbol === 'BTCUSDT'
|
| 264 |
const limitPct = isCrypto ? 0.12 : 0.03; // 12% for crypto, 3% for forex/gold
|
| 265 |
const maxDiff = lastClose * limitPct;
|
| 266 |
|
|
@@ -308,7 +308,7 @@ export class ChartManager {
|
|
| 308 |
if (!zones || zones.length === 0) return;
|
| 309 |
|
| 310 |
const lastClose = this.lastClose || zones[0].top;
|
| 311 |
-
const isCrypto = this.symbol === 'BTCUSDT'
|
| 312 |
const limitPct = isCrypto ? 0.12 : 0.03;
|
| 313 |
const maxDiff = lastClose * limitPct;
|
| 314 |
|
|
|
|
| 137 |
this.lastClose = candles[candles.length - 1].close;
|
| 138 |
|
| 139 |
// Configure exact precision based on asset class
|
| 140 |
+
const isForex = ['GBPUSD', 'USDCAD'].includes(symbol);
|
| 141 |
const precision = isForex ? 5 : 2;
|
| 142 |
const minMove = isForex ? 0.00001 : 0.01;
|
| 143 |
|
|
|
|
| 260 |
if (!orderBlocks || orderBlocks.length === 0 || !candles || candles.length === 0) return;
|
| 261 |
|
| 262 |
const lastClose = candles[candles.length - 1].close;
|
| 263 |
+
const isCrypto = this.symbol === 'BTCUSDT';
|
| 264 |
const limitPct = isCrypto ? 0.12 : 0.03; // 12% for crypto, 3% for forex/gold
|
| 265 |
const maxDiff = lastClose * limitPct;
|
| 266 |
|
|
|
|
| 308 |
if (!zones || zones.length === 0) return;
|
| 309 |
|
| 310 |
const lastClose = this.lastClose || zones[0].top;
|
| 311 |
+
const isCrypto = this.symbol === 'BTCUSDT';
|
| 312 |
const limitPct = isCrypto ? 0.12 : 0.03;
|
| 313 |
const maxDiff = lastClose * limitPct;
|
| 314 |
|
src/components/analysisPanel.js
CHANGED
|
@@ -512,7 +512,6 @@ export class AnalysisPanel {
|
|
| 512 |
|
| 513 |
switch (symbol) {
|
| 514 |
case 'BTCUSDT':
|
| 515 |
-
case 'ETHUSDT':
|
| 516 |
lots = 50 / slDist;
|
| 517 |
risk = slDist * lots;
|
| 518 |
distLabel = `$${slDist.toFixed(2)}`;
|
|
@@ -522,7 +521,6 @@ export class AnalysisPanel {
|
|
| 522 |
risk = slDist * lots * 100;
|
| 523 |
distLabel = `${slDist.toFixed(2)} pts`;
|
| 524 |
break;
|
| 525 |
-
case 'EURUSD':
|
| 526 |
case 'GBPUSD':
|
| 527 |
const pips = slDist / 0.0001;
|
| 528 |
lots = 50 / (pips * 10);
|
|
@@ -549,9 +547,7 @@ export class AnalysisPanel {
|
|
| 549 |
maxLots = 0.14;
|
| 550 |
} else if (symbol === 'XAUUSD') {
|
| 551 |
maxLots = 0.3;
|
| 552 |
-
} else if (
|
| 553 |
-
maxLots = 3.0;
|
| 554 |
-
} else if (['EURUSD', 'GBPUSD', 'USDCAD'].includes(symbol)) {
|
| 555 |
maxLots = 3.0;
|
| 556 |
}
|
| 557 |
|
|
@@ -560,13 +556,11 @@ export class AnalysisPanel {
|
|
| 560 |
// Recalculate precise actual risk based on final clamped lot size
|
| 561 |
switch (symbol) {
|
| 562 |
case 'BTCUSDT':
|
| 563 |
-
case 'ETHUSDT':
|
| 564 |
risk = slDist * lots;
|
| 565 |
break;
|
| 566 |
case 'XAUUSD':
|
| 567 |
risk = slDist * lots * 100;
|
| 568 |
break;
|
| 569 |
-
case 'EURUSD':
|
| 570 |
case 'GBPUSD':
|
| 571 |
risk = (slDist / 0.0001) * lots * 10;
|
| 572 |
break;
|
|
@@ -614,7 +608,7 @@ export class AnalysisPanel {
|
|
| 614 |
_getDecimals(symbol) {
|
| 615 |
const sym = symbol || this.currentSymbol;
|
| 616 |
if (sym === 'XAUUSD') return 2;
|
| 617 |
-
if (sym === 'BTCUSDT'
|
| 618 |
return 5;
|
| 619 |
}
|
| 620 |
|
|
|
|
| 512 |
|
| 513 |
switch (symbol) {
|
| 514 |
case 'BTCUSDT':
|
|
|
|
| 515 |
lots = 50 / slDist;
|
| 516 |
risk = slDist * lots;
|
| 517 |
distLabel = `$${slDist.toFixed(2)}`;
|
|
|
|
| 521 |
risk = slDist * lots * 100;
|
| 522 |
distLabel = `${slDist.toFixed(2)} pts`;
|
| 523 |
break;
|
|
|
|
| 524 |
case 'GBPUSD':
|
| 525 |
const pips = slDist / 0.0001;
|
| 526 |
lots = 50 / (pips * 10);
|
|
|
|
| 547 |
maxLots = 0.14;
|
| 548 |
} else if (symbol === 'XAUUSD') {
|
| 549 |
maxLots = 0.3;
|
| 550 |
+
} else if (['GBPUSD', 'USDCAD'].includes(symbol)) {
|
|
|
|
|
|
|
| 551 |
maxLots = 3.0;
|
| 552 |
}
|
| 553 |
|
|
|
|
| 556 |
// Recalculate precise actual risk based on final clamped lot size
|
| 557 |
switch (symbol) {
|
| 558 |
case 'BTCUSDT':
|
|
|
|
| 559 |
risk = slDist * lots;
|
| 560 |
break;
|
| 561 |
case 'XAUUSD':
|
| 562 |
risk = slDist * lots * 100;
|
| 563 |
break;
|
|
|
|
| 564 |
case 'GBPUSD':
|
| 565 |
risk = (slDist / 0.0001) * lots * 10;
|
| 566 |
break;
|
|
|
|
| 608 |
_getDecimals(symbol) {
|
| 609 |
const sym = symbol || this.currentSymbol;
|
| 610 |
if (sym === 'XAUUSD') return 2;
|
| 611 |
+
if (sym === 'BTCUSDT') return 2;
|
| 612 |
return 5;
|
| 613 |
}
|
| 614 |
|
src/components/header.js
CHANGED
|
@@ -139,7 +139,6 @@ export class Header {
|
|
| 139 |
_getDecimals(symbol) {
|
| 140 |
if (symbol === 'XAUUSD') return 2;
|
| 141 |
if (symbol === 'BTCUSDT') return 2;
|
| 142 |
-
if (symbol === 'ETHUSDT') return 2;
|
| 143 |
return 5; // forex pairs
|
| 144 |
}
|
| 145 |
|
|
|
|
| 139 |
_getDecimals(symbol) {
|
| 140 |
if (symbol === 'XAUUSD') return 2;
|
| 141 |
if (symbol === 'BTCUSDT') return 2;
|
|
|
|
| 142 |
return 5; // forex pairs
|
| 143 |
}
|
| 144 |
|
src/components/tradeManager.js
CHANGED
|
@@ -617,7 +617,7 @@ export class TradeManager {
|
|
| 617 |
|
| 618 |
getSymbolStats() {
|
| 619 |
const stats = {};
|
| 620 |
-
const symbols = ['BTCUSDT', '
|
| 621 |
for (const sym of symbols) {
|
| 622 |
stats[sym] = {
|
| 623 |
total: 0,
|
|
|
|
| 617 |
|
| 618 |
getSymbolStats() {
|
| 619 |
const stats = {};
|
| 620 |
+
const symbols = ['BTCUSDT', 'XAUUSD', 'GBPUSD', 'USDCAD'];
|
| 621 |
for (const sym of symbols) {
|
| 622 |
stats[sym] = {
|
| 623 |
total: 0,
|
src/data/dataManager.js
CHANGED
|
@@ -23,10 +23,8 @@ import { ServerStream } from './serverStream.js';
|
|
| 23 |
*/
|
| 24 |
export const SYMBOL_CONFIG = {
|
| 25 |
BTCUSDT: { provider: 'yahoo', wsSymbol: 'BTC-USD', displayName: 'BTC/USDT', type: 'crypto' },
|
| 26 |
-
ETHUSDT: { provider: 'yahoo', wsSymbol: 'ETH-USD', displayName: 'ETH/USDT', type: 'crypto' },
|
| 27 |
XAUUSD: { provider: 'yahoo', wsSymbol: 'GC=F', displayName: 'XAU/USD', type: 'commodity' },
|
| 28 |
GBPUSD: { provider: 'yahoo', wsSymbol: 'GBPUSD=X', displayName: 'GBP/USD', type: 'forex' },
|
| 29 |
-
EURUSD: { provider: 'yahoo', wsSymbol: 'EURUSD=X', displayName: 'EUR/USD', type: 'forex' },
|
| 30 |
USDCAD: { provider: 'yahoo', wsSymbol: 'USDCAD=X', displayName: 'USD/CAD', type: 'forex' },
|
| 31 |
};
|
| 32 |
|
|
|
|
| 23 |
*/
|
| 24 |
export const SYMBOL_CONFIG = {
|
| 25 |
BTCUSDT: { provider: 'yahoo', wsSymbol: 'BTC-USD', displayName: 'BTC/USDT', type: 'crypto' },
|
|
|
|
| 26 |
XAUUSD: { provider: 'yahoo', wsSymbol: 'GC=F', displayName: 'XAU/USD', type: 'commodity' },
|
| 27 |
GBPUSD: { provider: 'yahoo', wsSymbol: 'GBPUSD=X', displayName: 'GBP/USD', type: 'forex' },
|
|
|
|
| 28 |
USDCAD: { provider: 'yahoo', wsSymbol: 'USDCAD=X', displayName: 'USD/CAD', type: 'forex' },
|
| 29 |
};
|
| 30 |
|
src/data/twelveDataAPI.js
CHANGED
|
@@ -12,11 +12,9 @@
|
|
| 12 |
// App symbol -> Twelve Data symbol (forex/metals use slash notation).
|
| 13 |
const TD_SYMBOL = {
|
| 14 |
XAUUSD: 'XAU/USD',
|
| 15 |
-
EURUSD: 'EUR/USD',
|
| 16 |
GBPUSD: 'GBP/USD',
|
| 17 |
USDCAD: 'USD/CAD',
|
| 18 |
BTCUSDT: 'BTC/USD',
|
| 19 |
-
ETHUSDT: 'ETH/USD',
|
| 20 |
};
|
| 21 |
|
| 22 |
// App timeframe -> Twelve Data interval.
|
|
|
|
| 12 |
// App symbol -> Twelve Data symbol (forex/metals use slash notation).
|
| 13 |
const TD_SYMBOL = {
|
| 14 |
XAUUSD: 'XAU/USD',
|
|
|
|
| 15 |
GBPUSD: 'GBP/USD',
|
| 16 |
USDCAD: 'USD/CAD',
|
| 17 |
BTCUSDT: 'BTC/USD',
|
|
|
|
| 18 |
};
|
| 19 |
|
| 20 |
// App timeframe -> Twelve Data interval.
|
src/data/yahooFinanceAPI.js
CHANGED
|
@@ -8,10 +8,8 @@ const YAHOO_BASE = '/api/yahoo/v8/finance/chart';
|
|
| 8 |
|
| 9 |
export const YAHOO_SYMBOL_MAP = {
|
| 10 |
BTCUSDT: 'BTC-USD', // Bitcoin Spot
|
| 11 |
-
ETHUSDT: 'ETH-USD', // Ethereum Spot
|
| 12 |
XAUUSD: 'GC=F', // Gold Futures
|
| 13 |
GBPUSD: 'GBPUSD=X', // Spot GBP/USD
|
| 14 |
-
EURUSD: 'EURUSD=X', // Spot EUR/USD
|
| 15 |
USDCAD: 'USDCAD=X', // Spot USD/CAD
|
| 16 |
};
|
| 17 |
|
|
@@ -85,7 +83,7 @@ const TIMEFRAME_CONFIG = {
|
|
| 85 |
* @returns {Array} filtered candles (forex only; other symbols returned unchanged)
|
| 86 |
*/
|
| 87 |
export function filterForexOutlierWicks(candles, symbol) {
|
| 88 |
-
if (!['
|
| 89 |
return candles;
|
| 90 |
}
|
| 91 |
|
|
|
|
| 8 |
|
| 9 |
export const YAHOO_SYMBOL_MAP = {
|
| 10 |
BTCUSDT: 'BTC-USD', // Bitcoin Spot
|
|
|
|
| 11 |
XAUUSD: 'GC=F', // Gold Futures
|
| 12 |
GBPUSD: 'GBPUSD=X', // Spot GBP/USD
|
|
|
|
| 13 |
USDCAD: 'USDCAD=X', // Spot USD/CAD
|
| 14 |
};
|
| 15 |
|
|
|
|
| 83 |
* @returns {Array} filtered candles (forex only; other symbols returned unchanged)
|
| 84 |
*/
|
| 85 |
export function filterForexOutlierWicks(candles, symbol) {
|
| 86 |
+
if (!['GBPUSD', 'USDCAD'].includes(symbol) || !candles || candles.length === 0) {
|
| 87 |
return candles;
|
| 88 |
}
|
| 89 |
|
src/main.js
CHANGED
|
@@ -29,9 +29,7 @@ let history = [];
|
|
| 29 |
let lastTickPrice = null;
|
| 30 |
const livePrices = {
|
| 31 |
BTCUSDT: 0,
|
| 32 |
-
ETHUSDT: 0,
|
| 33 |
XAUUSD: 0,
|
| 34 |
-
EURUSD: 0,
|
| 35 |
GBPUSD: 0,
|
| 36 |
USDCAD: 1.38, // approximate fallback
|
| 37 |
};
|
|
|
|
| 29 |
let lastTickPrice = null;
|
| 30 |
const livePrices = {
|
| 31 |
BTCUSDT: 0,
|
|
|
|
| 32 |
XAUUSD: 0,
|
|
|
|
| 33 |
GBPUSD: 0,
|
| 34 |
USDCAD: 1.38, // approximate fallback
|
| 35 |
};
|