Bot & Automation

Treding Forex Ai

/root/hermes-projects/Treding Forex Ai

src/config/env.ts text
import "dotenv/config";
import { z } from "zod";
import type {
  BrokerAccountType,
  BrokerExecutionMode,
  Mt5BridgeConnectionConfig,
  BrokerProviderName,
  MarketDataProviderName,
  TradingMode,
  TradingRuntimeMode
} from "../domain/types";

const booleanFromEnv = z.preprocess((value) => {
  if (typeof value !== "string") {
    return value;
  }

  const normalized = value.trim().toLowerCase();
  if (["true", "1", "yes", "y"].includes(normalized)) {
    return true;
  }

  if (["false", "0", "no", "n", ""].includes(normalized)) {
    return false;
  }

  return value;
}, z.boolean());

const optionalNumber = (fallback: number) =>
  z.preprocess((value) => {
    if (value === undefined || value === null || value === "") {
      return fallback;
    }

    return Number(value);
  }, z.number().finite().positive());

const optionalNonNegativeInteger = (fallback: number) =>
  z.preprocess((value) => {
    if (value === undefined || value === null || value === "") {
      return fallback;
    }

    return Number(value);
  }, z.number().int().min(0));

const optionalNonNegativeNumber = (fallback: number) =>
  z.preprocess((value) => {
    if (value === undefined || value === null || value === "") {
      return fallback;
    }

    return Number(value);
  }, z.number().finite().min(0));

const optionalPositiveInteger = (fallback: number, min = 1) =>
  z.preprocess((value) => {
    if (value === undefined || value === null || value === "") {
      return fallback;
    }

    return Number(value);
  }, z.number().int().min(min));

const envSchema = z.object({
  NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
  BOT_DISPLAY_NAME: z.string().min(1).default("Treding Forex Ai"),
  TELEGRAM_BOT_TOKEN: z.string().optional(),
  TRADING_MODE: z.enum(["paper", "semi_auto", "full_auto"]).default("paper"),
  ENABLE_REAL_TRADING: booleanFromEnv.default(false),
  ACCOUNT_BALANCE: optionalNumber(1000),
  RISK_PER_TRADE_PERCENT: optionalNumber(1),
  MAX_DAILY_LOSS_PERCENT: optionalNumber(3),
  MAX_OPEN_TRADES: optionalNonNegativeInteger(3),
  MAX_LOT_PER_ORDER: optionalNumber(0.01),
  MARKET_DATA_PROVIDER: z.enum(["demo", "twelvedata", "mt5_bridge"]).default("demo"),
  TWELVEDATA_API_KEY: z.string().optional(),
  DATA_DIR: z.string().min(1).default("./data"),
  ALLOWED_TELEGRAM_USER_IDS: z.string().optional(),
  BROKER_PROVIDER: z.enum(["disabled", "paper", "mt5_bridge"]).default("disabled"),
  BROKER_API_KEY: z.string().optional(),
  BROKER_ACCOUNT_ID: z.string().optional(),
  BROKER_BASE_URL: z.string().optional(),
  MT5_BRIDGE_URL: z.string().url().optional().or(z.literal("")),
  MT5_BRIDGE_TOKEN: z.string().optional(),
  MT5_BRIDGE_DEMO_URL: z.string().url().optional().or(z.literal("")),
  MT5_BRIDGE_DEMO_TOKEN: z.string().optional(),
  MT5_BRIDGE_REAL_URL: z.string().url().optional().or(z.literal("")),
  MT5_BRIDGE_REAL_TOKEN: z.string().optional(),
  EXNESS_MT5_LOGIN: z.string().optional(),
  EXNESS_MT5_SERVER: z.string().optional(),
  EXNESS_ACCOUNT_TYPE: z.enum(["demo", "real"]).default("demo"),
  AUTO_TRADING_ENABLED: booleanFromEnv.default(false),
  AUTO_TRADE_SYMBOLS: z.string().default("XAUUSD"),
  AUTO_TRADE_MODE: z
    .enum(["scalping", "intraday", "swing", "conservative", "aggressive"])
    .default("intraday"),
  AUTO_SCAN_INTERVAL_SECONDS: optionalPositiveInteger(300, 5),
  AUTO_TRADE_COOLDOWN_SECONDS: optionalNonNegativeInteger(45),
  AUTO_TRADE_COOLDOWN_MINUTES: optionalNumber(1),
  AUTO_MONITOR_INTERVAL_SECONDS: optionalPositiveInteger(30, 5),
  AUTO_MIN_CONFIDENCE: optionalPositiveInteger(70, 1),
  AUTO_MAX_SPREAD_PIPS: optionalNumber(50),
  AUTO_SKIP_ROLLOVER_WINDOW: booleanFromEnv.default(true),
  AUTO_TRADE_ACCOUNT_MODE: z.enum(["demo", "real"]).default("demo"),
  AUTO_TRADE_STRATEGY: z
    .enum(["fast_scalping", "trend_scalping", "custom"])
    .default("trend_scalping"),
  AUTO_TRADE_LOT: z
    .preprocess((value) => (value === undefined || value === null || value === "" ? undefined : Number(value)), z.number().finite().positive())
    .optional(),
  AUTO_TRADE_TARGET_PROFIT_PIPS: z
    .preprocess((value) => (value === undefined || value === null || value === "" ? undefined : Number(value)), z.number().finite().positive())
    .optional(),
  AUTO_TRADE_STOP_LOSS_PIPS: z
    .preprocess((value) => (value === undefined || value === null || value === "" ? undefined : Number(value)), z.number().finite().positive())
    .optional(),
  AUTO_TRADE_DAILY_LOSS_LIMIT_PERCENT: z
    .preprocess((value) => (value === undefined || value === null || value === "" ? undefined : Number(value)), z.number().finite().positive())
    .optional(),
  AUTO_MAX_CONSECUTIVE_LOSSES: optionalPositiveInteger(2, 1),
  AUTO_LOSS_COOLDOWN_SECONDS: optionalPositiveInteger(900, 60),
  AUTO_MIN_REENTRY_DELAY_SECONDS: optionalPositiveInteger(60, 5),
  AUTO_MAX_SPREAD_TARGET_RATIO: optionalNonNegativeNumber(0),
  AUTO_MIN_REWARD_RISK: optionalNonNegativeNumber(0),
  AUTO_MAX_CONFIGURABLE_POSITIONS: optionalPositiveInteger(5, 1),
  AUTO_TRADE_CHAT_ID: z.string().optional(),
  AUTO_TRADE_USER_ID: z.string().optional()
});

