Bot & Automation
Treding Forex Ai
/root/hermes-projects/Treding Forex Ai
dist/src/services/strategy/forexStrategy.js
text
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ForexStrategy = void 0;
const node_crypto_1 = require("node:crypto");
const risk_1 = require("../../constants/risk");
const math_1 = require("../../utils/math");
class ForexStrategy {
marketDataService;
constructor(marketDataService) {
this.marketDataService = marketDataService;
}
async analyze(symbol, mode) {
const modeConfig = risk_1.TRADING_MODE_CONFIGS[mode];
const primaryTimeframe = modeConfig.timeframes[1] ?? modeConfig.timeframes[0];
const marketData = await this.marketDataService.getCandles(symbol, primaryTimeframe, 220);
const candles = marketData.candles;
if (candles.length < 60) {
throw new Error("Data candle tidak cukup untuk analisis");
}
const closes = candles.map((candle) => candle.close);
const currentPrice = closes.at(-1) ?? 0;
const ema20 = (0, math_1.exponentialMovingAverage)(closes, 20);
const ema50 = (0, math_1.exponentialMovingAverage)(closes, 50);
const ema200 = (0, math_1.exponentialMovingAverage)(closes, 200);
const rsi = (0, math_1.relativeStrengthIndex)(closes, 14);
const atr = (0, math_1.averageTrueRange)(candles, 14);
const macd = (0, math_1.macdHistogram)(closes);
const levels = (0, math_1.supportResistance)(candles, 40);
const volatilityPercent = currentPrice > 0 ? (atr / currentPrice) * 100 : 0;
const snapshot = {
ema20,
ema50,
ema200,
rsi,
atr,
macdHistogram: macd,
support: levels.support,
resistance: levels.resistance,
volatilityPercent
};
const bullishScore = scoreBullishSetup(snapshot, currentPrice);
const bearishScore = scoreBearishSetup(snapshot, currentPrice);
const directionScore = Math.max(bullishScore, bearishScore);
let confidence = Math.round((0, math_1.clamp)(43 + directionScore * 8 - volatilityPenalty(volatilityPercent), 35, 88));
const trend = describeTrend(snapshot);
let action = confidence >= modeConfig.confidenceThreshold
? bullishScore > bearishScore
? "BUY"
: "SELL"
: "WAIT";
// USER EXPLICIT OVERRIDE: For scalping, ALWAYS pick a direction (no WAIT)
if (mode === "scalping" && action === "WAIT") {
action = bullishScore >= bearishScore ? "BUY" : "SELL";
confidence = Math.max(confidence, modeConfig.confidenceThreshold); // Force pass
}
let stopDistance = Math.max(atr * modeConfig.atrStopMultiplier, symbol.pipSize * symbol.defaultSpreadPips * 2);
let tp1Distance = stopDistance * modeConfig.targetRiskReward;
let tp2Distance = stopDistance * (modeConfig.targetRiskReward + 0.5);
// USER EXPLICIT OVERRIDE: Extremely tight SL (1.20) and TP (0.80) for XAUUSD
if (symbol.code === "XAUUSD") {
stopDistance = 1.20;
tp1Distance = 0.80;
tp2Distance = 1.50;
}
else if (mode === "scalping") {
const maxSlDistance = 25 * symbol.pipSize;
const minSlDistance = 15 * symbol.pipSize;
stopDistance = (0, math_1.clamp)(stopDistance, minSlDistance, maxSlDistance);
const maxTp1Distance = 20 * symbol.pipSize;
const minTp1Distance = 10 * symbol.pipSize;
tp1Distance = (0, math_1.clamp)(tp1Distance, minTp1Distance, maxTp1Distance);
tp2Distance = tp1Distance * 1.5;
}
const priceDigits = symbol.pipSize >= 0.01 ? 2 : 5;
const entry = action === "WAIT" ? null : (0, math_1.roundTo)(currentPrice, priceDigits);
const stopLoss = action === "BUY"
? (0, math_1.roundTo)(currentPrice - stopDistance, priceDigits)
: action === "SELL"
? (0, math_1.roundTo)(currentPrice + stopDistance, priceDigits)
: null;
const takeProfit1 = action === "BUY"
? (0, math_1.roundTo)(currentPrice + tp1Distance, priceDigits)
: action === "SELL"
? (0, math_1.roundTo)(currentPrice - tp1Distance, priceDigits)
: null;
const takeProfit2 = action === "BUY"
? (0, math_1.roundTo)(currentPrice + tp2Distance, priceDigits)
: action === "SELL"
? (0, math_1.roundTo)(currentPrice - tp2Distance, priceDigits)
: null;
return {
id: (0, node_crypto_1.randomUUID)(),
symbol,
mode,
timeframe: primaryTimeframe,
action,
confidence,
currentPrice: (0, math_1.roundTo)(currentPrice, priceDigits),
trend,
entry,
stopLoss,
takeProfit1,
takeProfit2,
riskLevel: classifyRisk(confidence, volatilityPercent),
riskRewardRatio: action === "WAIT" ? 0 : modeConfig.targetRiskReward,
reasons: buildReasons(snapshot, trend, bullishScore, bearishScore, action),
invalidation: buildInvalidation(action, snapshot, priceDigits),
snapshot,
dataWarning: marketData.warning,
createdAt: new Date().toISOString()
};
}
}
exports.ForexStrategy = ForexStrategy;
function scoreBullishSetup(snapshot, currentPrice) {
let score = 0;
if (snapshot.ema20 > snapshot.ema50) {
score += 1;
}
if (snapshot.ema50 > snapshot.ema200) {
score += 1;
}
if (currentPrice > snapshot.ema20) {
score += 1;
}
if (snapshot.rsi > 52 && snapshot.rsi < 72) {
score += 1;
}
if (snapshot.macdHistogram > 0) {
score += 1;
}
return score;
}
function scoreBearishSetup(snapshot, currentPrice) {
let score = 0;
if (snapshot.ema20 < snapshot.ema50) {
score += 1;
}
if (snapshot.ema50 < snapshot.ema200) {
score += 1;
}
if (currentPrice < snapshot.ema20) {
score += 1;
}
if (snapshot.rsi < 48 && snapshot.rsi > 28) {
score += 1;
}
if (snapshot.macdHistogram < 0) {
score += 1;
}
return score;
}
function describeTrend(snapshot) {
if (snapshot.ema20 > snapshot.ema50 && snapshot.ema50 > snapshot.ema200) {
return "Bullish terstruktur";
}
if (snapshot.ema20 < snapshot.ema50 && snapshot.ema50 < snapshot.ema200) {
return "Bearish terstruktur";
}
return "Sideways / transisi";
}
function volatilityPenalty(volatilityPercent) {
if (volatilityPercent > 1.2) {
return 9;
}
if (volatilityPercent > 0.7) {
return 5;
}
return 0;
}
function classifyRisk(confidence, volatilityPercent) {
if (confidence >= 74 && volatilityPercent < 0.7) {
return "Low";
}
if (confidence >= 60 && volatilityPercent < 1.2) {
return "Medium";
}
return "High";
}
function buildReasons(snapshot, trend, bullishScore, bearishScore, action) {
return [
`Trend utama: ${trend}.`,
`EMA score bullish ${bullishScore}/5 dan bearish ${bearishScore}/5.`,
`RSI ${(0, math_1.roundTo)(snapshot.rsi, 2)} menunjukkan momentum ${describeRsi(snapshot.rsi)}.`,
`MACD histogram ${(0, math_1.roundTo)(snapshot.macdHistogram, 5)}.`,
`Support ${(0, math_1.roundTo)(snapshot.support, 5)} dan resistance ${(0, math_1.roundTo)(snapshot.resistance, 5)}.`,
action === "WAIT"
? "Setup belum cukup bersih, prioritas WAIT."
: "Setup memenuhi threshold mode, tetap wajib gunakan stop loss."
];
}
function describeRsi(rsi) {
if (rsi >= 70) {
return "overbought";
}
if (rsi <= 30) {
return "oversold";
}
if (rsi > 52) {
return "bullish ringan";
}
if (rsi < 48) {
return "bearish ringan";
}
return "netral";
}
function buildInvalidation(action, snapshot, priceDigits) {
if (action === "BUY") {
return `Batal jika harga close di bawah EMA50 (${(0, math_1.roundTo)(snapshot.ema50, priceDigits)}) atau momentum MACD berubah negatif.`;
}
if (action === "SELL") {
return `Batal jika harga close di atas EMA50 (${(0, math_1.roundTo)(snapshot.ema50, priceDigits)}) atau momentum MACD berubah positif.`;
}
return "Tunggu breakout valid atau struktur EMA lebih jelas sebelum entry.";
}