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