Bot & Automation
Treding Forex Ai
/root/hermes-projects/Treding Forex Ai
src/services/riskManager.ts
text
import { randomUUID } from "node:crypto";
import { TRADING_MODE_CONFIGS } from "../constants/risk";
import type {
MarketSymbol,
OrderIntent,
PositionSizing,
RiskConfig,
TradingSignal
} from "../domain/types";
import { clamp, roundTo } from "../utils/math";
export interface RiskDecision {
accepted: boolean;
reasons: string[];
positionSizing?: PositionSizing;
}
export class RiskManager {
public constructor(private readonly riskConfig: RiskConfig) {}
public getAccountBalance(): number {
return this.riskConfig.accountBalance;
}
public evaluate(
signal: TradingSignal,
openTradeCount: number,
todayLossAmount: number,
maxOpenTradesOverride?: number
): RiskDecision {
const reasons: string[] = [];
if (signal.action === "WAIT") {
reasons.push("Signal WAIT tidak boleh dieksekusi.");
}
const modeConfig = TRADING_MODE_CONFIGS[signal.mode];
if (signal.confidence < modeConfig.confidenceThreshold) {
reasons.push(`Confidence ${signal.confidence}% di bawah threshold ${modeConfig.confidenceThreshold}%.`);
}
const maxOpenTrades = maxOpenTradesOverride ?? this.riskConfig.maxOpenTrades;
if (maxOpenTrades > 0 && openTradeCount >= maxOpenTrades) {
reasons.push(`Open trade sudah mencapai batas ${maxOpenTrades}.`);
}
const maxDailyLoss = this.riskConfig.accountBalance * (this.riskConfig.maxDailyLossPercent / 100);
if (Math.abs(todayLossAmount) >= maxDailyLoss) {
reasons.push("Max daily loss sudah tercapai.");
}
if (signal.entry === null || signal.stopLoss === null) {
reasons.push("Entry atau stop loss belum valid.");
}
if (signal.riskRewardRatio < 1.5 && signal.action !== "WAIT") {
reasons.push("Risk reward ratio kurang dari 1:1.5.");
}
if (reasons.length > 0 || signal.entry === null || signal.stopLoss === null) {
return { accepted: false, reasons };
}
const positionSizing = this.calculatePositionSize(signal.symbol, signal.entry, signal.stopLoss, modeConfig.riskMultiplier);
return {
accepted: true,
reasons: ["Risk check lolos untuk paper trade."],
positionSizing
};
}
public buildOrderIntent(params: {
chatId: number;
userId: number;
signal: TradingSignal;
positionSizing: PositionSizing;
}): OrderIntent {
const { chatId, userId, signal, positionSizing } = params;
if (
signal.action === "WAIT" ||
signal.entry === null ||
signal.stopLoss === null ||
signal.takeProfit1 === null ||
signal.takeProfit2 === null
) {
throw new Error("Signal tidak bisa dibuat menjadi order.");
}
return {
id: randomUUID(),
chatId,
userId,
signalId: signal.id,
symbolCode: signal.symbol.code,
action: signal.action,
mode: signal.mode,
entry: signal.entry,
stopLoss: signal.stopLoss,
takeProfit1: signal.takeProfit1,
takeProfit2: signal.takeProfit2,
confidence: signal.confidence,
riskLevel: signal.riskLevel,
riskRewardRatio: signal.riskRewardRatio,
positionSizing,
status: "pending",
createdAt: new Date().toISOString()
};
}
private calculatePositionSize(
symbol: MarketSymbol,
entry: number,
stopLoss: number,
riskMultiplier: number
): PositionSizing {
const riskAmount = this.riskConfig.accountBalance * (this.riskConfig.riskPerTradePercent / 100) * riskMultiplier;
const stopDistancePips = Math.max(Math.abs(entry - stopLoss) / symbol.pipSize, 1);
const rawLotSize = riskAmount / (stopDistancePips * symbol.pipValuePerLot);
const maxLot = Math.min(symbol.maxLot, this.riskConfig.maxLotPerOrder);
const lotSize = clamp(roundTo(rawLotSize, 2), symbol.minLot, maxLot);
return {
lotSize,
riskAmount: roundTo(riskAmount, 2),
stopDistancePips: roundTo(stopDistancePips, 1)
};
}
}