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)
    };
  }
}