Created
April 27, 2025 07:51
-
-
Save zallesov/fa94d504e69c65de138a51250062c13a to your computer and use it in GitHub Desktop.
Meta API Connector
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import MetaApi from 'metaapi.cloud-sdk'; | |
| import { Order } from './types/Order'; | |
| import { PnL } from './types/PnL'; | |
| import { Position } from './types/Position'; | |
| import { error, log } from './logging'; | |
| import { ConditionalOrderProps, IBroker, SubmitFuturesOrderProps } from './IBroker'; | |
| import { MetatraderAccount, RpcMetaApiConnectionInstance } from 'metaapi.cloud-sdk'; | |
| export class MetaAPIBroker implements IBroker { | |
| private api: MetaApi; | |
| private account!: MetatraderAccount; // MetaTrader account type from SDK | |
| private connection!: RpcMetaApiConnectionInstance; // RPC connection type from SDK | |
| private static clients: Map<string, MetaAPIBroker> = new Map(); | |
| static getClient(token: string, login: string, password: string, serverName: string) { | |
| if (!this.clients.has(token)) { | |
| this.clients.set(token, new MetaAPIBroker(token, login, password, serverName)); | |
| } | |
| return this.clients.get(token)!; | |
| } | |
| constructor(private token: string, private login: string, private password: string, private serverName: string) { | |
| this.api = new MetaApi(this.token); | |
| } | |
| async connect() { | |
| log('Connecting to MetaApi with account:', this.login); | |
| if (!this.account) { | |
| const accounts = await this.api.metatraderAccountApi.getAccountsWithInfiniteScrollPagination(); | |
| this.account = accounts.find(a => a.login === this.login && a.type.startsWith('cloud'))!; | |
| if (!this.account) { | |
| log('Adding MT5 account to MetaApi'); | |
| // @ts-ignore | |
| this.account = await this.api.metatraderAccountApi.createAccount({ | |
| name: 'Test account', | |
| type: 'cloud' as any, | |
| login: this.login, | |
| password: this.password, | |
| server: this.serverName, | |
| platform: 'mt5', | |
| magic: 1000, | |
| }); | |
| } else { | |
| log('MT5 account already added to MetaApi', this.account.id); | |
| } | |
| } | |
| // wait until account is deployed and connected to broker | |
| // eslint-disable-next-line semi | |
| log('Deploying account'); | |
| await this.account.deploy(); | |
| // eslint-disable-next-line semi | |
| log('Waiting for API server to connect to broker (may take couple of minutes)'); | |
| await this.account.waitConnected(); | |
| // connect to MetaApi API | |
| this.connection = this.account.getRPCConnection(); | |
| await this.connection.connect(); | |
| // wait until terminal state synchronized to the local state | |
| log('Waiting for SDK to synchronize to terminal state (may take some time depending on your history size)'); | |
| await this.connection.waitSynchronized(); | |
| log('Connected to MetaApi with account:', this.account.id); | |
| } | |
| async getAccountCurrencyExchange(quoteCurrency: string, accountCurrency: string): Promise<number> { | |
| log('Fetching account currency exchange rate for:', { quoteCurrency, accountCurrency }); | |
| const rate = await this.connection.getSymbolPrice(`${accountCurrency}${quoteCurrency}`, false); | |
| log('Fetched account currency exchange rate:', rate); | |
| return 1 / ((rate.ask + rate.bid) / 2); | |
| } | |
| async getCurrentPrice(symbol: string): Promise<number> { | |
| log('Fetching current price for symbol:', symbol); | |
| const price = await this.connection.getSymbolPrice(symbol, false); | |
| log('Fetched current price:', price); | |
| return (price.ask + price.bid) / 2; | |
| } | |
| async getAvailableBalance(coin: string): Promise<number> { | |
| log('Fetching available balance for coin:', coin); | |
| const balance = await this.connection.getAccountInformation(); | |
| log('Fetched available balance:', balance.balance); | |
| return balance.balance; | |
| } | |
| async getOpenPositions(symbol: string): Promise<Position[]> { | |
| log('Fetching open positions for symbol:', symbol); | |
| const positions = await this.connection.getPositions(); | |
| log('Fetched open positions:', positions); | |
| return positions | |
| .filter(pos => pos.symbol === symbol) | |
| .map(position => ({ | |
| symbol: position.symbol, | |
| side: position.type === 'POSITION_TYPE_BUY' ? 'Buy' : 'Sell', | |
| leverage: 1, | |
| size: position.volume, | |
| avgPrice: position.openPrice, | |
| positionValue: position.volume * position.openPrice, | |
| createdTime: new Date(position.time), | |
| })); | |
| } | |
| async getOpenOrders(symbol: string): Promise<Order[] | undefined> { | |
| log('Fetching open orders for symbol:', symbol); | |
| const orders = await this.connection.getOrders(); | |
| log('Fetched open orders:', orders); | |
| return orders | |
| .filter(order => order.symbol === symbol) | |
| .map(order => ({ | |
| symbol: order.symbol, | |
| side: order.type.includes('BUY') ? 'Buy' : 'Sell', | |
| quantity: order.volume, | |
| orderType: order.type.includes('LIMIT') ? 'Limit' : 'Market', | |
| limitPrice: order.openPrice, | |
| orderId: order.id.toString(), | |
| status: 'submitted', | |
| executionTime: new Date(order.time), | |
| leverage: 1, | |
| stopLoss: order.stopLoss, | |
| takeProfit: order.takeProfit, | |
| })); | |
| } | |
| async getOpenOrder(symbol: string, side: 'buy' | 'sell'): Promise<Order | undefined> { | |
| log('Fetching open order for symbol and side:', { symbol, side }); | |
| const order = await this.getOpenOrders(symbol); | |
| log('Fetched open order:', order); | |
| return order?.find(order => order.side.toLowerCase() === side.toLowerCase()); | |
| } | |
| async cancelOrder(orderId: string, symbol: string): Promise<any> { | |
| log('Cancelling order with ID:', orderId); | |
| const response = await this.connection.cancelOrder(orderId); | |
| log('Cancelled order response:', response); | |
| return response; | |
| } | |
| async submitConditionalOrder(props: ConditionalOrderProps): Promise<Order> { | |
| log('Submitting conditional order with props:', props); | |
| try { | |
| let response; | |
| if (props.side === 'Buy') { | |
| response = await this.connection.createStopLimitBuyOrder( | |
| props.symbol, | |
| Number(props.quantity), | |
| Number(props.price), | |
| Number(props.triggerPrice), | |
| Number(props.stopLoss), | |
| Number(props.takeProfit), | |
| ); | |
| } else { | |
| response = await this.connection.createStopLimitSellOrder( | |
| props.symbol, | |
| Number(props.quantity), | |
| Number(props.price), | |
| Number(props.triggerPrice), | |
| Number(props.stopLoss), | |
| Number(props.takeProfit), | |
| ); | |
| } | |
| const currentPrice = await this.getCurrentPrice(props.symbol); | |
| log('Submitted conditional order response:', response); | |
| return { | |
| symbol: props.symbol, | |
| side: props.side, | |
| quantity: Number(props.quantity), | |
| leverage: props.leverage, | |
| orderType: 'Limit', | |
| limitPrice: Number(props.price), | |
| triggerPrice: Number(props.triggerPrice), | |
| currentPrice, | |
| orderId: response.orderId, | |
| stopLoss: props.stopLoss ? Number(props.stopLoss) : undefined, | |
| takeProfit: props.takeProfit ? Number(props.takeProfit) : undefined, | |
| executionTime: new Date(), | |
| status: 'submitted', | |
| response | |
| }; | |
| } catch (err) { | |
| error('MetaAPI failed to submit conditional order', err); | |
| throw err; | |
| } | |
| } | |
| async submitFuturesOrder(props: SubmitFuturesOrderProps): Promise<Order> { | |
| log('Submitting futures order with props:', props); | |
| try { | |
| const response = await this.connection.createLimitBuyOrder( | |
| props.symbol, | |
| Number(props.quantity), | |
| Number(props.price), | |
| props.stopLoss ? Number(props.stopLoss) : undefined, | |
| props.takeProfit ? Number(props.takeProfit) : undefined, | |
| ); | |
| const currentPrice = await this.getCurrentPrice(props.symbol); | |
| log('Submitted futures order response:', response); | |
| return { | |
| symbol: props.symbol, | |
| side: props.side, | |
| quantity: Number(props.quantity), | |
| leverage: props.leverage, | |
| orderType: props.type, | |
| limitPrice: props.price ? Number(props.price) : undefined, | |
| currentPrice, | |
| orderId: "orderId", | |
| stopLoss: props.stopLoss ? Number(props.stopLoss) : undefined, | |
| takeProfit: props.takeProfit ? Number(props.takeProfit) : undefined, | |
| executionTime: new Date(), | |
| status: 'submitted', | |
| reduceOnly: props.reduceOnly, | |
| response: "response" | |
| }; | |
| } catch (err) { | |
| error('MetaAPI failed to submit futures order', err); | |
| throw err; | |
| } | |
| } | |
| async getPnl(symbol: string): Promise<PnL> { | |
| log('Fetching PnL for symbol:', symbol); | |
| const deals = await this.connection.getDealsByTimeRange( | |
| new Date(Date.now() - 24 * 60 * 60 * 1000), // Last 24 hours | |
| new Date() | |
| ); | |
| const filteredDeals = deals[0] //.filter(deal => deal.symbol === symbol); | |
| if (filteredDeals.length === 0) { | |
| return { | |
| symbol, | |
| closedPnl: 0, | |
| side: 'Buy', | |
| leverage: 1, | |
| orderId: '', | |
| cumEntryValue: 0, | |
| avgEntryPrice: 0, | |
| cumExitValue: 0, | |
| avgExitPrice: 0, | |
| closedSize: 0 | |
| }; | |
| } | |
| return filteredDeals.reduce((acc, deal) => ({ | |
| symbol, | |
| side: deal.type.includes('BUY') ? 'Buy' : 'Sell', | |
| leverage: deal.leverage || 1, | |
| orderId: deal.orderId, | |
| closedPnl: acc.closedPnl + deal.profit, | |
| cumEntryValue: acc.cumEntryValue + (deal.volume * deal.entryPrice), | |
| avgEntryPrice: (acc.avgEntryPrice + deal.entryPrice) / 2, | |
| cumExitValue: acc.cumExitValue + (deal.volume * deal.exitPrice), | |
| avgExitPrice: (acc.avgExitPrice + deal.exitPrice) / 2, | |
| closedSize: acc.closedSize + deal.volume | |
| }), { | |
| symbol, | |
| closedPnl: 0, | |
| side: 'Buy' as const, | |
| leverage: 1, | |
| orderId: '', | |
| cumEntryValue: 0, | |
| avgEntryPrice: 0, | |
| cumExitValue: 0, | |
| avgExitPrice: 0, | |
| closedSize: 0 | |
| }); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment