Bot & Automation

Treding Forex Ai

/root/hermes-projects/Treding Forex Ai

src/services/tradingService.ts text
import { DEFAULT_TRADING_MODE } from "../constants/risk";
import { findMarketSymbol } from "../constants/markets";
import type { BrokerAdapter } from "./broker/broker";
import type {
  BrokerExecutionMode,
  BrokerExecutionStatus,
  ExecutedTrade,
  OrderIntent,
  MarketSymbol,
  Timeframe,
  TradingMode,
  TradingSignal
} from "../domain/types";
import type { ForexStrategy } from "./strategy/forexStrategy";
import type { RiskManager } from "./riskManager";
import type { JsonStore } from "./storage/jsonStore";

export interface SignalResult {
  signal: TradingSignal;
  orderIntent: OrderIntent | null;
  riskNotes: string[];
}

export class TradingService {
  public constructor(
    private readonly strategy: ForexStrategy,
    private readonly riskManager: RiskManager,
    private readonly store: JsonStore,
    private readonly broker: BrokerAdapter
  ) {}

  public async createSignal(params: {
    chatId: number;
    userId: number;
    symbolCode: string;
    mode?: TradingMode;
    executionMode?: BrokerExecutionMode;
    strategyProfile?: "fast_scalping" | "trend_scalping" | "custom";
    maxOpenTradesOverride?: number;
  }): Promise<SignalResult> {
    const symbol = findMarketSymbol(params.symbolCode);

    if (!symbol) {
      throw new Error("Symbol market tidak didukung.");
    }

    const signal = await this.strategy.analyze(symbol, params.mode ?? DEFAULT_TRADING_MODE, {
      executionMode: params.executionMode,
      scalpingProfile: params.strategyProfile
    });
    const openTrades = await this.store.listOpenPaperTrades(params.chatId);
    const todayTrades = await this.store.listTodayPaperTrades(params.chatId);
    const todayLoss = estimateTodayLoss(todayTrades);
    const openTradeCount = await this.resolveOpenTradeCount(
      params.chatId,
      openTrades.length,
      params.executionMode
    );
    const riskDecision = this.riskManager.evaluate(
      signal,
      openTradeCount,
      todayLoss,
      params.maxOpenTradesOverride
    );

    if (!riskDecision.accepted || !riskDecision.positionSizing) {
      return {
        signal,
        orderIntent: null,
        riskNotes: riskDecision.reasons
      };
    }

    const orderIntent = this.riskManager.buildOrderIntent({
      chatId: params.chatId,
      userId: params.userId,
      signal,
      positionSizing: riskDecision.positionSizing
    });
    await this.store.savePendingOrder(orderIntent);

    return {
      signal,
      orderIntent,
      riskNotes: riskDecision.reasons
    };
  }

  public async executePendingOrder(
    orderId: string,
    options?: { desiredExecutionMode?: BrokerExecutionMode }
  ): Promise<ExecutedTrade> {
    const order = await this.store.getPendingOrder(orderId);

    if (!order) {
      throw new Error("Order preview sudah tidak tersedia.");
    }

    const trade = await this.broker.execute(order, options);

    if (trade.broker !== "paper") {
      await this.store.appendPaperTrade(trade);
      await this.store.removePendingOrder(order.id);
    }

    return trade;
  }

  public async getBrokerExecutionStatus(
    desiredExecutionMode?: BrokerExecutionMode
  ): Promise<BrokerExecutionStatus> {
    return this.broker.getExecutionStatus({ desiredExecutionMode });
  }

  public getAccountBalance(): number {
    return this.riskManager.getAccountBalance();
  }

  public async savePendingOrder(order: OrderIntent): Promise<void> {
    await this.store.savePendingOrder(order);
  }

  public async recordBrokerClosure(
    brokerOrderId: string,
    closePrice: number,
    pnl: number
  ): Promise<ExecutedTrade | null> {
    const openTrades = await this.store.listAllOpenTrades();
    const trade = openTrades.find((item) => item.brokerOrderId === brokerOrderId);
    if (!trade) {
      return null;
    }

    return this.store.closeTrade(trade.id, closePrice, pnl);
  }

  public getMarketCandles(
    symbol: MarketSymbol,
    timeframe: Timeframe,
    limit: number,
    executionMode?: BrokerExecutionMode
  ) {
    return this.strategy.getCandles(symbol, timeframe, limit, executionMode);
  }

  public async listTodayTrades(chatId: number): Promise<ExecutedTrade[]> {
    return this.store.listTodayPaperTrades(chatId);
  }

  private async resolveOpenTradeCount(
    chatId: number,
    localOpenTradeCount: number,
    executionMode?: BrokerExecutionMode
  ): Promise<number> {
    const brokerStatus = await this.broker
      .getExecutionStatus({ desiredExecutionMode: executionMode })
      .catch(() => null);
    if (!brokerStatus || brokerStatus.provider !== "mt5_bridge" || !brokerStatus.connected) {
      return localOpenTradeCount;
    }

    const positions = await this.broker.getPositions({
      desiredExecutionMode:
        brokerStatus.currentMode === "demo" || brokerStatus.currentMode === "real"
          ? brokerStatus.currentMode
          : undefined
    }).catch(() => null);
    if (!positions) {
      return localOpenTradeCount;
    }

    const localPaperTrades = await this.store.listOpenPaperTrades(chatId);
    const localPaperOnlyCount = localPaperTrades.filter((trade) => trade.broker === "paper").length;
    return localPaperOnlyCount + positions.length;
  }

  /**
   * Tutup posisi paper trade berdasarkan orderId.
   * P&L dihitung dari selisih harga entry vs closePrice.
   */
  public async closePaperPosition(
    orderId: string,
    closePrice: number
  ): Promise<ExecutedTrade | null> {
    const allOpen = await this.store.listAllOpenTrades();
    const trade = allOpen.find((t) => t.id === orderId);

    if (!trade) {
      return null;
    }

    const pnl = calcPnl(trade, closePrice);
    return this.store.closeTrade(orderId, closePrice, pnl);
  }

  public async addToWatchlist(chatId: number, symbolCode: string): Promise<void> {
    const symbol = findMarketSymbol(symbolCode);

    if (!symbol) {
      throw new Error("Symbol market tidak didukung.");
    }

    await this.store.addWatchlist(chatId, symbol.code);
  }

  public async getWatchlist(chatId: number): Promise<string[]> {
    const entries = await this.store.listWatchlist(chatId);
    return entries.map((entry) => entry.symbolCode);
  }

  public async getTodayLoss(chatId: number): Promise<number> {
    const todayTrades = await this.store.listTodayPaperTrades(chatId);
    return estimateTodayLoss(todayTrades);
  }
}

function estimateTodayLoss(trades: ExecutedTrade[]): number {
  return trades.reduce((total, trade) => {
    const pnl = trade.pnl ?? 0;
    return pnl < 0 ? total + Math.abs(pnl) : total;
  }, 0);
}

/**
 * Hitung approximate P&L dalam satuan USD.
 * Untuk akurasi penuh seharusnya pakai symbol.pipValuePerLot,
 * namun di sini digunakan perkiraan: (pips * lotSize * 10_000 / 1000).
 * XAUUSD: 1 pip = $0.1 per 0.01 lot → factor 10 sudah representatif untuk paper mode.
 */
export function calcPnl(trade: ExecutedTrade, closePrice: number): number {
  const priceDiff =
    trade.action === "BUY" ? closePrice - trade.entry : trade.entry - closePrice;
  const rawPnl = priceDiff * trade.positionSizing.lotSize * 100;
  return Math.round(rawPnl * 100) / 100;
}