const parsedEnv = envSchema.parse(process.env);

export const env = {
  nodeEnv: parsedEnv.NODE_ENV,
  botDisplayName: parsedEnv.BOT_DISPLAY_NAME,
  telegramBotToken: parsedEnv.TELEGRAM_BOT_TOKEN,
  tradingMode: parsedEnv.TRADING_MODE as TradingRuntimeMode,
  enableRealTrading: parsedEnv.ENABLE_REAL_TRADING,
  marketDataProvider: parsedEnv.MARKET_DATA_PROVIDER as MarketDataProviderName,
  twelveDataApiKey: parsedEnv.TWELVEDATA_API_KEY,
  dataDir: parsedEnv.DATA_DIR,
  allowedTelegramUserIds: parseAllowedUserIds(parsedEnv.ALLOWED_TELEGRAM_USER_IDS),
  brokerProvider: parsedEnv.BROKER_PROVIDER as BrokerProviderName,
  brokerApiKey: parsedEnv.BROKER_API_KEY,
  brokerAccountId: parsedEnv.BROKER_ACCOUNT_ID,
  brokerBaseUrl: parsedEnv.BROKER_BASE_URL,
  exness: {
    mt5BridgeUrl: normalizeOptional(parsedEnv.MT5_BRIDGE_URL),
    mt5BridgeToken: parsedEnv.MT5_BRIDGE_TOKEN,
    mt5BridgeDemoUrl:
      normalizeOptional(parsedEnv.MT5_BRIDGE_DEMO_URL) ??
      normalizeOptional(parsedEnv.MT5_BRIDGE_URL),
    mt5BridgeDemoToken: parsedEnv.MT5_BRIDGE_DEMO_TOKEN ?? parsedEnv.MT5_BRIDGE_TOKEN,
    mt5BridgeRealUrl:
      normalizeOptional(parsedEnv.MT5_BRIDGE_REAL_URL) ??
      normalizeOptional(parsedEnv.MT5_BRIDGE_URL),
    mt5BridgeRealToken: parsedEnv.MT5_BRIDGE_REAL_TOKEN ?? parsedEnv.MT5_BRIDGE_TOKEN,
    mt5Login: parsedEnv.EXNESS_MT5_LOGIN,
    mt5Server: parsedEnv.EXNESS_MT5_SERVER,
    accountType: parsedEnv.EXNESS_ACCOUNT_TYPE as BrokerAccountType,
    bridgeConnections: buildBridgeConnections({
      demoUrl:
        normalizeOptional(parsedEnv.MT5_BRIDGE_DEMO_URL) ??
        normalizeOptional(parsedEnv.MT5_BRIDGE_URL),
      demoToken: parsedEnv.MT5_BRIDGE_DEMO_TOKEN ?? parsedEnv.MT5_BRIDGE_TOKEN,
      realUrl:
        normalizeOptional(parsedEnv.MT5_BRIDGE_REAL_URL) ??
        normalizeOptional(parsedEnv.MT5_BRIDGE_URL),
      realToken: parsedEnv.MT5_BRIDGE_REAL_TOKEN ?? parsedEnv.MT5_BRIDGE_TOKEN
    })
  },
  risk: {
    accountBalance: parsedEnv.ACCOUNT_BALANCE,
    riskPerTradePercent: parsedEnv.RISK_PER_TRADE_PERCENT,
    maxDailyLossPercent: parsedEnv.MAX_DAILY_LOSS_PERCENT,
    maxOpenTrades: parsedEnv.MAX_OPEN_TRADES,
    maxLotPerOrder: parsedEnv.MAX_LOT_PER_ORDER
  },
  autoTrading: {
    enabled: parsedEnv.AUTO_TRADING_ENABLED,
    symbols: parseSymbolList(parsedEnv.AUTO_TRADE_SYMBOLS),
    mode: parsedEnv.AUTO_TRADE_MODE as TradingMode,
    scanIntervalSeconds: parsedEnv.AUTO_SCAN_INTERVAL_SECONDS,
    cooldownSeconds: parsedEnv.AUTO_TRADE_COOLDOWN_SECONDS,
    monitorIntervalSeconds: parsedEnv.AUTO_MONITOR_INTERVAL_SECONDS,
    minConfidence: parsedEnv.AUTO_MIN_CONFIDENCE,
    maxSpreadPips: parsedEnv.AUTO_MAX_SPREAD_PIPS,
    skipRolloverWindow: parsedEnv.AUTO_SKIP_ROLLOVER_WINDOW,
    maxOpenPositions: parsedEnv.MAX_OPEN_TRADES, // 0 = tidak ada batas
    accountMode: parsedEnv.AUTO_TRADE_ACCOUNT_MODE as BrokerExecutionMode,
    strategy: parsedEnv.AUTO_TRADE_STRATEGY,
    lot: parsedEnv.AUTO_TRADE_LOT,
    targetProfitPips: parsedEnv.AUTO_TRADE_TARGET_PROFIT_PIPS,
    stopLossPips: parsedEnv.AUTO_TRADE_STOP_LOSS_PIPS,
    dailyLossLimitPercent: parsedEnv.AUTO_TRADE_DAILY_LOSS_LIMIT_PERCENT,
    maxConsecutiveLosses: parsedEnv.AUTO_MAX_CONSECUTIVE_LOSSES,
    lossCooldownSeconds: parsedEnv.AUTO_LOSS_COOLDOWN_SECONDS,
    minReentryDelaySeconds: parsedEnv.AUTO_MIN_REENTRY_DELAY_SECONDS,
    maxSpreadTargetRatio: parsedEnv.AUTO_MAX_SPREAD_TARGET_RATIO,
    minRewardRisk: parsedEnv.AUTO_MIN_REWARD_RISK,
    maxConfigurablePositions: parsedEnv.AUTO_MAX_CONFIGURABLE_POSITIONS,
    chatId:
      parseOptionalPositiveInteger(parsedEnv.AUTO_TRADE_CHAT_ID) ??
      firstAllowedUserId(parsedEnv.ALLOWED_TELEGRAM_USER_IDS),
    userId:
      parseOptionalPositiveInteger(parsedEnv.AUTO_TRADE_USER_ID) ??
      firstAllowedUserId(parsedEnv.ALLOWED_TELEGRAM_USER_IDS)
  }
};

function normalizeOptional(value: string | undefined): string | undefined {
  if (!value?.trim()) {
    return undefined;
  }

  return value.trim();
}

function parseAllowedUserIds(value: string | undefined): Set<number> {
  if (!value?.trim()) {
    return new Set();
  }

  return new Set(
    value
      .split(",")
      .map((item) => Number(item.trim()))
      .filter((item) => Number.isInteger(item) && item > 0)
  );
}

function firstAllowedUserId(value: string | undefined): number | undefined {
  return [...parseAllowedUserIds(value)].at(0);
}

function parseOptionalPositiveInteger(value: string | undefined): number | undefined {
  if (!value?.trim()) {
    return undefined;
  }

  const parsed = Number(value.trim());
  return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
}

function parseSymbolList(value: string): string[] {
  return value
    .split(",")
    .map((item) => item.trim().toUpperCase())
    .filter(Boolean);
}

function buildBridgeConnections(params: {
  demoUrl?: string;
  demoToken?: string;
  realUrl?: string;
  realToken?: string;
}): Mt5BridgeConnectionConfig[] {
  const connections: Mt5BridgeConnectionConfig[] = [];

  if (params.demoUrl && params.demoToken) {
    connections.push({
      mode: "demo",
      url: params.demoUrl,
      token: params.demoToken
    });
  }

  if (params.realUrl && params.realToken) {
    connections.push({
      mode: "real",
      url: params.realUrl,
      token: params.realToken
    });
  }

  return connections;
}