Bot & Automation

Market AI

/root/hermes-projects/Market AI

src/services/safetyFilter.ts text
/**
 * Safety Filter — dijalankan sebelum setiap scan auto-trading.
 *
 * Filter ini memastikan bot tidak entry saat kondisi pasar berbahaya:
 * - Spread terlalu lebar (volatilitas ekstrem)
 * - Market sedang dalam rollover window (XAUUSD: 23:00–01:00 UTC)
 * - Daily loss sudah melebihi batas maksimum
 * - Sudah ada open posisi untuk simbol yang sama
 */

export interface SafetyCheckResult {
  safe: boolean;
  reasons: string[];
}

export interface SafetyFilterConfig {
  /** Maksimum spread dalam pips sebelum entry ditolak. Default: 50 */
  maxSpreadPips: number;
  /** Skip rollover window XAUUSD (23:00–01:00 UTC). Default: true */
  skipRolloverWindow: boolean;
  /** Maksimum daily loss (dalam USD) sebelum trading dihentikan */
  maxDailyLossAmount: number;
  /** Maksimum posisi terbuka per simbol. 0 = tidak ada batas. Default: 1 */
  maxOpenPositions: number;
}

export class SafetyFilter {
  public constructor(private readonly config: SafetyFilterConfig) {}
 
  public updateConfig(newConfig: Partial<SafetyFilterConfig>): void {
    Object.assign(this.config, newConfig);
  }

  /**
   * Cek semua kondisi keamanan.
   * @param symbolCode Kode simbol yang akan di-scan
   * @param currentSpreadPips Spread saat ini dalam pips
   * @param todayLossAmount Total kerugian hari ini (positif = rugi)
   * @param openPositionCount Jumlah posisi terbuka untuk simbol ini
   */
  public check(
    symbolCode: string,
    currentSpreadPips: number,
    todayLossAmount: number,
    openPositionCount: number
  ): SafetyCheckResult {
    const reasons: string[] = [];

    // 1. Spread check
    if (currentSpreadPips > this.config.maxSpreadPips) {
      reasons.push(
        `Spread ${currentSpreadPips.toFixed(1)} pips melebihi batas ${this.config.maxSpreadPips} pips — pasar terlalu volatile.`
      );
    }

    // 2. Rollover window check (berlaku untuk semua simbol forex/gold)
    if (this.config.skipRolloverWindow && isRolloverWindow()) {
      reasons.push(
        `Market sedang dalam rollover window (23:00–01:00 UTC) — skip entry ${symbolCode}.`
      );
    }

    // 3. Daily loss limit check (hanya aktif jika maxDailyLossAmount > 0)
    if (this.config.maxDailyLossAmount > 0 && todayLossAmount >= this.config.maxDailyLossAmount) {
      reasons.push(
        `Daily loss ${todayLossAmount.toFixed(2)} sudah mencapai batas ${this.config.maxDailyLossAmount.toFixed(2)} — trading dihentikan hari ini.`
      );
    }

    // 4. Open position limit untuk simbol ini (0 = tidak ada batas)
    const maxPos = this.config.maxOpenPositions;
    if (maxPos > 0 && openPositionCount >= maxPos) {
      reasons.push(
        `Sudah ada ${openPositionCount} posisi terbuka untuk ${symbolCode} (maks ${maxPos}) — skip entry baru.`
      );
    }

    return {
      safe: reasons.length === 0,
      reasons
    };
  }

  /**
   * Estimasi spread saat ini dari data candle terakhir (ATR-based).
   * Jika tidak ada data live, gunakan defaultSpreadPips dari symbol config.
   * @param atr Average True Range dari candle terakhir
   * @param pipSize Ukuran 1 pip untuk simbol ini
   * @param defaultSpreadPips Spread default jika ATR tidak tersedia
   */
  public static estimateSpreadPips(
    atr: number,
    pipSize: number,
    defaultSpreadPips: number
  ): number {
    if (atr <= 0 || pipSize <= 0) {
      return defaultSpreadPips;
    }

    // ATR yang sangat tinggi biasanya terjadi saat spread melebar juga
    // Gunakan 5% dari ATR sebagai estimasi spread dalam pips
    const atrBasedEstimate = (atr * 0.05) / pipSize;
    return Math.max(atrBasedEstimate, defaultSpreadPips);
  }
}

/**
 * Cek apakah sekarang adalah rollover window XAUUSD/Forex.
 * Rollover terjadi antara 23:00–01:00 UTC setiap hari kerja.
 */
function isRolloverWindow(): boolean {
  const now = new Date();
  const hourUtc = now.getUTCHours();
  const minuteUtc = now.getUTCMinutes();
  const decimalHour = hourUtc + minuteUtc / 60;

  // 23:00 UTC hingga 01:00 UTC (melewati tengah malam)
  return decimalHour >= 23 || decimalHour < 1;
}