Bot & Automation
Market AI
/root/hermes-projects/Market AI
src/config/env.ts
text
import "dotenv/config";
import { z } from "zod";
import type {
BrokerAccountType,
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 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"]).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(),
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: optionalPositiveInteger(45, 10),
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_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,
mt5Login: parsedEnv.EXNESS_MT5_LOGIN,
mt5Server: parsedEnv.EXNESS_MT5_SERVER,
accountType: parsedEnv.EXNESS_ACCOUNT_TYPE as BrokerAccountType
},
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
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);
}