├── docs ├── bfb-logo.png ├── xtb-logo.png └── API Documentation_files │ ├── logo-small.png │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ ├── glyphicons-halflings.png │ ├── fontawesome-webfont_iefix.eot │ ├── jquery-scrollTo.min.js.download │ ├── documentation.css │ ├── bootstrap-responsive.min.css │ └── bootstrap.min.js.download ├── .prettierrc ├── src ├── v2 │ ├── utils │ │ ├── getUTCTimestampString.ts │ │ ├── sleep.ts │ │ ├── Object.ts │ │ ├── formatNumber.ts │ │ ├── Increment.ts │ │ ├── getObjectChanges.ts │ │ ├── parseCustomTag.ts │ │ ├── createPromise.ts │ │ ├── Counter.ts │ │ ├── getPositionType.ts │ │ ├── Time.ts │ │ ├── Logger.ts │ │ ├── parseJsonLogin.ts │ │ ├── Timer.ts │ │ ├── Listener.ts │ │ └── WebSocketWrapper.ts │ ├── interface │ │ ├── Interface.ts │ │ ├── Request.ts │ │ ├── Response.ts │ │ ├── Enum.ts │ │ └── Definitions.ts │ └── core │ │ ├── Trading │ │ ├── LimitPosition.ts │ │ ├── OpenPosition.ts │ │ └── Trading.ts │ │ ├── Transaction.ts │ │ ├── TradeRecord.ts │ │ ├── Stream │ │ ├── Stream.ts │ │ ├── StreamConnections.ts │ │ └── StreamConnection.ts │ │ ├── Socket │ │ ├── SocketConnections.ts │ │ ├── SocketConnection.ts │ │ └── Socket.ts │ │ └── XAPI.ts └── index.ts ├── .gitignore ├── .npmignore ├── test └── v2 │ ├── test │ ├── connectionTest.ts │ ├── subscribeTest.ts │ ├── messageQueuStressTest.ts │ ├── getCandlesTest.ts │ └── tradeTest.ts │ ├── parseLoginFile.ts │ ├── sandbox.ts │ └── tests.ts ├── tsconfig.json ├── .eslintrc.json ├── esbuild.config.js ├── LICENSE ├── package.json └── README.md /docs/bfb-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterszombati/xapi-node/HEAD/docs/bfb-logo.png -------------------------------------------------------------------------------- /docs/xtb-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterszombati/xapi-node/HEAD/docs/xtb-logo.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { "singleQuote": true, "semi": false, "printWidth": 120, "arrowParens": "avoid" } 2 | -------------------------------------------------------------------------------- /src/v2/utils/getUTCTimestampString.ts: -------------------------------------------------------------------------------- 1 | export function getUTCTimestampString(): string { 2 | return new Date().getTime().toString() 3 | } -------------------------------------------------------------------------------- /docs/API Documentation_files/logo-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterszombati/xapi-node/HEAD/docs/API Documentation_files/logo-small.png -------------------------------------------------------------------------------- /docs/API Documentation_files/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterszombati/xapi-node/HEAD/docs/API Documentation_files/fontawesome-webfont.eot -------------------------------------------------------------------------------- /docs/API Documentation_files/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterszombati/xapi-node/HEAD/docs/API Documentation_files/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /docs/API Documentation_files/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterszombati/xapi-node/HEAD/docs/API Documentation_files/fontawesome-webfont.woff -------------------------------------------------------------------------------- /docs/API Documentation_files/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterszombati/xapi-node/HEAD/docs/API Documentation_files/glyphicons-halflings.png -------------------------------------------------------------------------------- /src/v2/utils/sleep.ts: -------------------------------------------------------------------------------- 1 | export function sleep(ms: number) { 2 | return new Promise((resolve) => { 3 | setTimeout(() => resolve(undefined), ms) 4 | }) 5 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /node_modules 3 | /package-lock.json 4 | sensitive*.json 5 | sensitive.json 6 | /sensitive 7 | /logs 8 | 9 | /build 10 | /pnpm-lock.yaml 11 | -------------------------------------------------------------------------------- /docs/API Documentation_files/fontawesome-webfont_iefix.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterszombati/xapi-node/HEAD/docs/API Documentation_files/fontawesome-webfont_iefix.eot -------------------------------------------------------------------------------- /src/v2/utils/Object.ts: -------------------------------------------------------------------------------- 1 | export function isEmpty(obj: Record) { 2 | for (const prop in obj) { 3 | if (obj.hasOwnProperty(prop)) 4 | return false 5 | } 6 | return true 7 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /node_modules 3 | /package-lock.json 4 | sensitive*.json 5 | sensitive.json 6 | /sensitive 7 | /logs 8 | 9 | /react-test 10 | /src 11 | /docs 12 | /build/test 13 | /sandbox 14 | /test -------------------------------------------------------------------------------- /src/v2/utils/formatNumber.ts: -------------------------------------------------------------------------------- 1 | export function formatNumber(number: number, length: number): string { 2 | const result = number.toString() 3 | return length - result.length > 0 4 | ? '0'.repeat(length - result.length) + result 5 | : result 6 | } -------------------------------------------------------------------------------- /src/v2/utils/Increment.ts: -------------------------------------------------------------------------------- 1 | export class Increment { 2 | private _id: number = -1 3 | public get id() { 4 | this._id += 1 5 | if (this._id > 1000) { 6 | return this._id = 1 7 | } 8 | return this._id 9 | } 10 | } -------------------------------------------------------------------------------- /test/v2/test/connectionTest.ts: -------------------------------------------------------------------------------- 1 | import { XAPI } from '../../../src/v2/core/XAPI' 2 | 3 | export function connectionTest(x: XAPI): Promise { 4 | return new Promise((resolve, reject) => { 5 | try { 6 | resolve() 7 | } catch (e) { 8 | reject(e) 9 | } 10 | }) 11 | } -------------------------------------------------------------------------------- /src/v2/utils/getObjectChanges.ts: -------------------------------------------------------------------------------- 1 | export function getObjectChanges(from: Record, to: Record) { 2 | const obj: Record = {} 3 | 4 | for (const [key, value] of Object.entries(from)) { 5 | if (value !== to[key]) { 6 | obj[key] = to[key] 7 | } 8 | } 9 | 10 | return obj 11 | } -------------------------------------------------------------------------------- /test/v2/test/subscribeTest.ts: -------------------------------------------------------------------------------- 1 | import { XAPI } from '../../../src/v2/core/XAPI' 2 | 3 | export function subscribeTest(x: XAPI): Promise { 4 | return new Promise((resolve, reject) => { 5 | try { 6 | x.Stream.listen.getKeepAlive(data => { 7 | return resolve() 8 | }) 9 | x.Stream.subscribe.getKeepAlive() 10 | } catch (e) { 11 | reject(e) 12 | } 13 | }) 14 | } -------------------------------------------------------------------------------- /test/v2/parseLoginFile.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import {parseJsonLogin} from '../../src/v2/utils/parseJsonLogin' 3 | import {XAPIConfig} from '../../src/v2/core/XAPI' 4 | 5 | export function parseLoginFile(loginJsonFile: string): XAPIConfig { 6 | if (!fs.existsSync(loginJsonFile)) { 7 | throw `${loginJsonFile} is not exists.` 8 | } 9 | return parseJsonLogin(fs.readFileSync(loginJsonFile).toString()) 10 | } -------------------------------------------------------------------------------- /src/v2/utils/parseCustomTag.ts: -------------------------------------------------------------------------------- 1 | export function parseCustomTag(customTag: string | null): { transactionId: string | null; command: string | null } { 2 | if (!customTag) { 3 | return {transactionId: null, command: null} 4 | } 5 | const [command,transactionId] = customTag.split('_') 6 | if (!transactionId) { 7 | return {transactionId: null, command: null} 8 | } 9 | return {transactionId, command} 10 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "noImplicitAny": true, 5 | "removeComments": true, 6 | "preserveConstEnums": true, 7 | "sourceMap": true, 8 | "target": "es6", 9 | "outDir": "build", 10 | "declaration": true, 11 | "strictNullChecks": true, 12 | "skipLibCheck": true 13 | }, 14 | "include": [ 15 | "src/**/*" 16 | ], 17 | "exclude": [ 18 | ".idea", 19 | "node_modules", 20 | "**/*.spec.ts", 21 | "**/*.d.ts" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /src/v2/interface/Interface.ts: -------------------------------------------------------------------------------- 1 | import {TradeRecord} from "../core/TradeRecord" 2 | import {Time} from "../utils/Time" 3 | import {REQUEST_STATUS_FIELD} from "./Enum" 4 | 5 | export interface TradePositions { 6 | [position: number]: { 7 | value: TradeRecord | null 8 | lastUpdated: Time 9 | } 10 | } 11 | 12 | export interface TradeStatus { 13 | customComment: string | null 14 | message: string | null 15 | order: number 16 | requestStatus: REQUEST_STATUS_FIELD | null 17 | } -------------------------------------------------------------------------------- /src/v2/utils/createPromise.ts: -------------------------------------------------------------------------------- 1 | export type PromiseObject = { 2 | resolve: (data: T) => void 3 | reject: (err: E) => void 4 | promise: Promise 5 | } 6 | 7 | export function createPromise(): PromiseObject { 8 | let resolve, reject 9 | const promise = new Promise((_resolve, _reject) => { 10 | resolve = _resolve 11 | reject = _reject 12 | }) 13 | 14 | return { 15 | promise, 16 | // @ts-ignore 17 | resolve, 18 | // @ts-ignore 19 | reject, 20 | } 21 | } -------------------------------------------------------------------------------- /src/v2/utils/Counter.ts: -------------------------------------------------------------------------------- 1 | import {Listener} from "./Listener" 2 | import {Time} from "./Time" 3 | 4 | export class Counter extends Listener { 5 | on({ 6 | callback, 7 | key, 8 | }: ({ 9 | callback: (data: { key: string[], time: Time, count: number }) => void 10 | key?: string | null 11 | })) { 12 | return this.addListener( 'Counter', callback, key) 13 | } 14 | 15 | count(key: string[], count: number = 1): void { 16 | this.callListener('Counter', [{ 17 | key: key.length === 1 ? ['data', key[0]] : key, 18 | time:new Time(), 19 | count 20 | }]) 21 | } 22 | } -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], 7 | "overrides": [], 8 | "parser": "@typescript-eslint/parser", 9 | "parserOptions": { 10 | "ecmaVersion": "latest", 11 | "sourceType": "module" 12 | }, 13 | "plugins": ["@typescript-eslint"], 14 | "rules": { 15 | "linebreak-style": ["error", "unix"], 16 | "quotes": ["error", "single"], 17 | "semi": ["error", "never"], 18 | "@typescript-eslint/no-explicit-any": "off", 19 | "@typescript-eslint/no-unused-vars": "off" 20 | }, 21 | "ignorePatterns": ["**/*.js"] 22 | } 23 | -------------------------------------------------------------------------------- /src/v2/utils/getPositionType.ts: -------------------------------------------------------------------------------- 1 | import {CMD_FIELD, PositionType} from '../interface/Enum' 2 | 3 | export function getPositionType({ 4 | cmd, 5 | closed, 6 | close_time, 7 | }: { 8 | cmd: CMD_FIELD 9 | closed: boolean 10 | close_time: number 11 | }): PositionType { 12 | if (cmd === CMD_FIELD.SELL || cmd === CMD_FIELD.BUY) { 13 | return close_time === null && !closed 14 | ? PositionType.open 15 | : PositionType.closed 16 | } else { 17 | return cmd === CMD_FIELD.BALANCE || cmd === CMD_FIELD.CREDIT 18 | ? PositionType.source 19 | : PositionType.limit 20 | } 21 | } -------------------------------------------------------------------------------- /esbuild.config.js: -------------------------------------------------------------------------------- 1 | const esbuild = require('esbuild') 2 | const package = require('./package.json') 3 | 4 | console.log('external dependencies: ' + JSON.stringify(Object.keys(package.dependencies))) 5 | 6 | const formats = [ 7 | { format: 'cjs', extension: '.cjs' }, 8 | { format: 'esm', extension: '.mjs' }, 9 | ] 10 | 11 | for (const f of formats) 12 | esbuild 13 | .build({ 14 | entryPoints: ['src/index.ts'], 15 | bundle: true, 16 | external: Object.keys(package.dependencies), 17 | format: f.format, 18 | outfile: 'build/index' + f.extension, 19 | target: 'node16', 20 | define: { 21 | 'process.env.ES_TARGET': '"' + f.format + '"', 22 | }, 23 | }) 24 | .catch(e => { 25 | console.log(e) 26 | }) 27 | .then(() => console.log('Successfully bundled the package in the ' + f.format + ' format')) -------------------------------------------------------------------------------- /test/v2/test/messageQueuStressTest.ts: -------------------------------------------------------------------------------- 1 | import { XAPI } from '../../../src/v2/core/XAPI' 2 | import { Time } from '../../../src' 3 | 4 | export function messageQueuStressTest(x: XAPI): Promise { 5 | return new Promise(async (resolve, reject) => { 6 | try { 7 | let start: Time | null = null 8 | let received = 0 9 | 10 | x.Socket.listen.getVersion(_ => { 11 | console.log('Test: getVersion') 12 | received += 1 13 | if (received === 30) { 14 | console.log('Test: successful - 30th message arrived after ' + start?.elapsedMs() + 'ms') 15 | return resolve() 16 | } 17 | }) 18 | start = new Time() 19 | console.log('Test: started.') 20 | for (let i = 0; i < 30; i++) { 21 | x.Socket.send.getVersion() 22 | } 23 | } catch (e) { 24 | reject(e) 25 | } 26 | }) 27 | } -------------------------------------------------------------------------------- /test/v2/sandbox.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import { parseLoginFile } from './parseLoginFile' 3 | import { Writable } from 'stream' 4 | import {XAPI} from "../../src/v2/core/XAPI" 5 | 6 | describe('sandbox', () => { 7 | it('sandbox', async () => { 8 | const login = parseLoginFile(path.join(process.cwd(), 'sensitive', 'sensitive-demo-login.json')) 9 | 10 | const x = new XAPI({ ...login }) 11 | 12 | const info = new Writable() 13 | info._write = (chunk, encoding, next) => { 14 | console.log(chunk.toString()) 15 | next() 16 | } 17 | 18 | const error = new Writable() 19 | error._write = (chunk, encoding, next) => { 20 | console.error(chunk.toString()) 21 | next() 22 | } 23 | 24 | const debug = new Writable() 25 | debug._write = (chunk, encoding, next) => { 26 | console.log(chunk.toString()) 27 | next() 28 | } 29 | 30 | x.connect() 31 | 32 | return await new Promise(resolve => resolve()) 33 | }) 34 | }) -------------------------------------------------------------------------------- /src/v2/core/Trading/LimitPosition.ts: -------------------------------------------------------------------------------- 1 | import {XAPI} from '../XAPI' 2 | import {TradeRecord, TradeRecordParams} from '../TradeRecord' 3 | import {TYPE_FIELD} from '../../interface/Enum' 4 | 5 | export class LimitPosition extends TradeRecord { 6 | private XAPI: XAPI 7 | 8 | constructor(XAPI: XAPI, params: TradeRecordParams) { 9 | super(params) 10 | this.XAPI = XAPI 11 | } 12 | 13 | close(customComment = '') { 14 | return this.XAPI.trading.tradeTransaction({ 15 | order: this.order, 16 | symbol: this.symbol, 17 | type: TYPE_FIELD.DELETE, 18 | customComment: customComment || undefined, 19 | }) 20 | } 21 | 22 | modify(params: { tp?: number; sl?: number; price?: number; expiration?: number; customComment?: string }) { 23 | return this.XAPI.trading.tradeTransaction({ 24 | order: this.order, 25 | type: TYPE_FIELD.MODIFY, 26 | tp: params.tp, 27 | sl: params.sl, 28 | price: params.price, 29 | expiration: params.expiration, 30 | customComment: params.customComment, 31 | }) 32 | } 33 | } -------------------------------------------------------------------------------- /src/v2/utils/Time.ts: -------------------------------------------------------------------------------- 1 | const isNodeJS = typeof window === 'undefined' 2 | 3 | const calculateElapsedTime = isNodeJS ? ((time: [number, number]): number => { 4 | const hrtime = process.hrtime(time) 5 | return Math.floor(hrtime[0] * 1000 + hrtime[1] / 1000000) 6 | }) : ((time: [number, number]): number => performance.now() - time[0]) 7 | 8 | export class Time { 9 | protected unit: [number, number] 10 | protected UTCTimestamp: number 11 | 12 | constructor() { 13 | this.unit = isNodeJS ? process.hrtime() : [performance.now(), 0] 14 | this.UTCTimestamp = Date.now() 15 | return this 16 | } 17 | 18 | public getDifference(time: Time): number { 19 | return time.elapsedMs() - this.elapsedMs() 20 | } 21 | 22 | public get(): Date { 23 | return new Date(Date.now() - calculateElapsedTime(this.unit)) 24 | } 25 | 26 | public elapsedMs(): number { 27 | return calculateElapsedTime(this.unit) 28 | } 29 | 30 | public getUTC(): Date { 31 | return new Date(this.UTCTimestamp) 32 | } 33 | } -------------------------------------------------------------------------------- /src/v2/core/Trading/OpenPosition.ts: -------------------------------------------------------------------------------- 1 | import {XAPI} from '../XAPI' 2 | import {TradeRecord, TradeRecordParams} from '../TradeRecord' 3 | import {TYPE_FIELD} from '../../interface/Enum' 4 | 5 | export class OpenPosition extends TradeRecord { 6 | private XAPI: XAPI 7 | 8 | constructor(XAPI: XAPI, params: TradeRecordParams) { 9 | super(params) 10 | this.XAPI = XAPI 11 | } 12 | 13 | close(volume: number | undefined = undefined, customComment = '') { 14 | return this.XAPI.trading.tradeTransaction({ 15 | order: this.order, 16 | type: TYPE_FIELD.CLOSE, 17 | volume: volume === undefined ? this.volume : volume, 18 | symbol: this.symbol, 19 | price: 1, 20 | customComment: customComment || undefined, 21 | }) 22 | } 23 | 24 | modify(params: { tp?: number; sl?: number; offset?: number; customComment?: string }) { 25 | return this.XAPI.trading.tradeTransaction({ 26 | order: this.order, 27 | type: TYPE_FIELD.MODIFY, 28 | tp: params.tp, 29 | sl: params.sl, 30 | offset: params.offset, 31 | customComment: params.customComment || undefined, 32 | }) 33 | } 34 | } -------------------------------------------------------------------------------- /test/v2/test/getCandlesTest.ts: -------------------------------------------------------------------------------- 1 | import {XAPI} from '../../../src/v2/core/XAPI' 2 | import {PERIOD_FIELD} from '../../../src' 3 | 4 | export function getCandlesTest(x: XAPI): Promise { 5 | return new Promise(async (resolve, reject) => { 6 | try { 7 | const isEURUSDclosed = x.Time?.getUTCDay() === 6 || (x.Time?.getUTCDay() === 0 && x.Time.getUTCHours() < 22) 8 | || (x.Time?.getUTCDay() === 5 && x.Time.getUTCHours() >= 21) 9 | 10 | if (isEURUSDclosed) { 11 | throw new Error('unable to test when market closed') 12 | } 13 | 14 | x.Stream.listen.getCandles((data => { 15 | x.Stream.unSubscribe.getCandles('EURUSD') 16 | resolve() 17 | })) 18 | const socketId = x.Socket.getSocketId() 19 | await x.getPriceHistory({symbol:'EURUSD',period:PERIOD_FIELD.PERIOD_M1,socketId}) 20 | const streamId = socketId && x.Socket.connections[socketId].streamId 21 | await x.Stream.subscribe.getCandles('EURUSD', streamId) 22 | } catch (e) { 23 | reject(e) 24 | } 25 | }) 26 | } -------------------------------------------------------------------------------- /test/v2/test/tradeTest.ts: -------------------------------------------------------------------------------- 1 | import {XAPI} from '../../../src/v2/core/XAPI' 2 | 3 | export function tradeTest(x: XAPI): Promise { 4 | const symbol = 'EURUSD' 5 | const volume = 0.01 6 | 7 | return new Promise(async (resolve, reject) => { 8 | try { 9 | await x.Stream.subscribe.getTrades() 10 | await x.Stream.subscribe.getTradeStatus() 11 | const r = await x.trading.buy({ 12 | symbol, 13 | volume, 14 | limit: 0.0987 15 | }).transactionStatus 16 | const position = x.trading.limitPositions?.find(i => i.order === r.order && i.symbol === symbol && i.volume >= volume && i.open_price === 0.0987) 17 | if (!position) { 18 | throw new Error('position not found;' + JSON.stringify(x.trading.limitPositions?.map(i => i.valueOf()))) 19 | } 20 | await x.trading.close({ order: position.position }).transactionStatus 21 | const position1 = x.trading.limitPositions?.find(i => i.position === position.position && i.symbol === symbol && i.volume >= volume && i.open_price === 0.0987) 22 | if (position1) { 23 | throw new Error('position found after close;' + JSON.stringify(x.trading.limitPositions?.map(i => i.valueOf()))) 24 | } 25 | resolve() 26 | } catch (e) { 27 | reject(e) 28 | } 29 | }) 30 | } -------------------------------------------------------------------------------- /src/v2/interface/Request.ts: -------------------------------------------------------------------------------- 1 | import {CMD_FIELD} from './Enum' 2 | import {TRADE_TRANS_INFO} from './Definitions' 3 | 4 | export interface getCommissionDef { 5 | symbol: string 6 | volume: number 7 | } 8 | 9 | export interface getIbsHistory { 10 | end: number 11 | start: number 12 | } 13 | 14 | export interface getMarginTrade { 15 | symbol: string 16 | volume: number 17 | } 18 | 19 | export interface getNews { 20 | end: number 21 | start: number 22 | } 23 | 24 | export interface getProfitCalculation { 25 | closePrice: number 26 | cmd: CMD_FIELD 27 | openPrice: number 28 | symbol: string 29 | volume: number 30 | } 31 | 32 | export interface getSymbol { 33 | symbol: string 34 | } 35 | 36 | export interface getTickPrices { 37 | level: number 38 | symbols: string[] 39 | timestamp: number 40 | } 41 | 42 | export interface getTradeRecords { 43 | orders: number[] 44 | } 45 | 46 | export interface getTrades { 47 | openedOnly: boolean 48 | } 49 | 50 | export interface getTradesHistory { 51 | end: number 52 | start: number 53 | } 54 | 55 | export interface getTradingHours { 56 | symbols: string[] 57 | } 58 | 59 | export interface tradeTransaction { 60 | tradeTransInfo: TRADE_TRANS_INFO 61 | } 62 | 63 | export interface tradeTransactionStatus { 64 | order: number 65 | } -------------------------------------------------------------------------------- /src/v2/utils/Logger.ts: -------------------------------------------------------------------------------- 1 | import {Listener} from "./Listener" 2 | 3 | type X = Record | string 4 | 5 | export class Logger extends Listener { 6 | on({ 7 | type, 8 | callback, 9 | key, 10 | }: ({ 11 | type: string 12 | callback: (data: Record | string) => void 13 | key?: string | null 14 | } | { 15 | type: 'transaction' 16 | callback: (data: T) => void 17 | key?: string | null 18 | } | { 19 | type: 'info' 20 | callback: (data: I) => void 21 | key?: string | null 22 | } | { 23 | type: 'warn' 24 | callback: (data: W) => void 25 | key?: string | null 26 | } | { 27 | type: 'error' 28 | callback: (data: E) => void 29 | key?: string | null 30 | } | { 31 | type: 'debug' 32 | callback: (data: D) => void 33 | key?: string | null 34 | })) { 35 | return this.addListener(type, callback, key) 36 | } 37 | 38 | call(type: string, data: Record | string): void { 39 | this.callListener(type, [data]) 40 | } 41 | 42 | transaction(data: T) { 43 | this.callListener('transaction', [data]) 44 | } 45 | 46 | info(data: I) { 47 | this.callListener('info', [data]) 48 | } 49 | 50 | warn(data: W) { 51 | this.callListener('warn', [data]) 52 | } 53 | 54 | error(data: E) { 55 | this.callListener('error', [data]) 56 | } 57 | 58 | debug(data: D) { 59 | this.callListener('debug', [data]) 60 | } 61 | } -------------------------------------------------------------------------------- /src/v2/interface/Response.ts: -------------------------------------------------------------------------------- 1 | import {RATE_INFO_RECORD, TICK_RECORD} from './Definitions' 2 | import {REQUEST_STATUS_FIELD} from './Enum' 3 | 4 | export interface getChartRequestResponse { 5 | digits: number 6 | rateInfos: RATE_INFO_RECORD[] 7 | } 8 | 9 | export interface getCommissionDefResponse { 10 | commission: number 11 | rateOfExchange: number 12 | } 13 | 14 | export interface getCurrentUserDataResponse { 15 | companyUnit: number 16 | currency: string 17 | group: string 18 | ibAccount: boolean 19 | leverage: number 20 | leverageMultiplier: number 21 | spreadType: string 22 | trailingStop: boolean 23 | } 24 | 25 | export interface getMarginLevelResponse { 26 | balance: number 27 | credit: number 28 | currency: string 29 | equity: number 30 | margin: number 31 | margin_free: number 32 | margin_level: number 33 | } 34 | 35 | export interface getMarginTradeResponse { 36 | margin: number 37 | } 38 | 39 | export interface getProfitCalculationResponse { 40 | profit: number 41 | } 42 | 43 | export interface getServerTimeResponse { 44 | time: number 45 | timeString: string 46 | } 47 | 48 | export interface getTickPricesResponse { 49 | quotations: TICK_RECORD[] 50 | } 51 | 52 | export interface getVersionResponse { 53 | version: number 54 | } 55 | 56 | export interface tradeTransactionResponse { 57 | order: number 58 | } 59 | 60 | export interface tradeTransactionStatusResponse { 61 | ask: number 62 | bid: number 63 | customComment: string 64 | message: string 65 | order: number 66 | requestStatus: REQUEST_STATUS_FIELD 67 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 4-Clause License 2 | 3 | Copyright (c) 2019, Peter Szombati 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright 13 | notice, this list of conditions and the following disclaimer in the 14 | documentation and/or other materials provided with the distribution. 15 | 16 | 3. All advertising materials mentioning features or use of this software 17 | must display the following acknowledgement: 18 | This product includes software developed by Peter Szombati. 19 | 20 | 4. Neither the name of the Peter Szombati nor the 21 | names of its contributors may be used to endorse or promote products 22 | derived from this software without specific prior written permission. 23 | 24 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 25 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 26 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 27 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 28 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 29 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 31 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 32 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 33 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | -------------------------------------------------------------------------------- /src/v2/core/Transaction.ts: -------------------------------------------------------------------------------- 1 | import {createPromise} from "../utils/createPromise" 2 | import {Time} from "../utils/Time" 3 | 4 | export class Transaction, State = Init | Record> { 5 | public state: Record 6 | private _resolve: (data?: any) => any 7 | private _reject: (error?: any) => void 8 | 9 | constructor(state?: Init & State) { 10 | const {resolve, reject, promise} = createPromise() 11 | this._resolve = resolve 12 | this._reject = reject 13 | this._promise = promise 14 | this.state = { 15 | createdAt: new Time(), 16 | ...state, 17 | } 18 | } 19 | 20 | private _promise: Promise 21 | 22 | public get promise(): Promise<{ transaction: Transaction, data: any }> { 23 | return this._promise 24 | } 25 | 26 | public setState(state: Init | State) { 27 | this.state = {...this.state, ...state} 28 | } 29 | 30 | public resolve(data?: any): Error | undefined | void { 31 | if (!this.state.resolved && !this.state.rejected) { 32 | this.state.resolved = new Time() 33 | return this._resolve({transaction: this, data}) 34 | } 35 | return new Error('already resolved or rejected') 36 | } 37 | 38 | public reject(error?: any): Error | undefined | void { 39 | if (!this.state.resolved && !this.state.rejected) { 40 | this.state.rejected = new Time() 41 | if (typeof error === 'object' && error.error instanceof Error) { 42 | return this._reject({...error,transaction: this}) 43 | } else { 44 | return this._reject({transaction: this, error}) 45 | } 46 | } 47 | return new Error('already resolved or rejected') 48 | } 49 | } -------------------------------------------------------------------------------- /src/v2/utils/parseJsonLogin.ts: -------------------------------------------------------------------------------- 1 | export function parseJsonLogin(jsonString: string): { accountId: string, password: string, accountType: 'real' | 'demo', rateLimit?: number, host?: string, appName?: string, tradingDisabled?: boolean } { 2 | let json: any = {} 3 | try { 4 | json = JSON.parse(jsonString.trim()) 5 | } catch (e) { 6 | throw new Error('json parse failed') 7 | } 8 | if (typeof json !== 'object') { 9 | throw new Error(`json is not valid (typeof = ${typeof json})`) 10 | } 11 | 12 | let { 13 | accountId, 14 | password, 15 | accountType, 16 | type, 17 | rateLimit, 18 | host, 19 | appName, 20 | tradingDisabled 21 | }: { accountId: string, password: string, accountType?: 'real' | 'demo', type?: 'real' | 'demo', rateLimit?: number, host?: string, appName?: string, tradingDisabled?: boolean } = json 22 | accountType ||= type 23 | if ( 24 | typeof accountId !== 'string' || 25 | typeof password !== 'string' || 26 | typeof accountType !== 'string' || 27 | !['undefined', 'number'].includes(typeof rateLimit) || 28 | !['undefined', 'string'].includes(typeof host) || 29 | !['undefined', 'string'].includes(typeof appName) || 30 | !['undefined', 'boolean'].includes(typeof tradingDisabled) || 31 | Object.keys(json).length > 7 32 | ) { 33 | throw new Error('json is not valid') 34 | } 35 | if (!accountType || ['real', 'demo'].every(x => x !== accountType?.toLowerCase())) { 36 | throw new Error('json not contains valid "accountType" (it should be "real" or "demo")') 37 | } 38 | return { 39 | accountId, 40 | password, 41 | accountType: accountType.toLowerCase() as 'real' | 'demo', 42 | rateLimit, 43 | host, 44 | appName, 45 | tradingDisabled 46 | } 47 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xapi-node", 3 | "version": "3.0.5", 4 | "description": "This project makes it possible to get data from Forex market, execute market or limit order with NodeJS/JS through WebSocket connection", 5 | "exports": { 6 | ".": { 7 | "require": "./build/index.js", 8 | "import": "./build/index.js" 9 | } 10 | }, 11 | "types": "build/index.d.ts", 12 | "files": [ 13 | "build/*" 14 | ], 15 | "dependencies": { 16 | "ws": "8.17.1" 17 | }, 18 | "devDependencies": { 19 | "@types/chai": "4.3.3", 20 | "@types/mocha": "10.0.0", 21 | "@types/node": "18.7.23", 22 | "@types/ws": "8.5.3", 23 | "@typescript-eslint/eslint-plugin": "5.38.1", 24 | "@typescript-eslint/parser": "5.38.1", 25 | "chai": "4.3.6", 26 | "esbuild": "0.15.10", 27 | "eslint": "8.24.0", 28 | "mocha": "10.0.0", 29 | "prettier": "2.7.1", 30 | "ts-mocha": "10.0.0", 31 | "ts-node": "10.9.1", 32 | "typescript": "4.8.4" 33 | }, 34 | "scripts": { 35 | "build": "rm -rf ./build && tsc --declaration --emitDeclarationOnly & node ./esbuild.config.js", 36 | "test": "ts-mocha -p ./tsconfig.json ./test/v1/tests.ts test/v1/sandbox.ts --exit", 37 | "prettier": "prettier --check \"src/**/*.ts\" \"sandbox/**/*.ts\"", 38 | "eslint": "eslint --fix \"src/**/*.ts\" \"sandbox/**/*.ts\"", 39 | "lint": "tsc --noEmit && npm run prettier && npm run eslint" 40 | }, 41 | "repository": { 42 | "type": "git", 43 | "url": "git+https://github.com/peterszombati/xapi-node.git" 44 | }, 45 | "keywords": [ 46 | "xstation5", 47 | "trading", 48 | "xtb", 49 | "bfbcapital", 50 | "forex", 51 | "trading-api", 52 | "xopenhub" 53 | ], 54 | "author": "Peter Szombati", 55 | "license": "BSD 4-Clause", 56 | "bugs": { 57 | "url": "https://github.com/peterszombati/xapi-node/issues" 58 | }, 59 | "homepage": "https://github.com/peterszombati/xapi-node#readme" 60 | } 61 | -------------------------------------------------------------------------------- /src/v2/utils/Timer.ts: -------------------------------------------------------------------------------- 1 | import {createPromise} from "./createPromise" 2 | 3 | export class Timer { 4 | private interval: any = null 5 | private timeout: any = null 6 | 7 | setInterval(callback: () => void, ms: number) { 8 | this.clear() 9 | this.interval = setInterval(() => { 10 | try { // ignore errors 11 | callback() 12 | } catch (e) {} 13 | }, ms) 14 | } 15 | 16 | setTimeout(callback: () => void | Promise, ms: number): Promise { 17 | this.clear() 18 | const p = createPromise() 19 | const timeoutId = setTimeout(() => { 20 | try { 21 | const result = callback() 22 | if (result instanceof Promise) { 23 | result.then((data) => { 24 | if (timeoutId === this.timeout) { 25 | this.timeout = null 26 | } 27 | p.resolve(data) 28 | }) 29 | result.catch(e => { 30 | if (timeoutId === this.timeout) { 31 | this.timeout = null 32 | } 33 | p.reject(e) 34 | }) 35 | } else if (timeoutId === this.timeout) { 36 | this.timeout = null 37 | p.resolve(undefined) 38 | } 39 | } catch (e) { 40 | p.reject(e) 41 | } 42 | }, ms) 43 | this.timeout = timeoutId 44 | return p.promise 45 | } 46 | 47 | clear() { 48 | if (this.timeout !== null) { 49 | clearTimeout(this.timeout) 50 | this.timeout = null 51 | } 52 | if (this.interval !== null) { 53 | clearInterval(this.interval) 54 | this.interval = null 55 | } 56 | } 57 | 58 | isNull() { 59 | return this.interval === null && this.timeout === null 60 | } 61 | } -------------------------------------------------------------------------------- /test/v2/tests.ts: -------------------------------------------------------------------------------- 1 | import {parseLoginFile} from './parseLoginFile' 2 | import * as path from 'path' 3 | import {connectionTest} from './test/connectionTest' 4 | import {XAPI} from '../../src/v2/core/XAPI' 5 | import {tradeTest} from './test/tradeTest' 6 | import {subscribeTest} from './test/subscribeTest' 7 | import {getCandlesTest} from './test/getCandlesTest' 8 | import {messageQueuStressTest} from './test/messageQueuStressTest' 9 | import {Counter, Logger} from '../../src' 10 | 11 | const jsonPath = path.join(process.cwd(), 'sensitive', 'sensitive-demo-login.json') 12 | /* sensitive/sensitive-demo-login.json 13 | { 14 | "accountId": "", 15 | "password": "", 16 | "type": "real" 17 | } 18 | */ 19 | 20 | let x: XAPI | null = null 21 | 22 | async function init(): Promise { 23 | if (!x || Object.keys(x.Socket.connections).length === 0) { 24 | const login = parseLoginFile(jsonPath) 25 | const l = new Logger() 26 | l.on({ 27 | type: 'debug', 28 | callback: data => console.log(data) 29 | }) 30 | l.on({ 31 | type: 'transaction', 32 | callback: data => console.log(data) 33 | }) 34 | l.on({ 35 | type: 'error', 36 | callback: data => console.log(data) 37 | }) 38 | l.on({ 39 | type: 'info', 40 | callback: data => console.log(data) 41 | }) 42 | const c = new Counter() 43 | c.on({ 44 | callback: ({key, time, count}) => console.log(time.get().toISOString() + ':' + key.join(':') + ':' + count) 45 | }) 46 | x = new XAPI({...login}, l, c) 47 | await x.connect() 48 | } 49 | return x 50 | } 51 | 52 | describe('tests', () => { 53 | it('connectionTest', async function () { 54 | const x = await init() 55 | await connectionTest(x) 56 | }) 57 | 58 | it('messageQueuStressTest', async function () { 59 | this.timeout(8000) 60 | const x = await init() 61 | await messageQueuStressTest(x) 62 | }) 63 | 64 | it('candleTest', async function () { 65 | this.timeout(65000) 66 | const x = await init() 67 | await getCandlesTest(x) 68 | }) 69 | 70 | it('subscribeTest', async function () { 71 | this.timeout(8000) 72 | const x = await init() 73 | await subscribeTest(x) 74 | }) 75 | 76 | it('trade EURUSD', async function () { 77 | this.timeout(8000) 78 | const x = await init() 79 | await tradeTest(x) 80 | }) 81 | }) -------------------------------------------------------------------------------- /src/v2/utils/Listener.ts: -------------------------------------------------------------------------------- 1 | import {Increment} from "./Increment" 2 | 3 | export type ListenerChild = { stopListen: () => void} 4 | 5 | export class Listener { 6 | private increment = new Increment() 7 | 8 | private _listeners: any = {} 9 | 10 | public get listeners() { 11 | return this._listeners 12 | } 13 | 14 | public remove(listenerId: string, key: string) { 15 | if (this._listeners[listenerId] !== undefined 16 | && this._listeners[listenerId][key] !== undefined) { 17 | delete this._listeners[listenerId][key] 18 | if (Object.keys(this._listeners[listenerId]).length === 0) { 19 | delete this._listeners[listenerId] 20 | } 21 | } 22 | } 23 | 24 | protected addListener(listenerId: string, callBack: any, key: string | null = null): { stopListen: () => void } { 25 | if (typeof callBack === 'function') { 26 | if (this._listeners[listenerId] === undefined) { 27 | this._listeners[listenerId] = {} 28 | } 29 | key = key === null 30 | ? `g${new Date().getTime()}${this.increment.id}` 31 | : `s${key}` 32 | this._listeners[listenerId][key] = callBack 33 | return { 34 | // @ts-ignore 35 | stopListen: () => this.remove(listenerId, key) 36 | } 37 | } 38 | throw new Error('addListener "callBack" parameter is not callback') 39 | } 40 | 41 | protected callListener(listenerId: string, params: any[] = []): void { 42 | if (this._listeners[listenerId] !== undefined) { 43 | Object.keys(this._listeners[listenerId]).forEach((key: string) => { 44 | try { 45 | this._listeners[listenerId][key](...params) 46 | } catch (e) { 47 | } 48 | }) 49 | } 50 | } 51 | 52 | protected fetchListener(listenerId: string, params: any[] = []): ({ key: any, data: any } | { key: any, error: any })[] { 53 | const values: any[] = [] 54 | if (this._listeners[listenerId] !== undefined) { 55 | Object.keys(this._listeners[listenerId]).forEach((key: string) => { 56 | try { 57 | values.push({ 58 | key, 59 | data: this._listeners[listenerId][key](...params) 60 | }) 61 | } catch (e) { 62 | values.push({key, error: e}) 63 | } 64 | }) 65 | } 66 | return values 67 | } 68 | } -------------------------------------------------------------------------------- /docs/API Documentation_files/jquery-scrollTo.min.js.download: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2007-2013 Ariel Flesler - afleslergmailcom | http://flesler.blogspot.com 3 | * Dual licensed under MIT and GPL. 4 | * @author Ariel Flesler 5 | * @version 1.4.6 6 | */ 7 | ;(function($){var h=$.scrollTo=function(a,b,c){$(window).scrollTo(a,b,c)};h.defaults={axis:'xy',duration:parseFloat($.fn.jquery)>=1.3?0:1,limit:true};h.window=function(a){return $(window)._scrollable()};$.fn._scrollable=function(){return this.map(function(){var a=this,isWin=!a.nodeName||$.inArray(a.nodeName.toLowerCase(),['iframe','#document','html','body'])!=-1;if(!isWin)return a;var b=(a.contentWindow||a).document||a.ownerDocument||a;return/webkit/i.test(navigator.userAgent)||b.compatMode=='BackCompat'?b.body:b.documentElement})};$.fn.scrollTo=function(e,f,g){if(typeof f=='object'){g=f;f=0}if(typeof g=='function')g={onAfter:g};if(e=='max')e=9e9;g=$.extend({},h.defaults,g);f=f||g.duration;g.queue=g.queue&&g.axis.length>1;if(g.queue)f/=2;g.offset=both(g.offset);g.over=both(g.over);return this._scrollable().each(function(){if(e==null)return;var d=this,$elem=$(d),targ=e,toff,attr={},win=$elem.is('html,body');switch(typeof targ){case'number':case'string':if(/^([+-]=?)?\d+(\.\d+)?(px|%)?$/.test(targ)){targ=both(targ);break}targ=$(targ,this);if(!targ.length)return;case'object':if(targ.is||targ.style)toff=(targ=$(targ)).offset()}$.each(g.axis.split(''),function(i,a){var b=a=='x'?'Left':'Top',pos=b.toLowerCase(),key='scroll'+b,old=d[key],max=h.max(d,a);if(toff){attr[key]=toff[pos]+(win?0:old-$elem.offset()[pos]);if(g.margin){attr[key]-=parseInt(targ.css('margin'+b))||0;attr[key]-=parseInt(targ.css('border'+b+'Width'))||0}attr[key]+=g.offset[pos]||0;if(g.over[pos])attr[key]+=targ[a=='x'?'width':'height']()*g.over[pos]}else{var c=targ[pos];attr[key]=c.slice&&c.slice(-1)=='%'?parseFloat(c)/100*max:c}if(g.limit&&/^\d+$/.test(attr[key]))attr[key]=attr[key]<=0?0:Math.min(attr[key],max);if(!i&&g.queue){if(old!=attr[key])animate(g.onAfterFirst);delete attr[key]}});animate(g.onAfter);function animate(a){$elem.animate(attr,f,g.easing,a&&function(){a.call(this,targ,g)})}}).end()};h.max=function(a,b){var c=b=='x'?'Width':'Height',scroll='scroll'+c;if(!$(a).is('html,body'))return a[scroll]-$(a)[c.toLowerCase()]();var d='client'+c,html=a.ownerDocument.documentElement,body=a.ownerDocument.body;return Math.max(html[scroll],body[scroll])-Math.min(html[d],body[d])};function both(a){return typeof a=='object'?a:{top:a,left:a}}})(jQuery); 8 | 9 | 10 | // (function(a){a.fn.scrollTo=function(b){var c={offset:0,speed:"slow",override:null,easing:null};b&&(b.override&&(b.override=-1!=override("#")?b.override:"#"+b.override),a.extend(c,b));return this.each(function(b,e){a(e).click(function(b){var d;null!==a(e).attr("href").match(/#/)&&(b.preventDefault(),d=c.override?c.override:a(e).attr("href"),history.pushState?(history.pushState(null,null,d),a("html,body").stop().animate({scrollTop:a(d).offset().top+c.offset},c.speed,c.easing)):a("html,body").stop().animate({scrollTop:a(d).offset().top+ 11 | // c.offset},c.speed,c.easing,function(a){window.location.hash=d}))})})}})(jQuery); -------------------------------------------------------------------------------- /docs/API Documentation_files/documentation.css: -------------------------------------------------------------------------------- 1 | 2 | html{ 3 | margin-top: 30px; 4 | } 5 | body { 6 | } 7 | 8 | h2 { 9 | margin-top: 50px; 10 | } 11 | 12 | h3 { 13 | margin-top: 30px; 14 | } 15 | 16 | h4 { 17 | margin-top: 30px; 18 | font-size: 20px; 19 | } 20 | 21 | h5 { 22 | margin-top: 22px; 23 | font-size: 18px; 24 | } 25 | 26 | h2, h3, h4, h5, .record { 27 | padding-top:50px; 28 | margin-top:-50px; 29 | -webkit-background-clip:content-box; 30 | background-clip:content-box; 31 | } 32 | 33 | .navbar .nav>li span { 34 | color: #999; 35 | text-shadow: 0 -1px 0 rgba(0,0,0,0.25); 36 | float: none; 37 | padding: 10px 15px 10px; 38 | text-decoration: none; 39 | display: block; 40 | } 41 | 42 | #choose-version-dropdown a.dropdown-toggle { 43 | 44 | padding-left: 0px; 45 | } 46 | 47 | #title { 48 | margin-top: 150px; 49 | text-align: center; 50 | margin-bottom: 100px; 51 | } 52 | #title p { 53 | font-size: 1.4em; 54 | } 55 | 56 | #logo { 57 | height: 30px; 58 | width: 30px; 59 | background-image: url('logo-small.png'); 60 | background-repeat: no-repeat; 61 | background-size: 30px 30px; 62 | margin: 5px; 63 | padding: 0; 64 | float: left; 65 | } 66 | 67 | #page-wrapper { 68 | font-size: 2.0em; 69 | } 70 | 71 | #page-wrapper p, 72 | #page-wrapper ul, 73 | #page-wrapper table { 74 | font-size: 0.5em; 75 | } 76 | 77 | .field-name { 78 | color: #08c; 79 | } 80 | 81 | code { 82 | color: #08c; 83 | } 84 | 85 | table.request-fields { 86 | 87 | } 88 | 89 | table.reply-fields { 90 | 91 | } 92 | 93 | a { 94 | color: #08c; 95 | } 96 | 97 | #side-menu { 98 | position: fixed; 99 | width: 250px; 100 | left: 0; 101 | top: 40px; 102 | bottom: 0; 103 | overflow-y: scroll; 104 | } 105 | 106 | #side-menu h1 { 107 | background-color: #4d297d; 108 | padding-left: 10px; 109 | margin: 0; 110 | font-size: 25px; 111 | color: #ddd; 112 | font-weight: normal; 113 | } 114 | 115 | #side-menu ul { 116 | margin: 0px; 117 | } 118 | 119 | #side-menu a { 120 | padding-top: 3px; 121 | padding-bottom: 3px; 122 | display: block; 123 | border-bottom: 1px solid #444; 124 | color: #aaa; 125 | } 126 | 127 | #side-menu a:hover { 128 | color: #000; 129 | text-decoration: none; 130 | background-color: #fff; 131 | } 132 | 133 | #side-menu ul li a { 134 | padding-left: 10px; 135 | } 136 | 137 | #side-menu ul li ul li a { 138 | padding-left: 25px; 139 | } 140 | 141 | #side-menu ul li ul li ul li a { 142 | padding-left: 40px; 143 | } 144 | 145 | #side-menu li { 146 | list-style: none; 147 | background-color: #333; 148 | } 149 | 150 | #page-wrapper { 151 | margin-left: 250px; 152 | /* position: absolute; 153 | left: 250px; 154 | right: 0; 155 | top: 40px; 156 | bottom: 0; 157 | overflow-y: scroll;*/ 158 | } 159 | 160 | .shortlink { 161 | color: #999; 162 | } 163 | 164 | .shortlink:hover { 165 | color: #aaa; 166 | text-decoration: none; 167 | } 168 | 169 | ul ul, ul ol, ol ol, ol ul { 170 | margin-bottom: 15px; 171 | } 172 | 173 | @media print { 174 | .navbar { 175 | display: none; 176 | } 177 | 178 | i.icon-share { 179 | display: none; 180 | } 181 | 182 | #side-menu { 183 | display: none; 184 | } 185 | 186 | #page-wrapper { 187 | position: static; 188 | top: 0; 189 | left: 0; 190 | margin: 0 20px; 191 | } 192 | 193 | #page-wrapper ol p { 194 | text-align: justify; 195 | } 196 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { XAPI, XAPIConfig } from './v2/core/XAPI' 2 | import { XAPI as XAPIv2 } from './v2/core/XAPI' 3 | export { XAPIv2 } 4 | import { 5 | CALENDAR_RECORD, 6 | CHART_LAST_INFO_RECORD, 7 | CHART_RANGE_INFO_RECORD, 8 | IB_RECORD, 9 | NEWS_TOPIC_RECORD, 10 | QUOTES_RECORD, 11 | RATE_INFO_RECORD, 12 | STEP_RECORD, 13 | STEP_RULE_RECORD, 14 | STREAMING_BALANCE_RECORD, 15 | STREAMING_CANDLE_RECORD, 16 | STREAMING_KEEP_ALIVE_RECORD, 17 | STREAMING_NEWS_RECORD, 18 | STREAMING_PROFIT_RECORD, 19 | STREAMING_TICK_RECORD, 20 | STREAMING_TRADE_RECORD, 21 | STREAMING_TRADE_STATUS_RECORD, 22 | SYMBOL_RECORD, 23 | TICK_RECORD, 24 | TRADE_RECORD, 25 | TRADE_TRANS_INFO, 26 | TRADING_HOURS_RECORD, 27 | TRADING_RECORD, 28 | } from './v2/interface/Definitions' 29 | import { 30 | Candle, 31 | CHART_RATE_LIMIT_BY_PERIOD, 32 | CMD_FIELD, 33 | DAY_FIELD, 34 | errorCode, 35 | PERIOD_FIELD, 36 | REQUEST_STATUS_FIELD, 37 | STATE_FIELD, 38 | TYPE_FIELD, 39 | } from './v2/interface/Enum' 40 | import { parseJsonLogin } from './v2/utils/parseJsonLogin' 41 | import { Time } from './v2/utils/Time' 42 | import { TradeStatus } from './v2/interface/Interface' 43 | import { Timer } from './v2/utils/Timer' 44 | import { ListenerChild } from './v2/utils/Listener' 45 | import { OpenPosition } from './v2/core/Trading/OpenPosition' 46 | import { LimitPosition } from './v2/core/Trading/LimitPosition' 47 | import { TradeRecord } from './v2/core/TradeRecord' 48 | import { Logger } from './v2/utils/Logger' 49 | import { Counter } from './v2/utils/Counter' 50 | 51 | export default XAPI 52 | export { XAPIConfig } 53 | 54 | export { 55 | CALENDAR_RECORD, 56 | IB_RECORD, 57 | NEWS_TOPIC_RECORD, 58 | STEP_RULE_RECORD, 59 | SYMBOL_RECORD, 60 | TRADE_RECORD, 61 | TRADE_TRANS_INFO, 62 | TRADING_HOURS_RECORD, 63 | STREAMING_TRADE_RECORD, 64 | STREAMING_TICK_RECORD, 65 | STREAMING_PROFIT_RECORD, 66 | STREAMING_NEWS_RECORD, 67 | STREAMING_KEEP_ALIVE_RECORD, 68 | TRADING_RECORD, 69 | QUOTES_RECORD, 70 | TICK_RECORD, 71 | STEP_RECORD, 72 | RATE_INFO_RECORD, 73 | STREAMING_TRADE_STATUS_RECORD, 74 | STREAMING_CANDLE_RECORD, 75 | STREAMING_BALANCE_RECORD, 76 | CHART_LAST_INFO_RECORD, 77 | CHART_RANGE_INFO_RECORD, 78 | } 79 | 80 | export { 81 | CMD_FIELD, 82 | DAY_FIELD, 83 | PERIOD_FIELD, 84 | TYPE_FIELD, 85 | STATE_FIELD, 86 | REQUEST_STATUS_FIELD, 87 | CHART_RATE_LIMIT_BY_PERIOD, 88 | Candle, 89 | errorCode, 90 | } 91 | 92 | export { XAPI, parseJsonLogin, Time, Timer, TradeStatus, ListenerChild, OpenPosition, LimitPosition, TradeRecord, Logger, Counter } 93 | 94 | export function getContractValue({ 95 | price, 96 | lot, 97 | contractSize, 98 | currency, 99 | currencyProfit, 100 | }: { 101 | price: number 102 | lot: number 103 | contractSize: number 104 | currency: string 105 | currencyProfit: string 106 | }) { 107 | return lot * contractSize * (currency === currencyProfit ? price : 1) 108 | } 109 | 110 | export function getProfit({ 111 | openPrice, 112 | closePrice, 113 | isBuy, 114 | lot, 115 | contractSize, 116 | }: { 117 | openPrice: number 118 | closePrice: number 119 | isBuy: boolean 120 | lot: number 121 | contractSize: number 122 | }) { 123 | return (isBuy ? closePrice - openPrice : openPrice - closePrice) * lot * contractSize 124 | } -------------------------------------------------------------------------------- /src/v2/interface/Enum.ts: -------------------------------------------------------------------------------- 1 | // xapi 2 | export enum REQUEST_STATUS_FIELD { 3 | ERROR = 0, 4 | PENDING = 1, 5 | ACCEPTED = 3, 6 | REJECTED = 4, 7 | } 8 | 9 | export enum DAY_FIELD { 10 | MONDAY = 1, 11 | TUESDAY = 2, 12 | WEDNESDAY = 3, 13 | THURSDAY = 4, 14 | FRIDAY = 5, 15 | SATURDAY = 6, 16 | SUNDAY = 7, 17 | } 18 | 19 | export enum CMD_FIELD { 20 | BUY = 0, 21 | SELL = 1, 22 | BUY_LIMIT = 2, 23 | SELL_LIMIT = 3, 24 | BUY_STOP = 4, 25 | SELL_STOP = 5, 26 | BALANCE = 6, 27 | CREDIT = 7, 28 | } 29 | 30 | export enum TYPE_FIELD { 31 | OPEN = 0, 32 | PENDING = 1, 33 | CLOSE = 2, 34 | MODIFY = 3, 35 | DELETE = 4, 36 | } 37 | 38 | export enum STATE_FIELD { 39 | MODIFIED = 'Modified', 40 | DELETED = 'Deleted', 41 | } 42 | 43 | export enum PERIOD_FIELD { 44 | PERIOD_M1 = 1, 45 | PERIOD_M5 = 5, 46 | PERIOD_M15 = 15, 47 | PERIOD_M30 = 30, 48 | PERIOD_H1 = 60, 49 | PERIOD_H4 = 240, 50 | PERIOD_D1 = 1440, 51 | PERIOD_W1 = 10080, 52 | PERIOD_MN1 = 43200, 53 | } 54 | 55 | // xapi-node 56 | export const CHART_RATE_LIMIT_BY_PERIOD: Record = { 57 | PERIOD_M1: 28800, // 1 month 58 | PERIOD_M5: 17280, // 3 month 59 | PERIOD_M15: 5760, // 3 month 60 | PERIOD_M30: 6720, // 7 month 61 | PERIOD_H1: 3360, // 7 month 62 | PERIOD_H4: 1560, // 13 month 63 | PERIOD_D1: 19200, // 52 years 64 | PERIOD_W1: 3840, // 73 years 65 | PERIOD_MN1: 960, // 80 years 66 | } 67 | 68 | export enum PositionType { 69 | open = 0, 70 | closed = 1, 71 | limit = 2, 72 | source = 3, 73 | } 74 | 75 | export enum Candle { 76 | timestamp = 0, 77 | open = 1, 78 | high = 2, 79 | low = 3, 80 | close = 4, 81 | volume = 5, 82 | } 83 | 84 | export enum errorCode { 85 | XAPINODE_0 = 'XAPINODE_0', // Each command invocation should not contain more than 1kB of data. 86 | XAPINODE_1 = 'XAPINODE_1', // WebSocket closed 87 | XAPINODE_2 = 'XAPINODE_2', // messageQueues exceeded 150 size limit 88 | XAPINODE_3 = 'XAPINODE_3', // Transaction timeout (60s) 89 | XAPINODE_4 = 'XAPINODE_4', // Trading disabled 90 | XAPINODE_BE103 = 'XAPINODE_BE103', // User is not logged 91 | BE005 = 'BE005', // "userPasswordCheck: Invalid login or password" 92 | BE118 = 'BE118', // User already logged 93 | } 94 | 95 | export const Currency2Pair: Record = { 96 | HUF: 'EURHUF', 97 | USD: 'EURUSD', 98 | JPY: 'USDJPY', 99 | GBP: 'EURGBP', 100 | TRY: 'EURTRY', 101 | CHF: 'USDCHF', 102 | CZK: 'USDCZK', 103 | BRL: 'USDBRL', 104 | PLN: 'USDPLN', 105 | MXN: 'USDMXN', 106 | ZAR: 'USDZAR', 107 | RON: 'USDRON', 108 | AUD: 'EURAUD', 109 | CAD: 'USDCAD', 110 | SEK: 'USDSEK', 111 | NOK: 'EURNOK', 112 | NZD: 'EURNZD', 113 | EUR: 'DE30', 114 | CLP: 'USDCLP', 115 | DKK: 'VWS.DK_4', 116 | BTC: 'XEMBTC', 117 | ETH: 'TRXETH', 118 | } 119 | 120 | export type RelevantCurrencies = 121 | 'HUF' 122 | | 'USD' 123 | | 'JPY' 124 | | 'GBP' 125 | | 'TRY' 126 | | 'CHF' 127 | | 'CZK' 128 | | 'BRL' 129 | | 'PLN' 130 | | 'MXN' 131 | | 'ZAR' 132 | | 'RON' 133 | | 'AUD' 134 | | 'CAD' 135 | | 'SEK' 136 | | 'NOK' 137 | | 'NZD' 138 | | 'EUR' 139 | | 'CLP' 140 | | 'DKK' 141 | | 'BTC' 142 | | 'ETH' -------------------------------------------------------------------------------- /src/v2/core/TradeRecord.ts: -------------------------------------------------------------------------------- 1 | import {CMD_FIELD, PositionType} from '../interface/Enum' 2 | import {getPositionType} from '../utils/getPositionType' 3 | 4 | export type TradeRecordParams = { 5 | close_time: number 6 | close_price?: number | undefined 7 | closed: boolean 8 | cmd: CMD_FIELD 9 | comment: string 10 | commission: number 11 | customComment: string 12 | digits: number 13 | expiration: number | null 14 | margin_rate: number 15 | offset: number 16 | open_price: number 17 | open_time: number 18 | order: number 19 | order2: number 20 | position: number 21 | sl: number 22 | storage: number 23 | symbol: string 24 | tp: number 25 | volume: number 26 | } 27 | 28 | export class TradeRecord { 29 | public close_time: number 30 | 31 | public close_price: number | undefined 32 | public closed: boolean 33 | public cmd: CMD_FIELD 34 | public comment: string 35 | public commission: number 36 | public customComment: string 37 | public digits: number 38 | public expiration: number | null 39 | public margin_rate: number 40 | public offset: number 41 | public open_price: number 42 | public open_time: number 43 | public order: number 44 | public order2: number 45 | public position: number 46 | public sl: number 47 | public storage: number 48 | public symbol: string 49 | public tp: number 50 | public volume: number 51 | 52 | constructor(params: TradeRecordParams) { 53 | this.close_time = params.close_time 54 | this.closed = params.closed 55 | this.cmd = params.cmd 56 | this.comment = params.comment 57 | this.commission = params.commission 58 | this.customComment = params.customComment 59 | this.digits = params.digits 60 | this.expiration = params.expiration 61 | this.margin_rate = params.margin_rate 62 | this.offset = params.offset 63 | this.open_price = params.open_price 64 | this.open_time = params.open_time 65 | this.order = params.order 66 | this.order2 = params.order2 67 | this.position = params.position 68 | this.sl = params.sl 69 | this.storage = params.storage 70 | this.symbol = params.symbol 71 | this.tp = params.tp 72 | this.volume = params.volume 73 | this.close_price = this.position_type === PositionType.closed ? params.close_price : undefined 74 | } 75 | 76 | public get position_type() { 77 | return getPositionType({cmd: this.cmd, closed: this.closed, close_time: this.close_time}) 78 | } 79 | 80 | valueOf(): TradeRecordParams { 81 | return { 82 | close_time: this.close_time, 83 | close_price: this.close_price, 84 | closed: this.closed, 85 | cmd: this.cmd, 86 | comment: this.comment, 87 | commission: this.commission, 88 | customComment: this.customComment, 89 | digits: this.digits, 90 | expiration: this.expiration, 91 | margin_rate: this.margin_rate, 92 | offset: this.offset, 93 | open_price: this.open_price, 94 | open_time: this.open_time, 95 | order: this.order, 96 | order2: this.order2, 97 | position: this.position, 98 | sl: this.sl, 99 | storage: this.storage, 100 | symbol: this.symbol, 101 | tp: this.tp, 102 | volume: this.volume, 103 | } 104 | } 105 | } -------------------------------------------------------------------------------- /src/v2/core/Stream/Stream.ts: -------------------------------------------------------------------------------- 1 | import { 2 | STREAMING_BALANCE_RECORD, 3 | STREAMING_CANDLE_RECORD, 4 | STREAMING_KEEP_ALIVE_RECORD, 5 | STREAMING_NEWS_RECORD, 6 | STREAMING_PROFIT_RECORD, 7 | STREAMING_TICK_RECORD, 8 | STREAMING_TRADE_RECORD, 9 | STREAMING_TRADE_STATUS_RECORD, 10 | } from '../../interface/Definitions' 11 | import {Time} from '../../utils/Time' 12 | import {StreamConnections} from './StreamConnections' 13 | import {XAPI} from "../XAPI" 14 | 15 | interface StreamListen { 16 | (data: T, time: Time, jsonString: string, streamId: string): void 17 | } 18 | 19 | export class Stream extends StreamConnections { 20 | public listen = { 21 | getBalance: (callBack: StreamListen, key: string | null = null) => 22 | this.addListener('command_balance', callBack, key), 23 | getCandles: (callBack: StreamListen, key: string | null = null) => 24 | this.addListener('command_candle', callBack, key), 25 | getKeepAlive: (callBack: StreamListen, key: string | null = null) => 26 | this.addListener('command_keepAlive', callBack, key), 27 | getNews: (callBack: StreamListen, key: string | null = null) => 28 | this.addListener('command_news', callBack, key), 29 | getProfits: (callBack: StreamListen, key: string | null = null) => 30 | this.addListener('command_profit', callBack, key), 31 | getTickPrices: (callBack: StreamListen, key: string | null = null) => 32 | this.addListener('command_tickPrices', callBack, key), 33 | getTrades: (callBack: StreamListen, key: string | null = null) => 34 | this.addListener('command_trade', callBack, key), 35 | getTradeStatus: (callBack: StreamListen, key: string | null = null) => 36 | this.addListener('command_tradeStatus', callBack, key), 37 | } 38 | 39 | public subscribe = { 40 | getBalance: (streamId: string | undefined = undefined) => this.sendSubscribe('Balance', {}, streamId), 41 | getCandles: (symbol: string, streamId: string | undefined = undefined) => this.sendSubscribe('Candles', {symbol}, streamId), 42 | getKeepAlive: (streamId: string | undefined = undefined) => this.sendSubscribe('KeepAlive', {}, streamId), 43 | getNews: (streamId: string | undefined = undefined) => this.sendSubscribe('News', {}, streamId), 44 | getProfits: (streamId: string | undefined = undefined) => this.sendSubscribe('Profits', {}, streamId), 45 | getTickPrices: (symbol: string, minArrivalTime = 0, maxLevel = 6, streamId: string | undefined = undefined) => 46 | this.sendSubscribe('TickPrices', {symbol, minArrivalTime, maxLevel}, streamId), 47 | getTrades: (streamId: string | undefined = undefined) => this.sendSubscribe('Trades', {}, streamId), 48 | getTradeStatus: (streamId: string | undefined = undefined) => this.sendSubscribe('TradeStatus', {}, streamId), 49 | } 50 | 51 | public subscribeOnStream(streamId: string | undefined = undefined) { 52 | return { 53 | getBalance: () => this.sendSubscribe('Balance', {}, streamId), 54 | getCandles: (symbol: string) => this.sendSubscribe('Candles', {symbol}, streamId), 55 | getKeepAlive: () => this.sendSubscribe('KeepAlive', {}, streamId), 56 | getNews: () => this.sendSubscribe('News', {}, streamId), 57 | getProfits: () => this.sendSubscribe('Profits', {}, streamId), 58 | getTickPrices: (symbol: string, minArrivalTime = 0, maxLevel = 6) => 59 | this.sendSubscribe('TickPrices', {symbol, minArrivalTime, maxLevel}, streamId), 60 | getTrades: () => this.sendSubscribe('Trades', {}, streamId), 61 | getTradeStatus: () => this.sendSubscribe('TradeStatus', {}, streamId), 62 | } 63 | } 64 | public unSubscribe = { 65 | getBalance: () => this.sendUnsubscribe('Balance'), 66 | getCandles: (symbol: string) => this.sendUnsubscribe('Candles', {symbol}), 67 | getKeepAlive: () => this.sendUnsubscribe('KeepAlive'), 68 | getNews: () => this.sendUnsubscribe('News'), 69 | getProfits: () => this.sendUnsubscribe('Profits'), 70 | getTickPrices: (symbol: string) => this.sendUnsubscribe('TickPrices', {symbol}), 71 | getTrades: () => this.sendUnsubscribe('Trades'), 72 | getTradeStatus: () => this.sendUnsubscribe('TradeStatus'), 73 | } 74 | 75 | constructor(accountType: string, host: string, XAPI: XAPI) { 76 | super(`wss://${host}/${accountType}Stream`, XAPI) 77 | } 78 | } -------------------------------------------------------------------------------- /src/v2/utils/WebSocketWrapper.ts: -------------------------------------------------------------------------------- 1 | import {Listener} from './Listener' 2 | import {Timer} from './Timer' 3 | import type {WebSocket as WS} from 'ws' 4 | 5 | export const isNodeJS = () => typeof window === 'undefined' && typeof module !== 'undefined' && module.exports 6 | 7 | function getWS(): Promise { 8 | if (process.env.ES_TARGET == 'esm') { 9 | return import('ws') 10 | } else { 11 | // eslint-disable-next-line 12 | return new Promise(resolve => resolve(require('ws'))) 13 | } 14 | } 15 | 16 | export class WebSocketWrapper extends Listener { 17 | private ws: any = null 18 | private _tryReconnect = false 19 | private _connectionTimeout: Timer = new Timer() 20 | private url: string 21 | 22 | constructor(url: string, tryReconnectOnFail = true) { 23 | super() 24 | this.url = url 25 | this._tryReconnect = tryReconnectOnFail 26 | 27 | this.onOpen(() => { 28 | this._connectionTimeout.clear() 29 | }) 30 | this.onClose(() => { 31 | if (this._tryReconnect) { 32 | this._connectionTimeout.setTimeout(() => { 33 | if (this._tryReconnect) { 34 | this.connect() 35 | } 36 | }, 3000) 37 | } 38 | }) 39 | } 40 | 41 | private _status = false 42 | private _connecting = false 43 | 44 | get status(): boolean { 45 | return this._status 46 | } 47 | get connecting(): boolean { 48 | return this._connecting 49 | } 50 | 51 | public connect() { 52 | this._connectionTimeout.clear() 53 | if (isNodeJS()) { 54 | // NodeJS module 55 | getWS().then(WebSocketClient => { 56 | this._connecting = true 57 | this.ws = new WebSocketClient(this.url) 58 | this.ws.on('open', () => { 59 | if (this._status === false) { 60 | this._status = true 61 | this._connecting = false 62 | this.callListener('ws_statusChange', [true]) 63 | } else { 64 | this._connecting = false 65 | } 66 | this.callListener('ws_open') 67 | }) 68 | this.ws.on('close', () => { 69 | if (this._status) { 70 | this._status = false 71 | this._connecting = false 72 | this.callListener('ws_statusChange', [false]) 73 | } else { 74 | this._connecting = false 75 | } 76 | this.callListener('ws_close') 77 | }) 78 | this.ws.on('message', (message: any) => { 79 | this.callListener('ws_message', [message]) 80 | }) 81 | this.ws.on('error', (error: any) => { 82 | this.callListener('ws_error', [error]) 83 | }) 84 | }) 85 | } else { 86 | // JavaScript browser module 87 | this._connecting = true 88 | this.ws = new WebSocket(this.url) 89 | this.ws.onopen = () => { 90 | if (this._status === false) { 91 | this._status = true 92 | this._connecting = false 93 | this.callListener('ws_statusChange', [true]) 94 | } else { 95 | this._connecting = false 96 | } 97 | this.callListener('ws_open') 98 | } 99 | this.ws.onclose = () => { 100 | if (this._status) { 101 | this._status = false 102 | this._connecting = false 103 | this.callListener('ws_statusChange', [false]) 104 | } else { 105 | this._connecting = false 106 | } 107 | this.callListener('ws_close') 108 | } 109 | this.ws.onmessage = (event: any) => { 110 | this.callListener('ws_message', [event.data]) 111 | } 112 | this.ws.onerror = (error: any) => { 113 | this.callListener('ws_error', [error]) 114 | } 115 | } 116 | } 117 | 118 | onStatusChange(callback: (status: boolean) => void) { 119 | this.addListener('ws_statusChange', callback) 120 | } 121 | 122 | onOpen(callback: () => void) { 123 | this.addListener('ws_open', callback) 124 | } 125 | 126 | onMessage(callback: (message: any) => void) { 127 | this.addListener('ws_message', callback) 128 | } 129 | 130 | onError(callback: (error: any) => void) { 131 | this.addListener('ws_error', callback) 132 | } 133 | 134 | onClose(callback: () => void) { 135 | this.addListener('ws_close', callback) 136 | } 137 | 138 | async send(data: any): Promise { 139 | if (this.status) { 140 | this.ws.send(data) 141 | } else { 142 | throw new Error(this.url + ' websocket is not connected') 143 | } 144 | } 145 | 146 | close() { 147 | this._connectionTimeout.clear() 148 | this._tryReconnect = false 149 | this._connecting = false 150 | this.ws && this.ws.close() 151 | } 152 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Logo](https://github.com/peterszombati/xapi-node/raw/master/docs/xtb-logo.png)](https://www.xtb.com/en) 2 | 3 | # xapi-node 4 | 5 | This project makes it possible to get data from Forex market, execute market or limit order with NodeJS/JS through WebSocket connection 6 | 7 | This module may can be used for [X-Trade Brokers](https://www.xtb.com/en) xStation5 accounts 8 | 9 | WebSocket protocol description: https://peterszombati.github.io/xapi-node/ 10 | 11 | This module is usable on Front-end too. 12 | 13 | 14 | 15 | ## Getting started 16 | 17 | ### 1. Install via [npm](https://www.npmjs.com/package/xapi-node) 18 | 19 | ``` 20 | npm i xapi-node 21 | ``` 22 | 23 | ### 2. Example usage 24 | #### Authentication 25 | ```ts 26 | // TypeScript 27 | import XAPI from 'xapi-node' 28 | 29 | const x = new XAPI({ 30 | accountId: '(xStation5) accountID', 31 | password: '(xStation5) password', 32 | type: 'real' // or demo 33 | }) 34 | 35 | (async () => { 36 | await x.connect() 37 | console.log('Connection is ready') 38 | x.disconnect().then(() => console.log('Disconnected')) 39 | })().catch((e) => { 40 | console.error(e) 41 | }) 42 | ``` 43 | #### Authentication only for XTB accounts 44 | ```ts 45 | // TypeScript 46 | import XAPI from 'xapi-node' 47 | 48 | const x = new XAPI({ 49 | accountId: '(xStation5) accountID', 50 | password: '(xStation5) password', 51 | host: 'ws.xtb.com', // only for XTB accounts 52 | type: 'real' // or demo 53 | }) 54 | 55 | (async () => { 56 | await x.connect() 57 | x.disconnect().then(() => console.log('Disconnected')) 58 | })().catch((e) => { 59 | console.error(e) 60 | }) 61 | ``` 62 | 63 | #### placing buy limit on BITCOIN [CFD] 64 | ```ts 65 | x.Socket.send.tradeTransaction({ 66 | cmd: CMD_FIELD.BUY_LIMIT, 67 | customComment: null, 68 | expiration: new Date().getTime() + 60000 * 60 * 24 * 365, 69 | offset: 0, 70 | order: 0, 71 | price: 100, 72 | sl: 0, 73 | symbol: 'BITCOIN', 74 | tp: 8000, 75 | type: TYPE_FIELD.OPEN, 76 | volume: 10 77 | }).then(({order}) => { 78 | console.log('Success ' + order) 79 | }).catch(e => { 80 | console.error('Failed') 81 | console.error(e) 82 | }) 83 | ``` 84 | 85 | #### placing buy limit on US30 (Dow Jones Industrial Average) 86 | ```ts 87 | x.Socket.send.tradeTransaction({ 88 | cmd: CMD_FIELD.BUY_LIMIT, 89 | customComment: null, 90 | expiration: new Date().getTime() + 60000 * 60 * 24 * 365, 91 | offset: 0, 92 | order: 0, 93 | price: 21900, 94 | sl: 0, 95 | symbol: 'US30', 96 | tp: 26500, 97 | type: TYPE_FIELD.OPEN, 98 | volume: 0.2 99 | }).then(({order}) => { 100 | console.log('Success ' + order) 101 | }).catch(e => { 102 | console.error('Failed') 103 | console.error(e) 104 | }) 105 | ``` 106 | 107 | #### get live EURUSD price data changing 108 | ```ts 109 | x.Stream.listen.getTickPrices((data) => { 110 | console.log(data.symbol + ': ' + data.ask + ' | ' + data.askVolume + ' volume | ' + data.level + ' level' ) 111 | }) 112 | 113 | (async () => { 114 | await x.connect() 115 | x.Stream.subscribe.getTickPrices('EURUSD') 116 | .catch(() => { console.error('subscribe for EURUSD failed')}) 117 | })() 118 | 119 | /* output 120 | EURUSD: 1.10912 | 500000 volume | 0 level 121 | EURUSD: 1.10913 | 1000000 volume | 1 level 122 | EURUSD: 1.10916 | 1000000 volume | 2 level 123 | EURUSD: 1.10922 | 3000000 volume | 3 level 124 | EURUSD: 1.10931 | 3500000 volume | 4 level 125 | ... 126 | */ 127 | ``` 128 | #### get EURUSD M1 price history 129 | ```ts 130 | (async () => { 131 | await x.connect() 132 | x.getPriceHistory({ 133 | symbol:'EURUSD', 134 | period: PERIOD_FIELD.PERIOD_M1 135 | }).then(({candles, digits}) => { 136 | console.log(candles.length) 137 | console.log(candles[0]) 138 | console.log('digits = ' + digits) 139 | }) 140 | })() 141 | ``` 142 | #### market buy EURUSD (1.0 lot / 100000 EUR) 143 | ```ts 144 | (async () => { 145 | await x.connect() 146 | x.Socket.send.tradeTransaction({ 147 | cmd: CMD_FIELD.BUY, 148 | customComment: null, 149 | expiration: x.serverTime + 5000, 150 | offset: 0, 151 | order: 0, 152 | price: 1, 153 | symbol: 'EURUSD', 154 | tp: 0, 155 | sl: 0, 156 | type: TYPE_FIELD.OPEN, 157 | volume: 1 158 | }).then(({order}) => { 159 | console.log('Success ' + order) 160 | }).catch(e => { 161 | console.error('Failed') 162 | console.error(e) 163 | }) 164 | })() 165 | ``` 166 | #### modify open position (for example set new stop loss) 167 | ```ts 168 | (async () => { 169 | await x.connect() 170 | x.Socket.send.tradeTransaction({ 171 | order: 1234, // position number you can find it in (x.positions) 172 | type: TYPE_FIELD.MODIFY, 173 | sl: 1.05, // new stop loss level 174 | }).then(({order}) => { 175 | console.log('Success ' + order) 176 | }).catch(e => { 177 | console.error('Failed') 178 | console.error(e) 179 | }) 180 | })() 181 | ``` 182 | #### How to use logger 183 | ```ts 184 | import {Logger,XAPI} from 'xapi-node' 185 | 186 | const l = new Logger() 187 | l.on({ 188 | type: 'debug', 189 | callback: data => console.log(data) 190 | }) 191 | l.on({ 192 | type: 'transaction', 193 | callback: data => console.log(data) 194 | }) 195 | l.on({ 196 | type: 'error', 197 | callback: data => console.log(data) 198 | }) 199 | l.on({ 200 | type: 'info', 201 | callback: data => console.log(data) 202 | }) 203 | 204 | const x = new XAPI({ 205 | accountId: '(xStation5) accountID', 206 | password: '(xStation5) password', 207 | type: 'real' // or demo 208 | }, l) 209 | ``` 210 | -------------------------------------------------------------------------------- /src/v2/core/Stream/StreamConnections.ts: -------------------------------------------------------------------------------- 1 | import {Listener} from '../../utils/Listener' 2 | import {StreamConnection} from "./StreamConnection" 3 | import {Increment} from "../../utils/Increment" 4 | import {Time} from "../../utils/Time" 5 | import {XAPI} from "../XAPI" 6 | 7 | export class StreamConnections extends Listener { 8 | public connections: Record = {} 9 | public subscribes: Record< 10 | string /* command */, 11 | Record 13 | > 14 | > = {} 15 | private url: string 16 | protected XAPI: XAPI 17 | 18 | constructor(url: string, XAPI: XAPI) { 19 | super() 20 | this.url = url 21 | this.XAPI = XAPI 22 | this.addListener('onClose', (streamId: string) => { 23 | for (const command of Object.keys(this.subscribes)) { 24 | for (const [parameter, _streamIdObject] of Object.entries(this.subscribes[command])) { 25 | for (const _streamId of Object.keys(_streamIdObject)) { 26 | if (_streamId === streamId) { 27 | delete this.subscribes[command][parameter][_streamId] 28 | } 29 | } 30 | } 31 | } 32 | delete this.connections[streamId] 33 | }) 34 | } 35 | 36 | public onClose(callback: (streamId: string, connection: StreamConnection) => void) { 37 | return this.addListener('onClose', (streamId: string, connection: StreamConnection) => { 38 | callback(streamId, connection) 39 | }) 40 | } 41 | 42 | public onOpen(callback: (streamId: string, connection: StreamConnection) => void) { 43 | return this.addListener('onOpen', (streamId: string, connection: StreamConnection) => { 44 | callback(streamId, connection) 45 | }) 46 | } 47 | 48 | streamIdIncrement = new Increment() 49 | public async connect(timeoutMs: number, session: string, socketId: string): Promise { 50 | const streamId = `${new Date().getTime()}${this.streamIdIncrement.id}` 51 | this.connections[streamId] = new StreamConnection(this.url, session, (listenerId: string, params?: any[]) => this.callListener(listenerId, params), streamId, socketId, this.XAPI) 52 | await this.connections[streamId].connect(timeoutMs) 53 | return streamId 54 | } 55 | 56 | public getStreamId(command: string, completion: Record = {}): string | undefined { 57 | if (this.subscribes[command]) { 58 | if (this.subscribes[command][JSON.stringify(completion)]) { 59 | const streamIds = Object.keys(this.subscribes[command][JSON.stringify(completion)]) 60 | for (const streamId of streamIds) { 61 | if (this.connections[streamId]?.status === 'CONNECTED') { 62 | return streamId 63 | } else { 64 | delete this.subscribes[command][JSON.stringify(completion)][streamId] 65 | } 66 | } 67 | } else if (this.subscribes[command]['{}']) { 68 | const streamIds = Object.keys(this.subscribes[command]['{}']) 69 | for (const streamId of streamIds) { 70 | if (this.connections[streamId]?.status === 'CONNECTED') { 71 | return streamId 72 | } else { 73 | delete this.subscribes[command]['{}'][streamId] 74 | } 75 | } 76 | } else if (Object.keys(this.subscribes[command])[0]) { 77 | const firstKey = Object.keys(this.subscribes[command])[0] 78 | const streamIds = Object.keys(this.subscribes[command][firstKey]) 79 | for (const streamId of streamIds) { 80 | if (this.connections[streamId]?.status === 'CONNECTED') { 81 | return streamId 82 | } else { 83 | delete this.subscribes[command][firstKey][streamId] 84 | } 85 | } 86 | } 87 | } 88 | return Object.values(this.connections).map((connection) => { 89 | const times = connection.capacity.filter(i => i.elapsedMs() < 1500) 90 | return { 91 | point: times.length <= 4 ? times.length : (5 + (1500 - times[0].elapsedMs())), 92 | connection, 93 | } 94 | }).sort((a,b) => a.point - b.point)[0]?.connection?.streamId 95 | } 96 | 97 | protected sendSubscribe(command: string, completion: Record = {}, streamId: string | undefined = undefined) { 98 | if (!streamId) { 99 | streamId = this.getStreamId(command) 100 | if (!streamId) { 101 | throw new Error('there is no connected stream '+JSON.stringify({streamId})) 102 | } 103 | } 104 | if (!this.connections[streamId]) { 105 | throw new Error('there is no connected stream '+JSON.stringify({streamId})) 106 | } 107 | const promise = this.connections[streamId].sendCommand('get' + command, completion) 108 | if (this.subscribes[command]) { 109 | const completionKey = JSON.stringify(completion) 110 | if (!this.subscribes[command][completionKey]) { 111 | this.subscribes[command][completionKey] = {} 112 | } 113 | this.subscribes[command][completionKey][streamId] = new Time() 114 | } else { 115 | this.subscribes[command] = { 116 | [JSON.stringify(completion)]: { streamId: new Time() } 117 | } 118 | } 119 | return promise 120 | } 121 | 122 | protected sendUnsubscribe(command: string, completion: Record = {}) { 123 | if (!this.subscribes[command]) { 124 | return Promise.resolve(undefined) 125 | } 126 | const streamIds = Object.keys(this.subscribes[command][JSON.stringify(completion)]) 127 | if (streamIds.length === 0) { 128 | return Promise.resolve(undefined) 129 | } 130 | return Promise.allSettled(streamIds 131 | .filter((streamId) => this.connections[streamId]) 132 | .map((streamId) => this.connections[streamId].sendCommand('stop' + command, completion))) 133 | } 134 | } -------------------------------------------------------------------------------- /src/v2/core/Socket/SocketConnections.ts: -------------------------------------------------------------------------------- 1 | import {Listener} from '../../utils/Listener' 2 | import {SocketConnection} from './SocketConnection' 3 | import {Transaction} from '../Transaction' 4 | import {Time} from '../../utils/Time' 5 | import {Increment} from "../../utils/Increment" 6 | import {XAPI} from "../XAPI" 7 | 8 | export class SocketConnections extends Listener { 9 | public connections: Record = {} 10 | public transactions: Record = {} 11 | private url: string 12 | protected XAPI: XAPI 13 | 14 | constructor(url: string, XAPI: XAPI) { 15 | super() 16 | this.url = url 17 | this.XAPI = XAPI 18 | this.addListener('handleMessage', (params: { 19 | command: string, error?: any, returnData?: any, time: Time, transactionId: string, json: string, socketId: string 20 | }) => { 21 | const transaction = this.transactions[params.transactionId] 22 | if (transaction) { 23 | const elapsedMs = transaction.state?.sent?.elapsedMs() 24 | if (params.error) { 25 | this.XAPI.counter.count(['error', 'SocketConnections', 'handleMessage']) 26 | elapsedMs !== undefined && this.XAPI.counter.count(['data', 'SocketConnection', 'responseTime', 'handleMessage', 27 | transaction.state.command || 'undefined_command'], 28 | elapsedMs 29 | ) 30 | this.XAPI.counter.count(['data', 'SocketConnection', 'responseTime2', 'handleMessage', transaction.state.command || 'undefined_command'], 31 | transaction.state.createdAt.elapsedMs() 32 | ) 33 | transaction.reject({ 34 | error: params.error, 35 | jsonReceived: params.time, 36 | json: params.json, 37 | }) 38 | } else { 39 | elapsedMs !== undefined && this.XAPI.counter.count(['data', 'SocketConnection', 'responseTime', 'handleMessage', 40 | transaction.state.command || 'undefined_command'], 41 | elapsedMs 42 | ) 43 | this.XAPI.counter.count(['data', 'SocketConnection', 'responseTime2', 'handleMessage', 44 | transaction.state.command || 'undefined_command'], 45 | transaction.state.createdAt.elapsedMs() 46 | ) 47 | transaction.resolve({ 48 | returnData: params.returnData, 49 | jsonReceived: params.time, 50 | json: params.json, 51 | }) 52 | this.callListener(`command_${params.command}`, [params.returnData, params.time, transaction, params.json, params.socketId]) 53 | } 54 | delete this.transactions[params.transactionId] 55 | } 56 | }) 57 | this.addListener('onClose', (socketId: string) => { 58 | for (const t of Object.values(this.transactions)) { 59 | if (t.state.socketId === socketId) { 60 | t.reject({ 61 | error: new Error('socket closed'), 62 | jsonReceived: null, 63 | json: null, 64 | }) 65 | if (t.state.transactionId) { 66 | delete this.transactions[t.state.transactionId] 67 | } 68 | } 69 | } 70 | delete this.connections[socketId] 71 | }) 72 | } 73 | 74 | public onClose(callback: (socketId: string, connection: SocketConnection) => void) { 75 | return this.addListener('onClose', (socketId: string, connection: SocketConnection) => { 76 | callback(socketId, connection) 77 | }) 78 | } 79 | 80 | public onOpen(callback: (socketId: string, connection: SocketConnection) => void) { 81 | return this.addListener('onOpen', (socketId: string, connection: SocketConnection) => { 82 | callback(socketId, connection) 83 | }) 84 | } 85 | 86 | socketIdIncrement = new Increment() 87 | public connect(timeoutMs: number): Promise { 88 | const socketId = `${new Date().getTime()}${this.socketIdIncrement.id}` 89 | this.connections[socketId] = new SocketConnection( 90 | this.url, 91 | (listenerId: string, params?: any[]) => this.callListener(listenerId, params), 92 | socketId, 93 | this.XAPI 94 | ) 95 | return this.connections[socketId].connect(timeoutMs) 96 | .then(() => socketId) 97 | } 98 | 99 | transactionIncrement = new Increment() 100 | protected createTransactionId() { 101 | return `${new Date().getTime()}${this.transactionIncrement.id}` 102 | } 103 | 104 | public getSocketId(): string | undefined { 105 | return Object.values(this.connections).map((connection) => { 106 | const times = connection.capacity.filter(i => i.elapsedMs() < 1500) 107 | return { 108 | point: times.length <= 4 ? times.length : (5 + (1500 - times[4].elapsedMs())), 109 | connection, 110 | } 111 | }).sort((a,b) => a.point - b.point)[0]?.connection?.socketId 112 | } 113 | 114 | protected sendCommand( 115 | command: string, 116 | args: any = {}, 117 | transactionId: string | null = null, 118 | priority = false, 119 | socketId?: string | undefined, 120 | // @ts-ignore 121 | ): Promise<{ 122 | transaction: Transaction 123 | data: { 124 | returnData: T 125 | jsonReceived: Time 126 | json: string 127 | } 128 | }> { 129 | if (!transactionId) { 130 | transactionId = this.createTransactionId() 131 | } 132 | 133 | if (!socketId) { 134 | socketId = this.getSocketId() 135 | } 136 | 137 | const t = this.transactions[transactionId] = new Transaction({ 138 | transactionId, 139 | command, 140 | json: JSON.stringify({ 141 | command, 142 | arguments: Object.keys(args).length === 0 ? undefined : args, 143 | customTag: `${command}_${transactionId}`, 144 | }), 145 | args, 146 | socketId, 147 | priority, 148 | }) 149 | 150 | if (socketId) { 151 | if (this.connections[socketId]) { 152 | this.XAPI.counter.count(['data', 'SocketConnections', 'sendCommand', command]) 153 | this.connections[socketId].send(this.transactions[transactionId]) 154 | .catch(error => { 155 | this.XAPI.counter.count(['error', 'SocketConnections', 'sendCommand', command]) 156 | // @ts-ignore: invalid warning look at #103_line 157 | if (this.transactions[transactionId]) { 158 | // @ts-ignore: invalid warning look at #103_line 159 | this.transactions[transactionId].reject(error) 160 | // @ts-ignore: invalid warning look at #103_line 161 | delete this.transactions[transactionId] 162 | } 163 | }) 164 | } else { 165 | this.transactions[transactionId].reject(new Error('invalid socketId')) 166 | delete this.transactions[transactionId] 167 | } 168 | } else { 169 | this.transactions[transactionId].reject(new Error('there is no connected socket')) 170 | delete this.transactions[transactionId] 171 | } 172 | 173 | return t.promise 174 | } 175 | } -------------------------------------------------------------------------------- /src/v2/interface/Definitions.ts: -------------------------------------------------------------------------------- 1 | import {CMD_FIELD, DAY_FIELD, PERIOD_FIELD, REQUEST_STATUS_FIELD, STATE_FIELD, TYPE_FIELD} from './Enum' 2 | 3 | export interface CHART_RANGE_INFO_RECORD { 4 | end: number 5 | period: PERIOD_FIELD 6 | start: number 7 | symbol: string 8 | ticks: number 9 | } 10 | 11 | export interface CHART_LAST_INFO_RECORD { 12 | period: PERIOD_FIELD 13 | start: number 14 | symbol: string 15 | } 16 | 17 | export interface SYMBOL_RECORD { 18 | currency: string 19 | time: number 20 | swap_rollover3days: number 21 | marginMaintenance: number 22 | marginHedged: number 23 | longOnly: boolean 24 | timeString: string 25 | categoryName: 'STC' | 'FX' | 'CRT' | 'ETF' | 'IND' | 'CMD' | string 26 | lotStep: number 27 | marginMode: number 28 | leverage: number 29 | marginHedgedStrong: boolean 30 | symbol: string 31 | quoteId: number 32 | groupName: string 33 | percentage: number 34 | swapShort: number 35 | tickValue: number 36 | bid: number 37 | quoteIdCross: number 38 | pipsPrecision: number 39 | swapType: number 40 | description: string 41 | precision: number 42 | trailingEnabled: boolean 43 | ask: number 44 | profitMode: number 45 | exemode: number 46 | instantMaxVolume: number 47 | high: number 48 | swapEnable: boolean 49 | initialMargin: number 50 | expiration: number 51 | spreadTable: number 52 | currencyPair: boolean 53 | shortSelling: boolean 54 | contractSize: number 55 | spreadRaw: number 56 | lotMin: number 57 | lotMax: number 58 | currencyProfit: string 59 | stopsLevel: number 60 | type: number 61 | starting: number 62 | stepRuleId: number 63 | swapLong: number 64 | low: number 65 | tickSize: number 66 | } 67 | 68 | export interface STREAMING_BALANCE_RECORD { 69 | balance: number 70 | credit: number 71 | equity: number 72 | margin: number 73 | marginFree: number 74 | marginLevel: number 75 | } 76 | 77 | export interface STREAMING_CANDLE_RECORD { 78 | close: number 79 | ctm: number 80 | ctmString: string 81 | high: number 82 | low: number 83 | open: number 84 | quoteId: number 85 | symbol: string 86 | vol: number 87 | } 88 | 89 | export interface STREAMING_TRADE_STATUS_RECORD { 90 | customComment: string | null 91 | message: string | null 92 | order: number 93 | price: number | null | undefined // TODO check undefined is possible or not 94 | requestStatus: REQUEST_STATUS_FIELD | null 95 | } 96 | 97 | export interface CALENDAR_RECORD { 98 | country: string 99 | current: string 100 | forecast: string 101 | impact: string 102 | period: string 103 | previous: string 104 | time: number 105 | title: string 106 | } 107 | 108 | export interface RATE_INFO_RECORD { 109 | close: number 110 | ctm: number 111 | ctmString: string 112 | high: number 113 | low: number 114 | open: number 115 | vol: number 116 | } 117 | 118 | export interface IB_RECORD { 119 | closePrice: number 120 | login: number 121 | nominal: number 122 | openPrice: number 123 | side: number 124 | surname: string 125 | symbol: string 126 | timestamp: number 127 | volume: string 128 | } 129 | 130 | export interface NEWS_TOPIC_RECORD { 131 | body: string 132 | bodylen: number 133 | key: string 134 | time: number 135 | timeString: string 136 | title: string 137 | } 138 | 139 | export interface STEP_RULE_RECORD { 140 | id: number 141 | name: string 142 | steps: STEP_RECORD[] 143 | } 144 | 145 | export interface STEP_RECORD { 146 | fromValue: number 147 | step: number 148 | } 149 | 150 | export interface TICK_RECORD { 151 | ask: number 152 | askVolume: number 153 | bid: number 154 | bidVolume: number 155 | high: number 156 | level: number 157 | low: number 158 | spreadRaw: number 159 | spreadTable: number 160 | symbol: string 161 | timestamp: number 162 | } 163 | 164 | export interface TRADING_HOURS_RECORD { 165 | quotes: QUOTES_RECORD[] 166 | symbol: string 167 | trading: TRADING_RECORD[] 168 | } 169 | 170 | export interface QUOTES_RECORD { 171 | day: DAY_FIELD 172 | fromT: number 173 | toT: number 174 | } 175 | 176 | export interface TRADING_RECORD { 177 | day: DAY_FIELD 178 | fromT: number 179 | toT: number 180 | } 181 | 182 | export interface TRADE_TRANS_INFO { 183 | cmd: CMD_FIELD 184 | customComment: string | null 185 | expiration: number | Date 186 | offset: number 187 | order: number 188 | price: number 189 | sl: number 190 | symbol: string 191 | tp: number 192 | type: TYPE_FIELD 193 | volume: number 194 | } 195 | 196 | export interface TRADE_TRANS_INFO_MODIFY { 197 | cmd?: CMD_FIELD 198 | customComment?: string | null 199 | expiration?: number | Date | undefined 200 | offset?: number | undefined 201 | order?: number 202 | price?: number 203 | sl?: number | undefined 204 | symbol?: string 205 | tp?: number | undefined 206 | type: TYPE_FIELD.MODIFY 207 | volume?: number 208 | } 209 | 210 | export interface TRADE_TRANS_INFO_CLOSE { 211 | cmd?: CMD_FIELD 212 | customComment?: string | null 213 | expiration?: number | Date 214 | offset?: number | undefined 215 | order: number 216 | price: number 217 | sl?: number | undefined 218 | symbol: string 219 | tp?: number | undefined 220 | type: TYPE_FIELD.CLOSE 221 | volume: number 222 | } 223 | 224 | export interface TRADE_TRANS_INFO_DELETE { 225 | cmd?: CMD_FIELD 226 | customComment?: string | null 227 | expiration?: number | Date 228 | offset?: number | undefined 229 | order: number 230 | price?: number 231 | sl?: number | undefined 232 | symbol: string 233 | tp?: number | undefined 234 | type: TYPE_FIELD.DELETE 235 | volume?: number 236 | } 237 | 238 | export interface STREAMING_KEEP_ALIVE_RECORD { 239 | timestamp: number 240 | } 241 | 242 | export interface STREAMING_NEWS_RECORD { 243 | body: string 244 | key: string 245 | time: number 246 | title: string 247 | } 248 | 249 | export interface STREAMING_PROFIT_RECORD { 250 | order: number 251 | order2: number 252 | position: number 253 | profit: number 254 | } 255 | 256 | export interface STREAMING_TICK_RECORD { 257 | ask: number 258 | askVolume: number 259 | bid: number 260 | bidVolume: number 261 | high: number 262 | level: number 263 | low: number 264 | quoteId: number 265 | spreadRaw: number 266 | spreadTable: number 267 | symbol: string 268 | timestamp: number 269 | } 270 | 271 | export interface TRADE_RECORD { 272 | close_price: number 273 | close_time: number 274 | closed: boolean 275 | cmd: CMD_FIELD 276 | comment: string 277 | commission: number 278 | customComment: string 279 | digits: number 280 | expiration: number 281 | margin_rate: number 282 | offset: number 283 | open_price: number 284 | open_time: number 285 | order: number 286 | order2: number 287 | position: number 288 | profit: number 289 | sl: number 290 | storage: number 291 | symbol: string 292 | tp: number 293 | volume: number 294 | 295 | timestamp?: number 296 | open_timeString?: string 297 | close_timeString?: string 298 | expirationString?: string 299 | 300 | type?: TYPE_FIELD 301 | state?: STATE_FIELD 302 | } 303 | 304 | export interface STREAMING_TRADE_RECORD { 305 | close_price: number 306 | close_time: number 307 | closed: boolean 308 | cmd: CMD_FIELD 309 | comment: string 310 | commission: number 311 | customComment: string 312 | digits: number 313 | expiration: number 314 | margin_rate: number 315 | offset: number 316 | open_price: number 317 | open_time: number 318 | order: number 319 | order2: number 320 | position: number 321 | profit: number 322 | sl: number 323 | storage: number 324 | symbol: string 325 | tp: number 326 | volume: number 327 | 328 | type: TYPE_FIELD 329 | state: STATE_FIELD 330 | } -------------------------------------------------------------------------------- /src/v2/core/Stream/StreamConnection.ts: -------------------------------------------------------------------------------- 1 | import {WebSocketWrapper} from '../../utils/WebSocketWrapper' 2 | import {Time} from "../../utils/Time" 3 | import {Transaction} from "../Transaction" 4 | import {Timer} from "../../utils/Timer" 5 | import {sleep} from "../../utils/sleep" 6 | import {XAPI} from "../XAPI" 7 | 8 | export class StreamConnection { 9 | public connectedTime: Time | null = null 10 | public lastReceivedMessage: Time | null = null 11 | public capacity: Time[] = [] 12 | protected WebSocket: WebSocketWrapper 13 | private session: string 14 | public socketId: string 15 | public streamId: string 16 | private queue: { transaction: Transaction }[] = [] 17 | private queueTimer: Timer = new Timer() 18 | private callListener: (listenerId: string, params?: any[]) => void 19 | private connectionProgress: Transaction | null = null 20 | private disconnectionProgress: Transaction | null = null 21 | private XAPI: XAPI 22 | 23 | constructor(url: string, session: string, callListener: (listenerId: string, params?: any[]) => void, streamId: string, socketId: string, XAPI: XAPI) { 24 | this.session = session 25 | this.socketId = socketId 26 | this.streamId = streamId 27 | this.callListener = callListener 28 | this.XAPI = XAPI 29 | this.WebSocket = new WebSocketWrapper(url) 30 | 31 | const pingTimer = new Timer() 32 | this.WebSocket.onOpen(() => { 33 | this.connectedTime = new Time() 34 | this.connectionProgress?.resolve() 35 | this.disconnectionProgress?.reject(new Error('onOpen')) 36 | pingTimer.setInterval(() => { 37 | this.ping().catch(() => {}) 38 | }, 14500) 39 | this.callListener('onOpen', [streamId, this]) 40 | }) 41 | this.WebSocket.onClose(() => { 42 | this.connectedTime = null 43 | this.disconnectionProgress?.resolve() 44 | this.connectionProgress?.reject(new Error('onClose')) 45 | pingTimer.clear() 46 | this.callListener('onClose', [streamId, this]) 47 | }) 48 | 49 | this.WebSocket.onMessage((json: any) => { 50 | this.lastReceivedMessage = new Time() 51 | try { 52 | const message = JSON.parse(json.toString().trim()) 53 | this.XAPI.counter.count(['data', 'StreamConnection', 'incomingData'], json.length) 54 | 55 | this.callListener(`command_${message.command}`, [message.data, new Time(), json, streamId]) 56 | } catch (e) { 57 | this.XAPI.counter.count(['error', 'StreamConnection', 'handleMessage']) 58 | this.callListener(`handleMessage`, [{error: e, time: new Time(), json, streamId}]) 59 | } 60 | }) 61 | 62 | this.WebSocket.onError((error: any) => { 63 | this.connectionProgress && this.connectionProgress.reject(error) 64 | this.callListener(`handleMessage`, [{error, time: new Time(), json: null, streamId}]) 65 | }) 66 | } 67 | 68 | public get status(): 'CONNECTING' | 'CONNECTED' | 'DISCONNECTED' { 69 | return this.WebSocket.connecting ? 'CONNECTING' : (this.WebSocket.status ? 'CONNECTED' : 'DISCONNECTED') 70 | } 71 | 72 | public connect(timeoutMs: number) { 73 | if (this.WebSocket.status) { 74 | throw new Error('already connected') 75 | } 76 | if (this.connectionProgress) { 77 | return this.connectionProgress.promise 78 | } 79 | const t = new Transaction() 80 | const timer = new Timer() 81 | timer.setTimeout(() => { 82 | this.connectionProgress?.reject(new Error('timeout')) 83 | this.close() 84 | }, timeoutMs) 85 | this.connectionProgress = t 86 | this.WebSocket.connect() 87 | return t.promise.catch(e => { 88 | timer.clear() 89 | this.connectionProgress = null 90 | throw e 91 | }).then((r) => { 92 | timer.clear() 93 | this.connectionProgress = null 94 | this.ping().catch(() => {}) 95 | return r 96 | }) 97 | } 98 | 99 | public close() { 100 | if (!this.WebSocket.status && !this.WebSocket.connecting) { 101 | return Promise.resolve() 102 | } 103 | if (this.disconnectionProgress) { 104 | return this.disconnectionProgress.promise 105 | } 106 | const t = new Transaction() 107 | this.disconnectionProgress = t 108 | this.WebSocket.close() 109 | return t.promise.catch(e => { 110 | this.disconnectionProgress = null 111 | throw e 112 | }).then((r) => { 113 | this.disconnectionProgress = null 114 | return r 115 | }) 116 | } 117 | 118 | public ping() { 119 | return this.sendCommand('ping', {}) 120 | } 121 | 122 | public async sendCommand(command: string, completion: Record = {}): Promise<{ transaction: Transaction<{json: string},{sent?: Time}>, data: any}> { 123 | const t = new Transaction<{json: string},{sent?: Time}>({ 124 | json: JSON.stringify({ 125 | command, 126 | streamSessionId: this.session, 127 | ...completion, 128 | }), 129 | }) 130 | this.XAPI.counter.count(['data', 'StreamConnection', 'sendCommand', command]) 131 | return this.send(t) 132 | } 133 | 134 | private async cleanQueue() { 135 | for (; this.queue.length > 0;) { 136 | if (this.capacity[4].elapsedMs() < 1000) { 137 | break 138 | } 139 | const jsons = this.queue.splice(0, 1) 140 | if (jsons.length === 1) { 141 | if (jsons[0].transaction.state.createdAt.elapsedMs() > 9000) { 142 | jsons[0].transaction.reject(new Error('queue overloaded')) 143 | } else { 144 | try { 145 | this.send(jsons[0].transaction) 146 | if (this.queue.length > 0) { 147 | await sleep(250) 148 | } 149 | } catch (e) { 150 | } 151 | } 152 | } else { 153 | break 154 | } 155 | } 156 | } 157 | 158 | private callCleaner(elapsedMs: number): Promise { 159 | return this.queueTimer.setTimeout(async () => { 160 | await this.cleanQueue() 161 | if (this.queue.length > 0) { 162 | return await this.callCleaner(this.capacity[4].elapsedMs()) 163 | } 164 | return undefined 165 | }, 1000 - elapsedMs) 166 | } 167 | 168 | protected async send(transaction: Transaction<{json: string},{sent?: Time}>): Promise<{ transaction: Transaction<{json: string},{sent?: Time}>, data: any}> { 169 | if (transaction.state.json.length > 1000) { 170 | transaction.reject(new Error('Each command invocation should not contain more than 1kB of data.')) 171 | return transaction.promise 172 | } 173 | try { 174 | const elapsedMs = this.capacity.length > 4 ? this.capacity[4].elapsedMs() : 1001 175 | if (elapsedMs < 1000) { 176 | this.queue.push({transaction}) 177 | this.queueTimer.isNull() && await this.callCleaner(elapsedMs) 178 | return transaction.promise 179 | } 180 | const time: Time = new Time() 181 | if (this.capacity.length > 20) { 182 | this.capacity = [time, ...this.capacity.slice(0, 4)] 183 | } else { 184 | this.capacity.unshift(time) 185 | } 186 | await this.WebSocket.send(transaction.state.json) 187 | transaction.setState({ 188 | sent: new Time() 189 | }) 190 | this.XAPI.counter.count(['data', 'StreamConnection', 'outgoingData'], transaction.state.json.length) 191 | transaction.resolve(time) 192 | } catch (e) { 193 | transaction.reject(e) 194 | } 195 | return transaction.promise 196 | } 197 | } -------------------------------------------------------------------------------- /src/v2/core/Socket/SocketConnection.ts: -------------------------------------------------------------------------------- 1 | import {WebSocketWrapper} from '../../utils/WebSocketWrapper' 2 | import {Time} from '../../utils/Time' 3 | import {parseCustomTag} from '../../utils/parseCustomTag' 4 | import {Transaction} from '../Transaction' 5 | import {Timer} from "../../utils/Timer" 6 | import {createPromise, PromiseObject} from "../../utils/createPromise" 7 | import {sleep} from "../../utils/sleep" 8 | import {XAPI} from "../XAPI" 9 | 10 | export class SocketConnection { 11 | public connectedTime: Time | null = null 12 | public lastReceivedMessage: Time | null = null 13 | public capacity: Time[] = [] 14 | public loggedIn: boolean 15 | public streamId: string 16 | public socketId: string 17 | private queue: { transaction: Transaction, promise: PromiseObject }[] = [] 18 | private queueTimer: Timer = new Timer() 19 | protected WebSocket: WebSocketWrapper 20 | private callListener: (listenerId: string, params?: any[]) => void 21 | private connectionProgress: Transaction | null = null 22 | private disconnectionProgress: Transaction | null = null 23 | private XAPI: XAPI 24 | 25 | constructor(url: string, callListener: (listenerId: string, params?: any[]) => void, socketId: string, XAPI: XAPI) { 26 | this.socketId = socketId 27 | this.callListener = callListener 28 | this.XAPI = XAPI 29 | this.WebSocket = new WebSocketWrapper(url) 30 | 31 | const pingTimer = new Timer() 32 | this.WebSocket.onOpen(() => { 33 | this.connectedTime = new Time() 34 | this.connectionProgress?.resolve() 35 | this.disconnectionProgress?.reject(new Error('onOpen')) 36 | pingTimer.setInterval(() => { 37 | this.status === 'CONNECTED' && this.loggedIn && this.XAPI.Socket.send.ping(this.socketId) 38 | .catch(() => {}) 39 | }, 14500) 40 | this.callListener('onOpen', [socketId, this]) 41 | }) 42 | this.WebSocket.onClose(() => { 43 | this.connectedTime = null 44 | this.disconnectionProgress?.resolve() 45 | this.connectionProgress?.reject(new Error('onClose')) 46 | pingTimer.clear() 47 | this.callListener('onClose', [socketId, this]) 48 | }) 49 | 50 | this.WebSocket.onMessage((json: any) => { 51 | this.lastReceivedMessage = new Time() 52 | try { 53 | const message = JSON.parse(json.toString().trim()) 54 | this.XAPI.counter.count(['data', 'SocketConnection', 'incomingData'], json.length) 55 | 56 | this.handleMessage(message, new Time(), json, socketId) 57 | } catch (e) { 58 | this.XAPI.counter.count(['error', 'SocketConnection', 'handleMessage']) 59 | this.callListener('handleMessage', [{ 60 | command: null, 61 | error: e, 62 | time: new Time(), 63 | transactionId: null, 64 | json, 65 | socketId, 66 | }]) 67 | } 68 | }) 69 | 70 | this.WebSocket.onError((error: any) => { 71 | this.connectionProgress && this.connectionProgress.reject(error) 72 | this.callListener('handleMessage', [{ 73 | command: null, 74 | error, 75 | time: new Time(), 76 | transactionId: null, 77 | json: null, 78 | socketId, 79 | }]) 80 | }) 81 | } 82 | 83 | public get status(): 'CONNECTING' | 'CONNECTED' | 'DISCONNECTED' { 84 | return this.WebSocket.connecting ? 'CONNECTING' : (this.WebSocket.status ? 'CONNECTED' : 'DISCONNECTED') 85 | } 86 | 87 | public connect(timeoutMs: number) { 88 | if (this.WebSocket.status) { 89 | throw new Error('already connected') 90 | } 91 | if (this.connectionProgress) { 92 | return this.connectionProgress.promise 93 | } 94 | const t = new Transaction() 95 | const timer = new Timer() 96 | timer.setTimeout(() => { 97 | this.connectionProgress?.reject(new Error('timeout')) 98 | this.close() 99 | }, timeoutMs) 100 | this.connectionProgress = t 101 | this.WebSocket.connect() 102 | return t.promise.catch(e => { 103 | timer.clear() 104 | this.connectionProgress = null 105 | throw e 106 | }).then((r) => { 107 | timer.clear() 108 | this.connectionProgress = null 109 | return r 110 | }) 111 | } 112 | 113 | public close() { 114 | if (!this.WebSocket.status && !this.WebSocket.connecting) { 115 | return Promise.resolve() 116 | } 117 | if (this.disconnectionProgress) { 118 | return this.disconnectionProgress.promise 119 | } 120 | const t = new Transaction() 121 | this.disconnectionProgress = t 122 | this.WebSocket.close() 123 | return t.promise.catch(e => { 124 | this.disconnectionProgress = null 125 | throw e 126 | }).then((r) => { 127 | this.disconnectionProgress = null 128 | return r 129 | }) 130 | } 131 | 132 | private async cleanQueue() { 133 | for (; this.queue.length > 0;) { 134 | if (this.capacity[4].elapsedMs() < 1000) { 135 | break 136 | } 137 | const jsons = this.queue.splice(0, 1) 138 | if (jsons.length === 1) { 139 | if (jsons[0].transaction.state.createdAt.elapsedMs() > 9000) { 140 | jsons[0].promise.reject(new Error('timeout due to queue overloaded')) 141 | this.XAPI.counter.count(['data', 'SocketConnection', 'send', 'queue', 'timeout'], 142 | 1 143 | ) 144 | } else { 145 | try { 146 | this.send(jsons[0].transaction, jsons[0].promise) 147 | if (this.queue.length > 0) { 148 | await sleep(250) 149 | } 150 | } catch (e) { 151 | } 152 | } 153 | } else { 154 | break 155 | } 156 | } 157 | } 158 | 159 | private callCleaner(elapsedMs: number): Promise { 160 | return this.queueTimer.setTimeout(async () => { 161 | await this.cleanQueue() 162 | if (this.queue.length > 0) { 163 | return await this.callCleaner(this.capacity[4].elapsedMs()) 164 | } 165 | return undefined 166 | }, 1000 - elapsedMs) 167 | } 168 | 169 | public async send(transaction: Transaction, promise?: PromiseObject