diff --git a/cs/ccxt/api/alephx.cs b/cs/ccxt/api/alephx.cs new file mode 100644 index 0000000000000..bbfd12c5b2fb9 --- /dev/null +++ b/cs/ccxt/api/alephx.cs @@ -0,0 +1,49 @@ +// ------------------------------------------------------------------------------- + +// PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +// https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +// ------------------------------------------------------------------------------- + +namespace ccxt; + +public partial class alephx : Exchange +{ + public alephx (object args = null): base(args) {} + + public async Task v1PublicGetSystemStatus (object parameters = null) + { + return await this.callAsync ("v1PublicGetSystemStatus",parameters); + } + + public async Task v1PrivateGetAssetsBalances (object parameters = null) + { + return await this.callAsync ("v1PrivateGetAssetsBalances",parameters); + } + + public async Task v1PrivateGetOrders (object parameters = null) + { + return await this.callAsync ("v1PrivateGetOrders",parameters); + } + + public async Task v1PrivateGetOrdersId (object parameters = null) + { + return await this.callAsync ("v1PrivateGetOrdersId",parameters); + } + + public async Task v1PrivateGetTrades (object parameters = null) + { + return await this.callAsync ("v1PrivateGetTrades",parameters); + } + + public async Task v1PrivatePostOrders (object parameters = null) + { + return await this.callAsync ("v1PrivatePostOrders",parameters); + } + + public async Task v1PrivatePatchOrdersIdCancel (object parameters = null) + { + return await this.callAsync ("v1PrivatePatchOrdersIdCancel",parameters); + } + +} \ No newline at end of file diff --git a/dist/cjs/ccxt.js b/dist/cjs/ccxt.js index da5c085f8274d..25c4443394d85 100644 --- a/dist/cjs/ccxt.js +++ b/dist/cjs/ccxt.js @@ -13,6 +13,7 @@ var Precise = require('./src/base/Precise.js'); var functions = require('./src/base/functions.js'); var errors = require('./src/base/errors.js'); var ace = require('./src/ace.js'); +var alephx = require('./src/alephx.js'); var alpaca = require('./src/alpaca.js'); var ascendex = require('./src/ascendex.js'); var bequant = require('./src/bequant.js'); @@ -122,6 +123,7 @@ var xt = require('./src/xt.js'); var yobit = require('./src/yobit.js'); var zaif = require('./src/zaif.js'); var zonda = require('./src/zonda.js'); +var alephx$1 = require('./src/pro/alephx.js'); var alpaca$1 = require('./src/pro/alpaca.js'); var ascendex$1 = require('./src/pro/ascendex.js'); var bequant$1 = require('./src/pro/bequant.js'); @@ -202,6 +204,7 @@ const version = '4.4.34'; Exchange["default"].ccxtVersion = version; const exchanges = { 'ace': ace, + 'alephx': alephx, 'alpaca': alpaca, 'ascendex': ascendex, 'bequant': bequant, @@ -313,6 +316,7 @@ const exchanges = { 'zonda': zonda, }; const pro = { + 'alephx': alephx$1, 'alpaca': alpaca$1, 'ascendex': ascendex$1, 'bequant': bequant$1, @@ -438,6 +442,7 @@ exports.RequestTimeout = errors.RequestTimeout; exports.UnsubscribeError = errors.UnsubscribeError; exports.errors = errors; exports.ace = ace; +exports.alephx = alephx; exports.alpaca = alpaca; exports.ascendex = ascendex; exports.bequant = bequant; diff --git a/dist/cjs/src/abstract/alephx.js b/dist/cjs/src/abstract/alephx.js new file mode 100644 index 0000000000000..7e4804edc24ab --- /dev/null +++ b/dist/cjs/src/abstract/alephx.js @@ -0,0 +1,9 @@ +'use strict'; + +var Exchange$1 = require('../base/Exchange.js'); + +// ------------------------------------------------------------------------------- +class Exchange extends Exchange$1["default"] { +} + +module.exports = Exchange; diff --git a/dist/cjs/src/alephx.js b/dist/cjs/src/alephx.js new file mode 100644 index 0000000000000..f34e1fca3c1b9 --- /dev/null +++ b/dist/cjs/src/alephx.js @@ -0,0 +1,641 @@ +'use strict'; + +var alephx$1 = require('./abstract/alephx.js'); +var errors = require('./base/errors.js'); +var sha256 = require('./static_dependencies/noble-hashes/sha256.js'); + +// ---------------------------------------------------------------------------- +// ---------------------------------------------------------------------------- +/** + * @class alephx + * @augments Exchange + */ +class alephx extends alephx$1 { + describe() { + return this.deepExtend(super.describe(), { + 'id': 'alephx', + 'name': 'AlephX', + 'countries': ['CA'], + 'pro': true, + 'certified': false, + // rate-limits: N/A + 'rateLimit': 1000, + 'version': 'v1', + 'userAgent': this.userAgents['chrome'], + // 'headers': { + // 'ZKX-VERSION': '2018-05-30', + // }, + 'has': { + 'CORS': true, + 'spot': true, + 'margin': false, + 'swap': false, + 'future': false, + 'option': false, + 'addMargin': false, + 'cancelOrder': true, + 'cancelOrders': false, + 'createDepositAddress': false, + 'createLimitBuyOrder': false, + 'createLimitSellOrder': false, + 'createMarketBuyOrder': false, + 'createMarketBuyOrderWithCost': false, + 'createMarketOrderWithCost': false, + 'createMarketSellOrder': false, + 'createMarketSellOrderWithCost': false, + 'createOrder': true, + 'createPostOnlyOrder': false, + 'createReduceOnlyOrder': false, + 'createStopLimitOrder': false, + 'createStopMarketOrder': false, + 'createStopOrder': true, + 'deposit': false, + 'editOrder': false, + 'fetchAccounts': false, + 'fetchBalance': true, + 'fetchBidsAsks': false, + 'fetchCanceledOrders': false, + 'fetchCurrencies': false, + 'fetchDeposit': false, + 'fetchDepositAddress': false, + 'fetchDepositAddresses': false, + 'fetchDepositAddressesByNetwork': false, + 'fetchDeposits': false, + 'fetchFundingHistory': false, + 'fetchFundingRate': false, + 'fetchFundingRateHistory': false, + 'fetchFundingRates': false, + 'fetchIndexOHLCV': false, + 'fetchIsolatedBorrowRate': false, + 'fetchIsolatedBorrowRates': false, + 'fetchL2OrderBook': false, + 'fetchLedger': false, + 'fetchLeverage': false, + 'fetchLeverageTiers': false, + 'fetchMarginMode': false, + 'fetchMarkets': false, + 'fetchMarkOHLCV': false, + 'fetchMyBuys': false, + 'fetchMySells': false, + 'fetchMyTrades': true, + 'fetchOHLCV': false, + 'fetchOpenInterestHistory': false, + 'fetchOpenOrders': false, + 'fetchOrder': true, + 'fetchOrderBook': false, + 'fetchOrders': true, + 'fetchOrderTrades': true, + 'fetchPosition': false, + 'fetchPositionMode': false, + 'fetchPositions': false, + 'fetchPositionsRisk': false, + 'fetchPremiumIndexOHLCV': false, + 'fetchStatus': true, + 'fetchTicker': false, + 'fetchTickers': false, + 'fetchTime': false, + 'fetchTrades': false, + 'fetchTradingFee': 'emulated', + 'fetchTradingFees': false, + 'fetchWithdrawals': false, + 'reduceMargin': false, + 'setLeverage': false, + 'setMarginMode': false, + 'setPositionMode': false, + 'withdraw': false, + }, + 'urls': { + // 'logo': 'https://user-images.githubusercontent.com/1294454/40811661-b6eceae2-653a-11e8-829e-10bfadb078cf.jpg', + 'api': { + 'rest': 'https://api.alephx.xyz', + }, + 'www': 'https://demo.alephx.xyz', + // 'doc': [ + // 'https://developers.alephx.com/api/v1', + // ], + // 'fees': [ + // 'https://support.alephx.com/customer/portal/articles/2109597-buy-sell-bank-transfer-fees', + // ], + }, + 'requiredCredentials': { + 'apiKey': true, + 'secret': true, + }, + 'api': { + 'v1': { + 'public': { + 'get': { + 'system/status': 0, + }, + }, + 'private': { + 'get': { + 'assets/balances': 0, + 'orders': 0, + 'orders/{id}': 0, + 'trades': 0, + }, + 'post': { + 'orders': 0, + }, + 'patch': { + 'orders/{id}/cancel': 0, + }, + }, + }, + }, + 'exceptions': { + 'exact': {}, + 'broad': { + 'Wallet not allowed': errors.AuthenticationError, + 'Invalid signature': errors.AuthenticationError, + 'Unauthorized': errors.PermissionDenied, + 'Order is not cancellable': errors.BadRequest, + 'Asset is not supported': errors.BadRequest, + 'Not Found': errors.OrderNotFound, + }, + }, + }); + } + async createOrder(symbol, type, side, amount, price = undefined, params = {}) { + /** + * @method + * @name alephx#createOrder + * @description create an order + * @see POST https://api.alephx.xyz/api/v1/orders + * @param {string} symbol unified symbol of the market to create an order in + * @param {string} type 'market' or 'limit' + * @param {string} side 'buy' or 'sell' + * @param {float} amount how much you want to trade in units of the base currency, quote currency for 'market' 'buy' orders + * @param {float} [price] the price to fulfill the order, in units of the quote currency, ignored in market orders + * @param {object} [params] extra parameters specific to the exchange API endpoint + * @param {string} [params.timeInForce] 'gtc' + * @param {string} [params.idempotencyKey] uuid for idempotency key + * @returns {object} an [order structure]{@link https://docs.ccxt.com/#/?id=order-structure} + */ + const request = { + 'symbol': symbol, + 'type': type, + 'side': side, + 'quantity': amount.toString(), + 'price': price.toString(), + 'time_in_force': this.safeString2(params, 'timeInForce', 'gtc'), + 'idempotency_key': this.safeString2(params, 'idempotencyKey', this.uuid()), + }; + const response = await this.v1PrivatePostOrders(request); + // + // successful order + // + // + // failed order + // + // + const errorResponse = this.safeDict(response, 'error'); + if (errorResponse !== undefined) { + const errorReason = this.safeString(errorResponse, 'reason'); + const errorMessage = this.safeString(errorResponse, 'message'); + throw new errors.ExchangeError(errorReason + '' + errorMessage); + } + return this.parseOrder(response); + } + parseOrder(order, market = undefined) { + // + // createOrder + // + // { + // "order_id": "52cfe5e2-0b29-4c19-a245-a6a773de5030", + // "status": "pending_new" + // } + // + // + // fetchOrder, fetchOrders, cancelOrder + // + // { + // "id": "0da4eb8d-c108-4e6c-8c45-0b42fabd3a72", + // "status": "partially_filled", + // "type": "limit", + // "symbol": "CLEO-ALEO", + // "account_id": "cb77b9ab-f94d-4013-85b7-644b0b9ba9a9", + // "settled_quantity": "0", + // "base_quantity": "0.1", + // "filled_quantity": "0.04", + // "side": "buy", + // "price": "12.3", + // "remained_quantity": "0.06", + // "idempotency_key": "99888999-93ef-9831-9829-120a082bfcf2", + // "inserted_at": "2024-09-16T23:47:45.161888Z", + // "fee_asset":null, + // "filled_at": "2024-09-26T20:08:11.350542Z", + // "average_filled_price": "12.3", + // "canceled_at":null,"cumulative_fee": "0", + // "time_in_force": "gtc", + // "internal_status": "partially_filled" + // } + // + const createdDateTime = this.safeString(order, 'inserted_at'); + const filledDateTime = this.safeString(order, 'filled_at'); + return this.safeOrder({ + 'info': order, + 'id': this.safeString(order, 'order_id') || this.safeString(order, 'id'), + 'clientOrderId': this.safeString(order, 'idempotency_key'), + 'timestamp': createdDateTime ? this.parse8601(createdDateTime) : undefined, + 'datetime': createdDateTime, + 'lastTradeTimestamp': filledDateTime ? this.parse8601(filledDateTime) : undefined, + 'symbol': this.safeString(order, 'symbol'), + 'type': this.safeString(order, 'type'), + 'timeInForce': this.safeString(order, 'time_in_force', 'gtc'), + 'postOnly': true, + 'side': this.safeStringLower(order, 'side'), + 'price': this.safeString(order, 'price'), + 'stopPrice': undefined, + 'triggerPrice': undefined, + 'amount': this.safeString(order, 'base_quantity'), + 'filled': this.safeString(order, 'filled_quantity'), + 'remaining': this.safeString(order, 'remained_quantity'), + 'cost': undefined, + 'average': this.safeString(order, 'average_filled_price'), + 'status': this.safeString(order, 'status'), + 'fee': { + 'cost': this.safeString(order, 'cumulative_fee'), + 'currency': this.safeString(order, 'fee_asset'), + }, + 'trades': undefined, + }, market); + } + async cancelOrder(id, symbol = undefined, params = {}) { + /** + * @method + * @name alephx#cancelOrder + * @description cancels an open order + * @see PATCH https://api.alephx.xyz/api/v1/orders/{order_id}/cancel + * @param {string} id order id + * @param {string} symbol not used by alephx cancelOrder() + * @param {object} [params] extra parameters specific to the exchange API endpoint + * @returns {object} An [order structure]{@link https://docs.ccxt.com/#/?id=order-structure} + */ + const request = { + 'id': id, // order id + }; + const response = await this.v1PrivatePatchOrdersIdCancel(this.extend(request, params)); + const errorResponse = this.safeDict(response, 'error'); + if (errorResponse !== undefined) { + const errorReason = this.safeString(errorResponse, 'reason'); + const errorMessage = this.safeString(errorResponse, 'message'); + if (errorReason === 'Not Found') { + throw new errors.OrderNotFound(this.id + ' cancelOrder() error ' + errorReason); + } + else if (errorReason === 'Bad Request') { + throw new errors.BadRequest(this.id + ' cancelOrder() error ' + errorReason + ' ' + errorMessage); + } + else { + throw new errors.ExchangeError(errorReason + '' + errorMessage); + } + } + return this.parseOrder(response); + } + async fetchOrder(id, symbol = undefined, params = {}) { + /** + * @method + * @name alephx#fetchOrder + * @description fetches information on an order made by the user + * @see GET https://api.alephx.xyz/api/v1/orders/{order_id} + * @param {string} id the order id + * @param {string} symbol unified market symbol that the order was made in + * @param {object} [params] extra parameters specific to the exchange API endpoint + * @returns {object} An [order structure]{@link https://docs.ccxt.com/#/?id=order-structure} + */ + const request = { + 'id': id, // order id + }; + const response = await this.v1PrivateGetOrdersId(this.extend(request, params)); + const errorResponse = this.safeDict(response, 'error'); + if (errorResponse !== undefined) { + const errorReason = this.safeString(errorResponse, 'reason'); + if (errorReason === 'Not Found') { + throw new errors.OrderNotFound(this.id + ' fetchOrder() error ' + errorReason); + } + } + return this.parseOrder(response); + } + async fetchOrders(symbol = undefined, since = undefined, limit = 100, params = {}) { + /** + * @method + * @name alephx#fetchOrders + * @description fetches information on multiple orders made by the user + * @see GET https://api.alephx.xyz/api/v1/orders/ + * @param {string} symbol unified market symbol that the orders were made in + * @param {int} [since] the earliest time in ms to fetch orders + * @param {int} [limit] the maximum number of order structures to retrieve + * @param {object} [params] extra parameters specific to the exchange API endpoint + * @param {int} [params.until] the latest time in ms to fetch trades for + * @param {boolean} [params.paginate] default false, when true will automatically paginate by calling this endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + * @returns {Order[]} a list of [order structures]{@link https://docs.ccxt.com/#/?id=order-structure} + */ + const response = await this.v1PrivateGetOrders(); + const market = undefined; + return this.parseOrders(response, market, since, limit); + } + async fetchMyTrades(symbol = undefined, since = undefined, limit = undefined, params = {}) { + /** + * @method + * @name alephx#fetchMyTrades + * @description fetch all trades made by the user + * @see GET https://api.alephx.xyz/api/v1/trades + * @param {string} symbol unified market symbol of the trades + * @param {int} [since] timestamp in ms of the earliest order, default is undefined + * @param {int} [limit] the maximum number of trade structures to fetch + * @param {object} [params] extra parameters specific to the exchange API endpoint + * @param {int} [params.until] the latest time in ms to fetch trades for + * @param {boolean} [params.paginate] default false, when true will automatically paginate by calling this endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + * @returns {Trade[]} a list of [trade structures]{@link https://docs.ccxt.com/#/?id=trade-structure} + */ + const response = await this.v1PrivateGetTrades(); + const trades = this.safeList(response, 'data'); + const market = undefined; + // + // { "data": [ + // { "id": "32672029-b46b-4139-9779-95444053f40a", + // "status": "unsettled", + // "symbol": "CLEO-ALEO", + // "base_quantity": "0.01", + // "side": "buy", + // "price": "12.3", + // "buy_order_id": "0da4eb8d-c108-4e6c-8c45-0b42fabd3a72", + // "sell_order_id": "86c61562-ff14-43c9-9a03-4be804d184d0", + // "quote_quantity": "0.123", + // "inserted_at": "2024-09-26T15:18:06.603489Z", + // "aggressor_side": "sell", + // "fee": null, + // "fee_asset": null, + // "updated_at": "2024-09-26T15:18:06.603489Z" + // }]} + // + return this.parseTrades(trades, market, since, limit); + } + parseTrade(trade, market = undefined) { + // returned trade + // + // [ + // { + // id: '32672029-b46b-4139-9779-95444053f40a', + // order: '0da4eb8d-c108-4e6c-8c45-0b42fabd3a72', + // info: { + // id: '32672029-b46b-4139-9779-95444053f40a', + // status: 'unsettled', + // symbol: 'CLEO-ALEO', + // base_quantity: '0.01', + // side: 'buy', + // price: '12.3', + // buy_order_id: '0da4eb8d-c108-4e6c-8c45-0b42fabd3a72', + // sell_order_id: '86c61562-ff14-43c9-9a03-4be804d184d0', + // quote_quantity: '0.123', + // inserted_at: '2024-09-26T15:18:06.603489Z', + // aggressor_side: 'sell', + // fee: null, + // fee_asset: null, + // updated_at: '2024-09-26T15:18:06.603489Z' + // }, + // timestamp: 1727363886603, + // datetime: '2024-09-26T15:18:06.603489Z', + // symbol: 'CLEO-ALEO', + // type: undefined, + // side: 'buy', + // takerOrMaker: undefined, + // price: 12.3, + // amount: 0.01, + // cost: 0.123, + // fee: { cost: undefined, currency: undefined }, + // fees: [] + // } + // ] + const createdDateTime = this.safeString(trade, 'inserted_at'); + const traderSide = this.safeString(trade, 'side'); + const traderOrderId = traderSide === 'buy' ? this.safeString(trade, 'buy_order_id') : this.safeString(trade, 'sell_order_id'); + return this.safeTrade({ + 'id': this.safeString(trade, 'id'), + 'order': traderOrderId, + 'info': trade, + 'timestamp': this.parse8601(createdDateTime), + 'datetime': createdDateTime, + 'symbol': this.safeString(trade, 'symbol'), + 'type': 'gtc', + 'side': traderSide, + 'takerOrMaker': undefined, + 'price': this.safeString(trade, 'price'), + 'amount': this.safeString(trade, 'base_quantity'), + 'cost': this.safeString(trade, 'quote_quantity'), + 'fee': { + 'cost': this.safeString(trade, 'fee'), + 'currency': this.safeString(trade, 'fee_asset'), + }, + }, market); + } + async fetchOrderTrades(id, symbol = undefined, since = undefined, limit = undefined, params = {}) { + /** + * @method + * @name alephx#fetchOrderTrades + * @description fetch all the trades made from a single order + * @see https://api.alephx.xyz/api/v1/trades?filters=[{"field":"order_id","op":"==","value":"order_id"}] + * @param {string} id order id + * @param {string} symbol unified market symbol + * @param {int} [since] the earliest time in ms to fetch trades for + * @param {int} [limit] the maximum number of trades to retrieve + * @param {object} [params] extra parameters specific to the exchange API endpoint + * @returns {object[]} a list of [trade structures]{@link https://docs.ccxt.com/#/?id=trade-structure} + */ + const filters = []; + const filter = { + 'field': 'order_id', + 'op': '==', + 'value': id, + }; + filters.push(filter); + const request = { + 'filters': JSON.stringify(filters), + }; + const response = await this.v1PrivateGetTrades(request); + const trades = this.safeList(response, 'data'); + const market = undefined; + // + // { "data": [ + // { "id": "32672029-b46b-4139-9779-95444053f40a", + // "status": "unsettled", + // "symbol": "CLEO-ALEO", + // "base_quantity": "0.01", + // "side": "buy", + // "price": "12.3", + // "buy_order_id": "0da4eb8d-c108-4e6c-8c45-0b42fabd3a72", + // "sell_order_id": "86c61562-ff14-43c9-9a03-4be804d184d0", + // "quote_quantity": "0.123", + // "inserted_at": "2024-09-26T15:18:06.603489Z", + // "aggressor_side": "sell", + // "fee": null, + // "fee_asset": null, + // "updated_at": "2024-09-26T15:18:06.603489Z" + // }]} + // + return this.parseTrades(trades, market, since, limit); + } + async fetchStatus(params = {}) { + /** + * @method + * @name alephx#fetchStatus + * @description the latest known information on the availability of the exchange API + * @see https://api.alephx.xyz/api/v1/system/status + * @param {object} [params] extra parameters specific to the exchange API endpoint + * @returns {object} a [status structure]{@link https://docs.ccxt.com/#/?id=exchange-status-structure} + */ + const response = await this.v1PublicGetSystemStatus(params); + // + // OK + // + return { + 'status': (response === 'OK') ? 'ok' : 'maintenance', + 'updated': undefined, + 'eta': undefined, + 'url': undefined, + 'info': response, + }; + } + async fetchBalance(params = {}) { + /** + * @method + * @name alephxn#fetchBalance + * @description query for balance and get the amount of funds available for trading or funds locked in orders + * @see https://api.alephx.xyz/api/v1/assets/balances + * @param {object} [params] extra parameters specific to the exchange API endpoint + * @returns {object} a [balance structure]{@link https://docs.ccxt.com/#/?id=balance-structure} + */ + // await this.loadMarkets (); + const response = await this.v1PrivateGetAssetsBalances(params); + // [ + // { + // "total": "19.996900", + // "available": "14.756900", + // "asset": "CLEO", + // "locked": "5.240000" + // }, + // { + // "total": "10.054720", + // "available": "-52.145280", + // "asset": "ALEO", + // "locked": "62.200000" + // } + // ] + return this.parseBalance(response); + } + parseBalance(response) { + const balances = this.toArray(response); + const result = { + 'info': response, + 'timestamp': undefined, + 'datetime': undefined, + }; + for (let i = 0; i < balances.length; i++) { + const balance = balances[i]; + const code = this.safeString(balance, 'asset'); + const account = this.account(); + account['free'] = this.safeString(balance, 'available'); + account['used'] = this.safeString(balance, 'locked'); + account['total'] = this.safeString(balance, 'total'); + result[code] = account; + } + return this.safeBalance(result); + } + sign(path, api = [], method = 'GET', params = {}, headers = undefined, body = undefined) { + const version = api[0]; + const signed = api[1] === 'private'; + let fullPath = '/api/' + version + '/' + this.implodeParams(path, params); + const query = this.omit(params, this.extractParams(path)); + const savedPath = fullPath; + if (method === 'GET') { + if (Object.keys(query).length) { + fullPath += '?' + this.urlencodeWithArrayRepeat(query); + } + } + const url = this.urls['api']['rest'] + fullPath; + if (signed) { + const authorization = this.safeString(this.headers, 'Authorization'); + let authorizationString = undefined; + if (authorization !== undefined) { + authorizationString = authorization; + } + else if (this.token && !this.checkRequiredCredentials(false)) { + authorizationString = 'Bearer ' + this.token; + } + else { + this.checkRequiredCredentials(); + if (method !== 'GET') { + if (Object.keys(query).length) { + body = this.json(query); + } + } + // doesn't need payload in the signature. inside url is enough + const timestampString = this.seconds().toString(); + const auth = timestampString + method + savedPath; + const signature = this.hmac(this.encode(auth), this.encode(this.secret), sha256.sha256); + headers = { + 'ZKX_ACCESS_KEY': this.apiKey, + 'ZKX_ACCESS_SIGN': signature, + 'ZKX_ACCESS_TIMESTAMP': timestampString, + 'Content-Type': 'application/json', + }; + } + if (authorizationString !== undefined) { + headers = { + 'Authorization': authorizationString, + 'Content-Type': 'application/json', + }; + if (method !== 'GET') { + if (Object.keys(query).length) { + body = this.json(query); + } + } + } + } + return { 'url': url, 'method': method, 'body': body, 'headers': headers }; + } + handleErrors(code, reason, url, method, headers, body, response, requestHeaders, requestBody) { + if (response === undefined) { + return undefined; // fallback to default error handler + } + const feedback = this.id + ' ' + body; + // + // + // { + // "error": { + // { + // "reason": "Bad Request", + // "message": "Order is not cancellable" + // } + // } + // } + // + const errorResponse = this.safeDict(response, 'error'); + const errorCode = this.safeString(errorResponse, 'reason'); + if (errorCode !== undefined) { + const errorMessage = this.safeString(errorResponse, 'message'); + this.throwExactlyMatchedException(this.exceptions['exact'], errorCode, feedback); + this.throwBroadlyMatchedException(this.exceptions['broad'], errorMessage, feedback); + throw new errors.ExchangeError(feedback); + } + // const errors = this.safeList(response, 'errors'); + // if (errors !== undefined) { + // if (Array.isArray(errors)) { + // const numErrors = errors.length; + // if (numErrors > 0) { + // errorCode = this.safeString (errors[0], 'id'); + // const errorMessage = this.safeString (errors[0], 'message'); + // if (errorCode !== undefined) { + // this.throwExactlyMatchedException(this.exceptions['exact'], errorCode, feedback); + // this.throwBroadlyMatchedException(this.exceptions['broad'], errorMessage, feedback); + // throw new ExchangeError(feedback); + // } + // } + // } + // } + return undefined; + } +} + +module.exports = alephx; diff --git a/dist/cjs/src/pro/alephx.js b/dist/cjs/src/pro/alephx.js new file mode 100644 index 0000000000000..ecf5b66f41748 --- /dev/null +++ b/dist/cjs/src/pro/alephx.js @@ -0,0 +1,361 @@ +'use strict'; + +var alephx$1 = require('../alephx.js'); +var errors = require('../base/errors.js'); +var Cache = require('../base/ws/Cache.js'); +var sha256 = require('../static_dependencies/noble-hashes/sha256.js'); + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +class alephx extends alephx$1 { + describe() { + return this.deepExtend(super.describe(), { + 'has': { + 'ws': true, + 'cancelAllOrdersWs': false, + 'cancelOrdersWs': false, + 'cancelOrderWs': false, + 'createOrderWs': false, + 'editOrderWs': false, + 'fetchBalanceWs': false, + 'fetchOpenOrdersWs': false, + 'fetchOrderWs': false, + 'fetchTradesWs': false, + 'watchBalance': false, + 'watchMyTrades': true, + 'watchOHLCV': false, + 'watchOrderBook': false, + 'watchOrderBookForSymbols': false, + 'watchOrders': true, + 'watchTicker': false, + 'watchTickers': false, + 'watchTrades': false, + 'watchTradesForSymbols': false, + }, + 'urls': { + 'api': { + 'ws': 'wss://api.alephx.xyz/websocket', + }, + }, + 'options': { + 'tradesLimit': 1000, + 'ordersLimit': 1000, + 'myTradesLimit': 1000, + 'sides': { + 'bid': 'bids', + 'offer': 'asks', + }, + }, + }); + } + async subscribe(name, isPrivate, symbol = undefined, params = {}) { + /** + * @ignore + * @method + * @description subscribes to a websocket channel + * @see https://api.alephx.xyz/websocket + * @param {string} name the name of the channel + * @param {string|string[]} [symbol] unified market symbol + * @param {object} [params] extra parameters specific to the exchange API endpoint + * @returns {object} subscription to a websocket channel + */ + let url = this.urls['api']['ws']; + let messageHash = name; + if (isPrivate) { + const auth = this.createWSAuth(); + url = url + '?api_key=' + auth['api_key'] + '×tamp=' + auth['timestamp'] + '&signature=' + auth['signature']; + messageHash = messageHash + ':' + auth['api_key']; + } + const subscribe = { + 'event': 'phx_join', + 'topic': messageHash, + 'payload': {}, + 'ref': messageHash, + 'join_ref': messageHash, + }; + return await this.watch(url, messageHash, subscribe, messageHash); + } + createWSAuth() { + const subscribe = {}; + const timestamp = this.numberToString(this.seconds()); + this.checkRequiredCredentials(); + const auth = timestamp; + subscribe['api_key'] = this.apiKey; + subscribe['timestamp'] = timestamp; + subscribe['signature'] = this.hmac(this.encode(auth), this.encode(this.secret), sha256.sha256); + return subscribe; + } + async watchMyTrades(symbol = undefined, since = undefined, limit = undefined, params = {}) { + /** + * @method + * @name alephx#watchMyTrades + * @description watches information on multiple trades made by the user + * @see trades channel + * @param {string} symbol unified symbol of the market to fetch trades for + * @param {int} [since] timestamp in ms of the earliest trade to fetch + * @param {int} [limit] the maximum amount of trades to fetch + * @param {object} [params] extra parameters specific to the exchange API endpoint + * @returns {object[]} a list of [trade structures]{@link https://docs.ccxt.com/#/?id=trade-structure} + */ + const name = 'trades'; + const trades = await this.subscribe(name, true, symbol, params); + if (this.newUpdates) { + limit = trades.getLimit(symbol, limit); + } + return this.filterBySinceLimit(trades, since, limit, 'timestamp', true); + } + async watchOrders(symbol = undefined, since = undefined, limit = undefined, params = {}) { + /** + * @method + * @name alephx#watchOrders + * @description watches information on multiple orders made by the user + * @see orders channel + * @param {string} [symbol] unified market symbol of the market orders were made in + * @param {int} [since] the earliest time in ms to fetch orders for + * @param {int} [limit] the maximum number of order structures to retrieve + * @param {object} [params] extra parameters specific to the exchange API endpoint + * @returns {object[]} a list of [order structures]{@link https://docs.ccxt.com/#/?id=order-structure} + */ + const name = 'orders'; + const orders = await this.subscribe(name, true, symbol, params); + if (this.newUpdates) { + limit = orders.getLimit(symbol, limit); + } + return this.filterBySinceLimit(orders, since, limit, 'timestamp', true); + } + handleTrade(client, message) { + // { + // ref: null, + // payload: { + // timestamp: '2024-10-04T03:11:30.111216Z', + // channel: 'trades', + // trade: { + // symbol: 'CLEO-ALEO', + // price: '1.1', + // base_quantity: '0.1', + // quote_quantity: '0.11', + // buy_order_id: 'ad2066e6-a47c-449d-99be-79ac82e7d163', + // sell_order_id: '1676786b-145f-4dcf-adde-74e5cce9ebc3', + // status: 'unsettled', + // aggressor_side: 'sell', + // id: 'e0b8354a-d71a-4577-bee5-ce52d8fabcf5', + // fee: null, + // fee_asset: null, + // } + // }, + // topic: 'trades:cb77b9ab-f94d-4013-85b7-644b0b9ba9a9', + // event: 'trades' + // } + if (this.myTrades === undefined) { + const limit = this.safeInteger(this.options, 'tradesLimit', 1000); + this.myTrades = new Cache.ArrayCacheBySymbolById(limit); + } + const payload = this.safeDict(message, 'payload'); + const trade = this.safeDict(payload, 'trade'); + const parsed = this.parseWsTrade(trade); + this.myTrades.append(parsed); + const messageHash = this.safeString(message, 'topic'); + client.resolve(this.myTrades, messageHash); + return message; + } + parseWsTrade(trade, market = undefined) { + // { + // symbol: 'CLEO-ALEO', + // price: '1.1', + // base_quantity: '0.1', + // quote_quantity: '0.11', + // buy_order_id: 'ad2066e6-a47c-449d-99be-79ac82e7d163', + // sell_order_id: '1676786b-145f-4dcf-adde-74e5cce9ebc3', + // status: 'unsettled', + // aggressor_side: 'sell', + // id: 'e0b8354a-d71a-4577-bee5-ce52d8fabcf5', + // fee: null, + // fee_asset: null + // } + const createdDateTime = this.safeString(trade, 'inserted_at'); + const traderSide = this.safeString(trade, 'side'); + const traderOrderId = (traderSide === 'buy' ? this.safeString(trade, 'buy_order_id') : this.safeString(trade, 'sell_order_id')); + return this.safeTrade({ + 'id': this.safeString(trade, 'id'), + 'order': traderOrderId, + 'info': trade, + 'timestamp': this.parse8601(createdDateTime), + 'datetime': createdDateTime, + 'symbol': this.safeString(trade, 'symbol'), + 'type': 'gtc', + 'side': traderSide, + 'takerOrMaker': undefined, + 'price': this.safeString(trade, 'price'), + 'amount': this.safeString(trade, 'base_quantity'), + 'cost': this.safeString(trade, 'quote_quantity'), + 'fee': { + 'cost': this.safeString(trade, 'fee'), + 'currency': this.safeString(trade, 'fee_asset'), + }, + }, market); + } + handleOrder(client, message) { + // { + // ref: null, + // payload: { + // timestamp: '2024-10-04T02:29:36.263148Z', + // channel: 'orders', + // order: { + // id: 'eed7ce96-f34b-483d-8d87-925eef6f0702', + // status: 'new', + // type: 'limit', + // symbol: 'CLEO-ALEO', + // inserted_at: '2024-10-04T02:29:35.693172Z', + // account_id: 'cb77b9ab-f94d-4013-85b7-644b0b9ba9a9', + // updated_at: '2024-10-04T02:29:36.254349Z', + // filled_quantity: '0', + // base_quantity: '0.1', + // idempotency_key: '99888999-93ef-9831-9829-820a082bfcf8', + // price: '1.1', + // remained_quantity: '0.1', + // side: 'buy', + // time_in_force: 'gtc', + // canceled_at: null, + // average_filled_price: null, + // canceled_quantity: '0', + // cumulative_fee: '0', + // fee_asset: null, + // filled_at: null, + // filled_value: '0', + // lock_version: 3, + // quote_quantity: null, + // sequence_id: 187, + // settled_quantity: '0' + // } + // }, + // topic: 'orders:cb77b9ab-f94d-4013-85b7-644b0b9ba9a9', + // event: 'orders' + // } + if (this.orders === undefined) { + const limit = this.safeInteger(this.options, 'ordersLimit', 1000); + this.orders = new Cache.ArrayCacheBySymbolById(limit); + } + const payload = this.safeDict(message, 'payload'); + const order = this.safeDict(payload, 'order'); + const parsed = this.parseWsOrder(order); + this.orders.append(parsed); + const messageHash = this.safeString(message, 'topic'); + client.resolve(this.orders, messageHash); + return message; + } + parseWsOrder(order, market = undefined) { + // { + // id: 'eed7ce96-f34b-483d-8d87-925eef6f0702', + // status: 'new', + // type: 'limit', + // symbol: 'CLEO-ALEO', + // inserted_at: '2024-10-04T02:29:35.693172Z', + // account_id: 'cb77b9ab-f94d-4013-85b7-644b0b9ba9a9', + // updated_at: '2024-10-04T02:29:36.254349Z', + // filled_quantity: '0', + // base_quantity: '0.1', + // idempotency_key: '99888999-93ef-9831-9829-820a082bfcf8', + // price: '1.1', + // remained_quantity: '0.1', + // side: 'buy', + // time_in_force: 'gtc', + // canceled_at: null, + // average_filled_price: null, + // canceled_quantity: '0', + // cumulative_fee: '0', + // fee_asset: null, + // filled_at: null, + // filled_value: '0', + // lock_version: 3, + // quote_quantity: null, + // sequence_id: 187, + // settled_quantity: '0' + // } + const id = this.safeString(order, 'id'); + const clientOrderId = this.safeString(order, 'idempotency_key'); + const createdDateTime = this.safeString(order, 'inserted_at'); + const filledDateTime = this.safeString(order, 'filled_at'); + const updatedDateTime = this.safeString(order, 'updated_at'); + return this.safeOrder({ + 'info': order, + 'symbol': this.safeString(order, 'symbol'), + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': this.parse8601(createdDateTime), + 'datetime': createdDateTime, + 'lastTradeTimestamp': filledDateTime ? this.parse8601(filledDateTime) : undefined, + 'type': this.safeString(order, 'type'), + 'timeInForce': this.safeString(order, 'time_in_force', 'gtc'), + 'postOnly': true, + 'side': this.safeString(order, 'side'), + 'price': this.safeString(order, 'price'), + 'stopPrice': undefined, + 'triggerPrice': undefined, + 'amount': this.safeString(order, 'base_quantity'), + 'cost': undefined, + 'average': this.safeString(order, 'average_filled_price'), + 'filled': this.safeString(order, 'filled_quantity'), + 'remaining': this.safeString(order, 'remained_quantity'), + 'status': this.safeStringLower(order, 'status'), + 'fee': { + 'cost': this.safeString(order, 'cumulative_fee'), + 'currency': this.safeString(market, 'fee_asset'), + }, + 'trades': undefined, + 'lastUpdatedTimestamp': updatedDateTime ? this.parse8601(updatedDateTime) : undefined, + }); + } + handleSubscriptionStatus(client, message) { + // + // { + // "type": "subscriptions", + // "channels": [ + // { + // "name": "level2", + // "product_ids": [ "ETH-BTC" ] + // } + // ] + // } + // + return message; + } + handleHeartbeats(client, message) { + // although the subscription takes a product_ids parameter (i.e. symbol), + // there is no (clear) way of mapping the message back to the symbol. + // + // { + // "channel": "heartbeats", + // "client_id": "", + // "timestamp": "2023-06-23T20:31:26.122969572Z", + // "sequence_num": 0, + // "events": [ + // { + // "current_time": "2023-06-23 20:31:56.121961769 +0000 UTC m=+91717.525857105", + // "heartbeat_counter": "3049" + // } + // ] + // } + // + return message; + } + handleMessage(client, message) { + const channel = this.safeString(this.safeDict(message, 'payload'), 'channel'); + const methods = { + // 'subscriptions': this.handleSubscriptionStatus, + 'trades': this.handleTrade, + 'orders': this.handleOrder, + // 'heartbeats': this.handleHeartbeats, + }; + const type = this.safeString(message, 'type'); + if (type === 'error') { + const errorMessage = this.safeString(message, 'message'); + throw new errors.ExchangeError(errorMessage); + } + const method = this.safeValue(methods, channel); + if (method) { + method.call(this, client, message); + } + } +} + +module.exports = alephx; diff --git a/examples/js/alephx-examples.js b/examples/js/alephx-examples.js new file mode 100644 index 0000000000000..3382057a09ae4 --- /dev/null +++ b/examples/js/alephx-examples.js @@ -0,0 +1,41 @@ +import ccxt from '../../js/ccxt.js'; +// AUTO-TRANSPILE // +async function example() { + const exchange = new ccxt.alephx({ + // local + 'apiKey': 'API_KEY', + 'secret': 'SECRET', + 'proxyUrl': 'http://localhost:4000', + 'urls': { + 'api': { + 'rest': '', + }, + }, + // remote + // 'apiKey': 'API_KEY', + // 'secret': 'SECRET', + }); + exchange.verbose = true; // uncomment for debugging purposes if necessary + // 1. createOrder + const symbol = 'CLEO-ALEO'; + const type = 'limit'; + const side = 'buy'; + const amount = 0.1; + const price = 1.1; + const params = { 'timeInForce': 'gtc', 'idempotencyKey': exchange.uuid() }; + const newOrder = await exchange.createOrder(symbol, type, side, amount, price, params); + console.log(newOrder); + // 2. fetchOrders + const orders = await exchange.fetchOrders(); + console.log(orders); + // 3. fetchOrder + const getOrder = await exchange.fetchOrder(newOrder['id']); + console.log(getOrder); + // 4. cancelOrder + const cancelOrder = await exchange.cancelOrder(newOrder['id']); + console.log(cancelOrder); + // 5. fetchMyTrades + const trades = await exchange.fetchMyTrades(); + console.log(trades); +} +await example(); diff --git a/examples/js/alephx-ws-examples.js b/examples/js/alephx-ws-examples.js new file mode 100644 index 0000000000000..d1dabde24ec60 --- /dev/null +++ b/examples/js/alephx-ws-examples.js @@ -0,0 +1,26 @@ +import ccxt from '../../js/ccxt.js'; +// AUTO-TRANSPILE // +async function example() { + const exchange = new ccxt.pro.alephx({ + 'apiKey': 'API_KEY', + 'secret': 'SECRET', + 'urls': { + 'api': { + 'ws': 'ws://localhost:4000/websocket', + }, + }, + }); + exchange.verbose = true; + while (true) { + await exchange.loadHttpProxyAgent(); + const trades = await exchange.watchMyTrades(); + for (const [key, value] of Object.entries(trades)) { + console.log(key, value); + } + // const orders = await exchange.watchOrders (); + // for (const [key, value] of Object.entries(orders)) { + // console.log(key, value); + // } + } +} +await example(); diff --git a/examples/php/alephx-examples.php b/examples/php/alephx-examples.php new file mode 100644 index 0000000000000..4894d28b15bb7 --- /dev/null +++ b/examples/php/alephx-examples.php @@ -0,0 +1,61 @@ + 'API_KEY', + 'secret' => 'SECRET', + 'proxyUrl' => 'http://localhost:4000', + 'urls' => array( + 'api' => array( + 'rest' => '', + ), + ), + )); + $exchange->verbose = true; // uncomment for debugging purposes if necessary + // 1. createOrder + $symbol = 'CLEO-ALEO'; + $type = 'limit'; + $side = 'buy'; + $amount = 0.1; + $price = 1.1; + $params = array( + 'timeInForce' => 'gtc', + 'idempotencyKey' => $exchange->uuid(), + ); + $new_order = Async\await($exchange->create_order($symbol, $type, $side, $amount, $price, $params)); + var_dump($new_order); + // 2. fetchOrders + $orders = Async\await($exchange->fetch_orders()); + var_dump($orders); + // 3. fetchOrder + $get_order = Async\await($exchange->fetch_order($new_order['id'])); + var_dump($get_order); + // 4. cancelOrder + $cancel_order = Async\await($exchange->cancel_order($new_order['id'])); + var_dump($cancel_order); + // 5. fetchMyTrades + $trades = Async\await($exchange->fetch_my_trades()); + var_dump($trades); + }) (); +} + + +Async\await(example()); diff --git a/examples/php/alephx-ws-examples.php b/examples/php/alephx-ws-examples.php new file mode 100644 index 0000000000000..5dd2b5292534d --- /dev/null +++ b/examples/php/alephx-ws-examples.php @@ -0,0 +1,41 @@ + 'API_KEY', + 'secret' => 'SECRET', + 'urls' => array( + 'api' => array( + 'ws' => 'ws://localhost:4000/websocket', + ), + ), + )); + $exchange->verbose = true; + while (true) { + Async\await($exchange->load_http_proxy_agent()); + $trades = Async\await($exchange->watch_my_trades()); + + } + }) (); +} + + +Async\await(example()); diff --git a/examples/py/alephx-examples.py b/examples/py/alephx-examples.py new file mode 100644 index 0000000000000..ca8e43768f350 --- /dev/null +++ b/examples/py/alephx-examples.py @@ -0,0 +1,58 @@ +import os +import sys +from pprint import pprint + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +sys.path.append(root + '/python') + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +import asyncio +import ccxt.async_support as ccxt # noqa: E402 + +# AUTO-TRANSPILE # +async def example(): + exchange = ccxt.alephx({ + 'apiKey': 'API_KEY', + 'secret': 'SECRET', + 'proxyUrl': 'http://localhost:4000', + 'urls': { + 'api': { + 'rest': '', + }, + }, + }) + exchange.verbose = True # uncomment for debugging purposes if necessary + # 1. createOrder + symbol = 'CLEO-ALEO' + type = 'limit' + side = 'buy' + amount = 0.1 + price = 1.1 + params = { + 'timeInForce': 'gtc', + 'idempotencyKey': exchange.uuid(), + } + new_order = await exchange.create_order(symbol, type, side, amount, price, params) + pprint(new_order) + # 2. fetchOrders + orders = await exchange.fetch_orders() + pprint(orders) + # 3. fetchOrder + get_order = await exchange.fetch_order(new_order['id']) + pprint(get_order) + # 4. cancelOrder + cancel_order = await exchange.cancel_order(new_order['id']) + pprint(cancel_order) + # 5. fetchMyTrades + trades = await exchange.fetch_my_trades() + pprint(trades) + + await exchange.close() + + +asyncio.run(example()) diff --git a/examples/py/alephx-ws-examples.py b/examples/py/alephx-ws-examples.py new file mode 100644 index 0000000000000..186d2efd48f81 --- /dev/null +++ b/examples/py/alephx-ws-examples.py @@ -0,0 +1,41 @@ +import os +import sys +from pprint import pprint + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +sys.path.append(root + '/python') + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +import asyncio +import ccxt.pro as ccxt # noqa: E402 + + +# AUTO-TRANSPILE # +async def example(): + exchange = ccxt.alephx({ + 'apiKey': 'API_KEY', + 'secret': 'SECRET', + 'urls': { + 'api': { + 'ws': 'ws://localhost:4000/websocket', + }, + }, + }) + exchange.verbose = True + while True: + # await exchange.load_http_proxy_agent() + trades = await exchange.watch_my_trades() + pprint(trades) + + orders = await exchange.watch_orders() + pprint(orders) + + await exchange.close() + + +asyncio.run(example()) diff --git a/examples/py/gateio-ws-examples.py b/examples/py/gateio-ws-examples.py new file mode 100644 index 0000000000000..bf92d3262a007 --- /dev/null +++ b/examples/py/gateio-ws-examples.py @@ -0,0 +1,30 @@ +import os +import sys +from pprint import pprint + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +sys.path.append(root + '/python') + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +import asyncio +import ccxt.pro as ccxt # noqa: E402 + + +# AUTO-TRANSPILE # +async def example(): + exchange = ccxt.gateio() + exchange.verbose = True + while True: + # await exchange.load_http_proxy_agent() + obs = await exchange.watch_order_book('ALEO/USDT') + pprint(obs) + + await exchange.close() + + +asyncio.run(example()) diff --git a/examples/ts/alephx-examples.ts b/examples/ts/alephx-examples.ts new file mode 100644 index 0000000000000..77589bdbf5a8a --- /dev/null +++ b/examples/ts/alephx-examples.ts @@ -0,0 +1,45 @@ +import ccxt from '../../js/ccxt.js'; + +// AUTO-TRANSPILE // + +async function example () { + const exchange = new ccxt.alephx ({ + // local + 'apiKey': 'API_KEY', + 'secret': 'SECRET', + 'proxyUrl': 'http://localhost:4000', + 'urls': { + 'api': { + 'rest': '', + }, + }, + // remote + // 'apiKey': 'API_KEY', + // 'secret': 'SECRET', + }); + + exchange.verbose = true; // uncomment for debugging purposes if necessary + // 1. createOrder + const symbol = 'CLEO-ALEO'; + const type = 'limit'; + const side = 'buy'; + const amount = 0.1; + const price = 1.1; + const params = { 'timeInForce': 'gtc', 'idempotencyKey': exchange.uuid () }; + const newOrder = await exchange.createOrder (symbol, type, side, amount, price, params); + console.log (newOrder); + // 2. fetchOrders + const orders = await exchange.fetchOrders (); + console.log (orders); + // 3. fetchOrder + const getOrder = await exchange.fetchOrder (newOrder['id']); + console.log (getOrder); + // 4. cancelOrder + const cancelOrder = await exchange.cancelOrder (newOrder['id']); + console.log (cancelOrder); + // 5. fetchMyTrades + const trades = await exchange.fetchMyTrades (); + console.log (trades); + +} +await example (); diff --git a/examples/ts/alephx-ws-examples.ts b/examples/ts/alephx-ws-examples.ts new file mode 100644 index 0000000000000..8e38129fe6ecb --- /dev/null +++ b/examples/ts/alephx-ws-examples.ts @@ -0,0 +1,28 @@ +import ccxt from '../../js/ccxt.js'; + +// AUTO-TRANSPILE // + +async function example () { + const exchange = new ccxt.pro.alephx ({ + 'apiKey': 'API_KEY', + 'secret': 'SECRET', + 'urls': { + 'api': { + 'ws': 'ws://localhost:4000/websocket', + }, + }, + }); + exchange.verbose = true; + while (true) { + await exchange.loadHttpProxyAgent (); + const trades = await exchange.watchMyTrades (); + for (const [key, value] of Object.entries (trades)) { + console.log (key, value); + } + // const orders = await exchange.watchOrders (); + // for (const [key, value] of Object.entries(orders)) { + // console.log(key, value); + // } + } +} +await example (); diff --git a/exchanges.cfg b/exchanges.cfg index 9c8a4608cc09f..6461868b2fcfe 100644 --- a/exchanges.cfg +++ b/exchanges.cfg @@ -6,4 +6,9 @@ # binance # huobi -# kraken +kraken +alephx +hashkey +gate +gateio +mexc diff --git a/js/ccxt.d.ts b/js/ccxt.d.ts index a11c9ca72819b..00a4ab3d8b94c 100644 --- a/js/ccxt.d.ts +++ b/js/ccxt.d.ts @@ -6,6 +6,7 @@ import type { Int, int, Str, Strings, Num, Bool, IndexType, OrderSide, OrderType import { BaseError, ExchangeError, AuthenticationError, PermissionDenied, AccountNotEnabled, AccountSuspended, ArgumentsRequired, BadRequest, BadSymbol, OperationRejected, NoChange, MarginModeAlreadySet, MarketClosed, ManualInteractionNeeded, InsufficientFunds, InvalidAddress, AddressPending, InvalidOrder, OrderNotFound, OrderNotCached, OrderImmediatelyFillable, OrderNotFillable, DuplicateOrderId, ContractUnavailable, NotSupported, InvalidProxySettings, ExchangeClosedByUser, OperationFailed, NetworkError, DDoSProtection, RateLimitExceeded, ExchangeNotAvailable, OnMaintenance, InvalidNonce, ChecksumError, RequestTimeout, BadResponse, NullResponse, CancelPending, UnsubscribeError } from './src/base/errors.js'; declare const version = "4.4.34"; import ace from './src/ace.js'; +import alephx from './src/alephx.js'; import alpaca from './src/alpaca.js'; import ascendex from './src/ascendex.js'; import bequant from './src/bequant.js'; @@ -115,6 +116,7 @@ import xt from './src/xt.js'; import yobit from './src/yobit.js'; import zaif from './src/zaif.js'; import zonda from './src/zonda.js'; +import alephxPro from './src/pro/alephx.js'; import alpacaPro from './src/pro/alpaca.js'; import ascendexPro from './src/pro/ascendex.js'; import bequantPro from './src/pro/bequant.js'; @@ -190,6 +192,7 @@ import woofiproPro from './src/pro/woofipro.js'; import xtPro from './src/pro/xt.js'; declare const exchanges: { ace: typeof ace; + alephx: typeof alephx; alpaca: typeof alpaca; ascendex: typeof ascendex; bequant: typeof bequant; @@ -301,6 +304,7 @@ declare const exchanges: { zonda: typeof zonda; }; declare const pro: { + alephx: typeof alephxPro; alpaca: typeof alpacaPro; ascendex: typeof ascendexPro; bequant: typeof bequantPro; @@ -381,6 +385,7 @@ declare const ccxt: { Precise: typeof Precise; exchanges: string[]; pro: { + alephx: typeof alephxPro; alpaca: typeof alpacaPro; ascendex: typeof ascendexPro; bequant: typeof bequantPro; @@ -457,6 +462,7 @@ declare const ccxt: { }; } & { ace: typeof ace; + alephx: typeof alephx; alpaca: typeof alpaca; ascendex: typeof ascendex; bequant: typeof bequant; diff --git a/js/ccxt.js b/js/ccxt.js index 368f7454db48d..41c0e2dbf2030 100644 --- a/js/ccxt.js +++ b/js/ccxt.js @@ -42,6 +42,7 @@ const version = '4.4.34'; Exchange.ccxtVersion = version; //----------------------------------------------------------------------------- import ace from './src/ace.js'; +import alephx from './src/alephx.js'; import alpaca from './src/alpaca.js'; import ascendex from './src/ascendex.js'; import bequant from './src/bequant.js'; @@ -152,6 +153,7 @@ import yobit from './src/yobit.js'; import zaif from './src/zaif.js'; import zonda from './src/zonda.js'; // pro exchanges +import alephxPro from './src/pro/alephx.js'; import alpacaPro from './src/pro/alpaca.js'; import ascendexPro from './src/pro/ascendex.js'; import bequantPro from './src/pro/bequant.js'; @@ -227,6 +229,7 @@ import woofiproPro from './src/pro/woofipro.js'; import xtPro from './src/pro/xt.js'; const exchanges = { 'ace': ace, + 'alephx': alephx, 'alpaca': alpaca, 'ascendex': ascendex, 'bequant': bequant, @@ -338,6 +341,7 @@ const exchanges = { 'zonda': zonda, }; const pro = { + 'alephx': alephxPro, 'alpaca': alpacaPro, 'ascendex': ascendexPro, 'bequant': bequantPro, diff --git a/js/src/abstract/alephx.d.ts b/js/src/abstract/alephx.d.ts new file mode 100644 index 0000000000000..bd5b817669440 --- /dev/null +++ b/js/src/abstract/alephx.d.ts @@ -0,0 +1,14 @@ +import { implicitReturnType } from '../base/types.js'; +import { Exchange as _Exchange } from '../base/Exchange.js'; +interface Exchange { + v1PublicGetSystemStatus(params?: {}): Promise; + v1PrivateGetAssetsBalances(params?: {}): Promise; + v1PrivateGetOrders(params?: {}): Promise; + v1PrivateGetOrdersId(params?: {}): Promise; + v1PrivateGetTrades(params?: {}): Promise; + v1PrivatePostOrders(params?: {}): Promise; + v1PrivatePatchOrdersIdCancel(params?: {}): Promise; +} +declare abstract class Exchange extends _Exchange { +} +export default Exchange; diff --git a/js/src/abstract/alephx.js b/js/src/abstract/alephx.js new file mode 100644 index 0000000000000..864db8b8baec8 --- /dev/null +++ b/js/src/abstract/alephx.js @@ -0,0 +1,11 @@ +// ---------------------------------------------------------------------------- + +// PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +// https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code +// EDIT THE CORRESPONDENT .ts FILE INSTEAD + +// ------------------------------------------------------------------------------- +import { Exchange as _Exchange } from '../base/Exchange.js'; +class Exchange extends _Exchange { +} +export default Exchange; diff --git a/js/src/alephx.d.ts b/js/src/alephx.d.ts new file mode 100644 index 0000000000000..8d9ab1661e23d --- /dev/null +++ b/js/src/alephx.d.ts @@ -0,0 +1,33 @@ +import Exchange from './abstract/alephx.js'; +import type { Balances, Int, OrderSide, OrderType, Order, Trade, Str, Market, Num, Dict, int } from './base/types.js'; +/** + * @class alephx + * @augments Exchange + */ +export default class alephx extends Exchange { + describe(): any; + createOrder(symbol: string, type: OrderType, side: OrderSide, amount: number, price?: Num, params?: {}): Promise; + parseOrder(order: Dict, market?: Market): Order; + cancelOrder(id: string, symbol?: Str, params?: {}): Promise; + fetchOrder(id: string, symbol?: Str, params?: {}): Promise; + fetchOrders(symbol?: Str, since?: Int, limit?: Int, params?: {}): Promise; + fetchMyTrades(symbol?: Str, since?: Int, limit?: Int, params?: {}): Promise; + parseTrade(trade: Dict, market?: Market): Trade; + fetchOrderTrades(id: string, symbol?: Str, since?: Int, limit?: Int, params?: {}): Promise; + fetchStatus(params?: {}): Promise<{ + status: string; + updated: any; + eta: any; + url: any; + info: any; + }>; + fetchBalance(params?: {}): Promise; + parseBalance(response: any): Balances; + sign(path: any, api?: any[], method?: string, params?: {}, headers?: any, body?: any): { + url: string; + method: string; + body: any; + headers: any; + }; + handleErrors(code: int, reason: string, url: string, method: string, headers: Dict, body: string, response: any, requestHeaders: any, requestBody: any): any; +} diff --git a/js/src/alephx.js b/js/src/alephx.js new file mode 100644 index 0000000000000..e5a5cdc979e65 --- /dev/null +++ b/js/src/alephx.js @@ -0,0 +1,646 @@ +// ---------------------------------------------------------------------------- + +// PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +// https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code +// EDIT THE CORRESPONDENT .ts FILE INSTEAD + +// ---------------------------------------------------------------------------- +import Exchange from './abstract/alephx.js'; +import { AuthenticationError, ExchangeError, BadRequest, OrderNotFound, PermissionDenied } from './base/errors.js'; +import { sha256 } from './static_dependencies/noble-hashes/sha256.js'; +// ---------------------------------------------------------------------------- +/** + * @class alephx + * @augments Exchange + */ +export default class alephx extends Exchange { + describe() { + return this.deepExtend(super.describe(), { + 'id': 'alephx', + 'name': 'AlephX', + 'countries': ['CA'], + 'pro': true, + 'certified': false, + // rate-limits: N/A + 'rateLimit': 1000, + 'version': 'v1', + 'userAgent': this.userAgents['chrome'], + // 'headers': { + // 'ZKX-VERSION': '2018-05-30', + // }, + 'has': { + 'CORS': true, + 'spot': true, + 'margin': false, + 'swap': false, + 'future': false, + 'option': false, + 'addMargin': false, + 'cancelOrder': true, + 'cancelOrders': false, + 'createDepositAddress': false, + 'createLimitBuyOrder': false, + 'createLimitSellOrder': false, + 'createMarketBuyOrder': false, + 'createMarketBuyOrderWithCost': false, + 'createMarketOrderWithCost': false, + 'createMarketSellOrder': false, + 'createMarketSellOrderWithCost': false, + 'createOrder': true, + 'createPostOnlyOrder': false, + 'createReduceOnlyOrder': false, + 'createStopLimitOrder': false, + 'createStopMarketOrder': false, + 'createStopOrder': true, + 'deposit': false, + 'editOrder': false, + 'fetchAccounts': false, + 'fetchBalance': true, + 'fetchBidsAsks': false, + 'fetchCanceledOrders': false, + 'fetchCurrencies': false, + 'fetchDeposit': false, + 'fetchDepositAddress': false, + 'fetchDepositAddresses': false, + 'fetchDepositAddressesByNetwork': false, + 'fetchDeposits': false, + 'fetchFundingHistory': false, + 'fetchFundingRate': false, + 'fetchFundingRateHistory': false, + 'fetchFundingRates': false, + 'fetchIndexOHLCV': false, + 'fetchIsolatedBorrowRate': false, + 'fetchIsolatedBorrowRates': false, + 'fetchL2OrderBook': false, + 'fetchLedger': false, + 'fetchLeverage': false, + 'fetchLeverageTiers': false, + 'fetchMarginMode': false, + 'fetchMarkets': false, + 'fetchMarkOHLCV': false, + 'fetchMyBuys': false, + 'fetchMySells': false, + 'fetchMyTrades': true, + 'fetchOHLCV': false, + 'fetchOpenInterestHistory': false, + 'fetchOpenOrders': false, + 'fetchOrder': true, + 'fetchOrderBook': false, + 'fetchOrders': true, + 'fetchOrderTrades': true, + 'fetchPosition': false, + 'fetchPositionMode': false, + 'fetchPositions': false, + 'fetchPositionsRisk': false, + 'fetchPremiumIndexOHLCV': false, + 'fetchStatus': true, + 'fetchTicker': false, + 'fetchTickers': false, + 'fetchTime': false, + 'fetchTrades': false, + 'fetchTradingFee': 'emulated', + 'fetchTradingFees': false, + 'fetchWithdrawals': false, + 'reduceMargin': false, + 'sandbox': true, + 'setLeverage': false, + 'setMarginMode': false, + 'setPositionMode': false, + 'withdraw': false, + }, + 'urls': { + // 'logo': 'https://user-images.githubusercontent.com/1294454/40811661-b6eceae2-653a-11e8-829e-10bfadb078cf.jpg', + 'test': { + 'rest': 'https://api-testnet.alephx.xyz', + }, + 'api': { + 'rest': 'https://api.alephx.xyz', + }, + 'www': 'https://demo.alephx.xyz', + // 'doc': [ + // 'https://developers.alephx.com/api/v1', + // ], + // 'fees': [ + // 'https://support.alephx.com/customer/portal/articles/2109597-buy-sell-bank-transfer-fees', + // ], + }, + 'requiredCredentials': { + 'apiKey': true, + 'secret': true, + }, + 'api': { + 'v1': { + 'public': { + 'get': { + 'system/status': 0, + }, + }, + 'private': { + 'get': { + 'assets/balances': 0, + 'orders': 0, + 'orders/{id}': 0, + 'trades': 0, + }, + 'post': { + 'orders': 0, + }, + 'patch': { + 'orders/{id}/cancel': 0, + }, + }, + }, + }, + 'exceptions': { + 'exact': {}, + 'broad': { + 'Wallet not allowed': AuthenticationError, + 'Invalid signature': AuthenticationError, + 'Unauthorized': PermissionDenied, + 'Order is not cancellable': BadRequest, + 'Asset is not supported': BadRequest, + 'Not Found': OrderNotFound, + }, + }, + }); + } + async createOrder(symbol, type, side, amount, price = undefined, params = {}) { + /** + * @method + * @name alephx#createOrder + * @description create an order + * @see POST https://api.alephx.xyz/api/v1/orders + * @param {string} symbol unified symbol of the market to create an order in + * @param {string} type 'market' or 'limit' + * @param {string} side 'buy' or 'sell' + * @param {float} amount how much you want to trade in units of the base currency, quote currency for 'market' 'buy' orders + * @param {float} [price] the price to fulfill the order, in units of the quote currency, ignored in market orders + * @param {object} [params] extra parameters specific to the exchange API endpoint + * @param {string} [params.timeInForce] 'gtc' + * @param {string} [params.idempotencyKey] uuid for idempotency key + * @returns {object} an [order structure]{@link https://docs.ccxt.com/#/?id=order-structure} + */ + const request = { + 'symbol': symbol, + 'type': type, + 'side': side, + 'quantity': amount.toString(), + 'price': price.toString(), + 'time_in_force': this.safeString2(params, 'timeInForce', 'gtc'), + 'idempotency_key': this.safeString2(params, 'idempotencyKey', this.uuid()), + }; + const response = await this.v1PrivatePostOrders(request); + // + // successful order + // + // + // failed order + // + // + const errorResponse = this.safeDict(response, 'error'); + if (errorResponse !== undefined) { + const errorReason = this.safeString(errorResponse, 'reason'); + const errorMessage = this.safeString(errorResponse, 'message'); + throw new ExchangeError(errorReason + '' + errorMessage); + } + return this.parseOrder(response); + } + parseOrder(order, market = undefined) { + // + // createOrder + // + // { + // "order_id": "52cfe5e2-0b29-4c19-a245-a6a773de5030", + // "status": "pending_new" + // } + // + // + // fetchOrder, fetchOrders, cancelOrder + // + // { + // "id": "0da4eb8d-c108-4e6c-8c45-0b42fabd3a72", + // "status": "partially_filled", + // "type": "limit", + // "symbol": "CLEO-ALEO", + // "account_id": "cb77b9ab-f94d-4013-85b7-644b0b9ba9a9", + // "settled_quantity": "0", + // "base_quantity": "0.1", + // "filled_quantity": "0.04", + // "side": "buy", + // "price": "12.3", + // "remained_quantity": "0.06", + // "idempotency_key": "99888999-93ef-9831-9829-120a082bfcf2", + // "inserted_at": "2024-09-16T23:47:45.161888Z", + // "fee_asset":null, + // "filled_at": "2024-09-26T20:08:11.350542Z", + // "average_filled_price": "12.3", + // "canceled_at":null,"cumulative_fee": "0", + // "time_in_force": "gtc", + // "internal_status": "partially_filled" + // } + // + const createdDateTime = this.safeString(order, 'inserted_at'); + const filledDateTime = this.safeString(order, 'filled_at'); + return this.safeOrder({ + 'info': order, + 'id': this.safeString(order, 'order_id') || this.safeString(order, 'id'), + 'clientOrderId': this.safeString(order, 'idempotency_key'), + 'timestamp': createdDateTime ? this.parse8601(createdDateTime) : undefined, + 'datetime': createdDateTime, + 'lastTradeTimestamp': filledDateTime ? this.parse8601(filledDateTime) : undefined, + 'symbol': this.safeString(order, 'symbol'), + 'type': this.safeString(order, 'type'), + 'timeInForce': this.safeString(order, 'time_in_force', 'gtc'), + 'postOnly': true, + 'side': this.safeStringLower(order, 'side'), + 'price': this.safeString(order, 'price'), + 'stopPrice': undefined, + 'triggerPrice': undefined, + 'amount': this.safeString(order, 'base_quantity'), + 'filled': this.safeString(order, 'filled_quantity'), + 'remaining': this.safeString(order, 'remained_quantity'), + 'cost': undefined, + 'average': this.safeString(order, 'average_filled_price'), + 'status': this.safeString(order, 'status'), + 'fee': { + 'cost': this.safeString(order, 'cumulative_fee'), + 'currency': this.safeString(order, 'fee_asset'), + }, + 'trades': undefined, + }, market); + } + async cancelOrder(id, symbol = undefined, params = {}) { + /** + * @method + * @name alephx#cancelOrder + * @description cancels an open order + * @see PATCH https://api.alephx.xyz/api/v1/orders/{order_id}/cancel + * @param {string} id order id + * @param {string} symbol not used by alephx cancelOrder() + * @param {object} [params] extra parameters specific to the exchange API endpoint + * @returns {object} An [order structure]{@link https://docs.ccxt.com/#/?id=order-structure} + */ + const request = { + 'id': id, // order id + }; + const response = await this.v1PrivatePatchOrdersIdCancel(this.extend(request, params)); + const errorResponse = this.safeDict(response, 'error'); + if (errorResponse !== undefined) { + const errorReason = this.safeString(errorResponse, 'reason'); + const errorMessage = this.safeString(errorResponse, 'message'); + if (errorReason === 'Not Found') { + throw new OrderNotFound(this.id + ' cancelOrder() error ' + errorReason); + } + else if (errorReason === 'Bad Request') { + throw new BadRequest(this.id + ' cancelOrder() error ' + errorReason + ' ' + errorMessage); + } + else { + throw new ExchangeError(errorReason + '' + errorMessage); + } + } + return this.parseOrder(response); + } + async fetchOrder(id, symbol = undefined, params = {}) { + /** + * @method + * @name alephx#fetchOrder + * @description fetches information on an order made by the user + * @see GET https://api.alephx.xyz/api/v1/orders/{order_id} + * @param {string} id the order id + * @param {string} symbol unified market symbol that the order was made in + * @param {object} [params] extra parameters specific to the exchange API endpoint + * @returns {object} An [order structure]{@link https://docs.ccxt.com/#/?id=order-structure} + */ + const request = { + 'id': id, // order id + }; + const response = await this.v1PrivateGetOrdersId(this.extend(request, params)); + const errorResponse = this.safeDict(response, 'error'); + if (errorResponse !== undefined) { + const errorReason = this.safeString(errorResponse, 'reason'); + if (errorReason === 'Not Found') { + throw new OrderNotFound(this.id + ' fetchOrder() error ' + errorReason); + } + } + return this.parseOrder(response); + } + async fetchOrders(symbol = undefined, since = undefined, limit = 100, params = {}) { + /** + * @method + * @name alephx#fetchOrders + * @description fetches information on multiple orders made by the user + * @see GET https://api.alephx.xyz/api/v1/orders/ + * @param {string} symbol unified market symbol that the orders were made in + * @param {int} [since] the earliest time in ms to fetch orders + * @param {int} [limit] the maximum number of order structures to retrieve + * @param {object} [params] extra parameters specific to the exchange API endpoint + * @param {int} [params.until] the latest time in ms to fetch trades for + * @param {boolean} [params.paginate] default false, when true will automatically paginate by calling this endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + * @returns {Order[]} a list of [order structures]{@link https://docs.ccxt.com/#/?id=order-structure} + */ + const response = await this.v1PrivateGetOrders(); + const market = undefined; + return this.parseOrders(response, market, since, limit); + } + async fetchMyTrades(symbol = undefined, since = undefined, limit = undefined, params = {}) { + /** + * @method + * @name alephx#fetchMyTrades + * @description fetch all trades made by the user + * @see GET https://api.alephx.xyz/api/v1/trades + * @param {string} symbol unified market symbol of the trades + * @param {int} [since] timestamp in ms of the earliest order, default is undefined + * @param {int} [limit] the maximum number of trade structures to fetch + * @param {object} [params] extra parameters specific to the exchange API endpoint + * @param {int} [params.until] the latest time in ms to fetch trades for + * @param {boolean} [params.paginate] default false, when true will automatically paginate by calling this endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + * @returns {Trade[]} a list of [trade structures]{@link https://docs.ccxt.com/#/?id=trade-structure} + */ + const response = await this.v1PrivateGetTrades(); + const trades = this.safeList(response, 'data'); + const market = undefined; + // + // { "data": [ + // { "id": "32672029-b46b-4139-9779-95444053f40a", + // "status": "unsettled", + // "symbol": "CLEO-ALEO", + // "base_quantity": "0.01", + // "side": "buy", + // "price": "12.3", + // "buy_order_id": "0da4eb8d-c108-4e6c-8c45-0b42fabd3a72", + // "sell_order_id": "86c61562-ff14-43c9-9a03-4be804d184d0", + // "quote_quantity": "0.123", + // "inserted_at": "2024-09-26T15:18:06.603489Z", + // "aggressor_side": "sell", + // "fee": null, + // "fee_asset": null, + // "updated_at": "2024-09-26T15:18:06.603489Z" + // }]} + // + return this.parseTrades(trades, market, since, limit); + } + parseTrade(trade, market = undefined) { + // returned trade + // + // [ + // { + // id: '32672029-b46b-4139-9779-95444053f40a', + // order: '0da4eb8d-c108-4e6c-8c45-0b42fabd3a72', + // info: { + // id: '32672029-b46b-4139-9779-95444053f40a', + // status: 'unsettled', + // symbol: 'CLEO-ALEO', + // base_quantity: '0.01', + // side: 'buy', + // price: '12.3', + // buy_order_id: '0da4eb8d-c108-4e6c-8c45-0b42fabd3a72', + // sell_order_id: '86c61562-ff14-43c9-9a03-4be804d184d0', + // quote_quantity: '0.123', + // inserted_at: '2024-09-26T15:18:06.603489Z', + // aggressor_side: 'sell', + // fee: null, + // fee_asset: null, + // updated_at: '2024-09-26T15:18:06.603489Z' + // }, + // timestamp: 1727363886603, + // datetime: '2024-09-26T15:18:06.603489Z', + // symbol: 'CLEO-ALEO', + // type: undefined, + // side: 'buy', + // takerOrMaker: undefined, + // price: 12.3, + // amount: 0.01, + // cost: 0.123, + // fee: { cost: undefined, currency: undefined }, + // fees: [] + // } + // ] + const createdDateTime = this.safeString(trade, 'inserted_at'); + const traderSide = this.safeString(trade, 'side'); + const traderOrderId = (traderSide === 'buy') ? this.safeString(trade, 'buy_order_id') : this.safeString(trade, 'sell_order_id'); + return this.safeTrade({ + 'id': this.safeString(trade, 'id'), + 'order': traderOrderId, + 'info': trade, + 'timestamp': this.parse8601(createdDateTime), + 'datetime': createdDateTime, + 'symbol': this.safeString(trade, 'symbol'), + 'type': 'gtc', + 'side': traderSide, + 'takerOrMaker': undefined, + 'price': this.safeString(trade, 'price'), + 'amount': this.safeString(trade, 'base_quantity'), + 'cost': this.safeString(trade, 'quote_quantity'), + 'fee': { + 'cost': this.safeString(trade, 'fee'), + 'currency': this.safeString(trade, 'fee_asset'), + }, + }, market); + } + async fetchOrderTrades(id, symbol = undefined, since = undefined, limit = undefined, params = {}) { + /** + * @method + * @name alephx#fetchOrderTrades + * @description fetch all the trades made from a single order + * @see https://api.alephx.xyz/api/v1/trades?filters=[{"field":"order_id","op":"==","value":"order_id"}] + * @param {string} id order id + * @param {string} symbol unified market symbol + * @param {int} [since] the earliest time in ms to fetch trades for + * @param {int} [limit] the maximum number of trades to retrieve + * @param {object} [params] extra parameters specific to the exchange API endpoint + * @returns {object[]} a list of [trade structures]{@link https://docs.ccxt.com/#/?id=trade-structure} + */ + const filters = []; + const filter = { + 'field': 'order_id', + 'op': '==', + 'value': id, + }; + filters.push(filter); + const request = { + 'filters': JSON.stringify(filters), + }; + const response = await this.v1PrivateGetTrades(request); + const trades = this.safeList(response, 'data'); + const market = undefined; + // + // { "data": [ + // { "id": "32672029-b46b-4139-9779-95444053f40a", + // "status": "unsettled", + // "symbol": "CLEO-ALEO", + // "base_quantity": "0.01", + // "side": "buy", + // "price": "12.3", + // "buy_order_id": "0da4eb8d-c108-4e6c-8c45-0b42fabd3a72", + // "sell_order_id": "86c61562-ff14-43c9-9a03-4be804d184d0", + // "quote_quantity": "0.123", + // "inserted_at": "2024-09-26T15:18:06.603489Z", + // "aggressor_side": "sell", + // "fee": null, + // "fee_asset": null, + // "updated_at": "2024-09-26T15:18:06.603489Z" + // }]} + // + return this.parseTrades(trades, market, since, limit); + } + async fetchStatus(params = {}) { + /** + * @method + * @name alephx#fetchStatus + * @description the latest known information on the availability of the exchange API + * @see https://api.alephx.xyz/api/v1/system/status + * @param {object} [params] extra parameters specific to the exchange API endpoint + * @returns {object} a [status structure]{@link https://docs.ccxt.com/#/?id=exchange-status-structure} + */ + const response = await this.v1PublicGetSystemStatus(params); + // + // OK + // + return { + 'status': (response === 'OK') ? 'ok' : 'maintenance', + 'updated': undefined, + 'eta': undefined, + 'url': undefined, + 'info': response, + }; + } + async fetchBalance(params = {}) { + /** + * @method + * @name alephxn#fetchBalance + * @description query for balance and get the amount of funds available for trading or funds locked in orders + * @see https://api.alephx.xyz/api/v1/assets/balances + * @param {object} [params] extra parameters specific to the exchange API endpoint + * @returns {object} a [balance structure]{@link https://docs.ccxt.com/#/?id=balance-structure} + */ + // await this.loadMarkets (); + const response = await this.v1PrivateGetAssetsBalances(params); + // [ + // { + // "total": "19.996900", + // "available": "14.756900", + // "asset": "CLEO", + // "locked": "5.240000" + // }, + // { + // "total": "10.054720", + // "available": "-52.145280", + // "asset": "ALEO", + // "locked": "62.200000" + // } + // ] + return this.parseBalance(response); + } + parseBalance(response) { + const balances = this.toArray(response); + const result = { + 'info': response, + 'timestamp': undefined, + 'datetime': undefined, + }; + for (let i = 0; i < balances.length; i++) { + const balance = balances[i]; + const code = this.safeString(balance, 'asset'); + const account = this.account(); + account['free'] = this.safeString(balance, 'available'); + account['used'] = this.safeString(balance, 'locked'); + account['total'] = this.safeString(balance, 'total'); + result[code] = account; + } + return this.safeBalance(result); + } + sign(path, api = [], method = 'GET', params = {}, headers = undefined, body = undefined) { + const version = api[0]; + const signed = api[1] === 'private'; + let fullPath = '/api/' + version + '/' + this.implodeParams(path, params); + const query = this.omit(params, this.extractParams(path)); + const savedPath = fullPath; + if (method === 'GET') { + if (Object.keys(query).length) { + fullPath += '?' + this.urlencodeWithArrayRepeat(query); + } + } + const url = this.urls['api']['rest'] + fullPath; + if (signed) { + const authorization = this.safeString(this.headers, 'Authorization'); + let authorizationString = undefined; + if (authorization !== undefined) { + authorizationString = authorization; + } + else if (this.token && !this.checkRequiredCredentials(false)) { + authorizationString = 'Bearer ' + this.token; + } + else { + this.checkRequiredCredentials(); + if (method !== 'GET') { + if (Object.keys(query).length) { + body = this.json(query); + } + } + // doesn't need payload in the signature. inside url is enough + const timestampString = this.seconds().toString(); + const auth = timestampString + method + savedPath; + const signature = this.hmac(this.encode(auth), this.encode(this.secret), sha256); + headers = { + 'ZKX_ACCESS_KEY': this.apiKey, + 'ZKX_ACCESS_SIGN': signature, + 'ZKX_ACCESS_TIMESTAMP': timestampString, + 'Content-Type': 'application/json', + }; + } + if (authorizationString !== undefined) { + headers = { + 'Authorization': authorizationString, + 'Content-Type': 'application/json', + }; + if (method !== 'GET') { + if (Object.keys(query).length) { + body = this.json(query); + } + } + } + } + return { 'url': url, 'method': method, 'body': body, 'headers': headers }; + } + handleErrors(code, reason, url, method, headers, body, response, requestHeaders, requestBody) { + if (response === undefined) { + return undefined; // fallback to default error handler + } + const feedback = this.id + ' ' + body; + // + // + // { + // "error": { + // { + // "reason": "Bad Request", + // "message": "Order is not cancellable" + // } + // } + // } + // + const errorResponse = this.safeDict(response, 'error'); + const errorCode = this.safeString(errorResponse, 'reason'); + if (errorCode !== undefined) { + const errorMessage = this.safeString(errorResponse, 'message'); + this.throwExactlyMatchedException(this.exceptions['exact'], errorCode, feedback); + this.throwBroadlyMatchedException(this.exceptions['broad'], errorMessage, feedback); + throw new ExchangeError(feedback); + } + // const errors = this.safeList(response, 'errors'); + // if (errors !== undefined) { + // if (Array.isArray(errors)) { + // const numErrors = errors.length; + // if (numErrors > 0) { + // errorCode = this.safeString (errors[0], 'id'); + // const errorMessage = this.safeString (errors[0], 'message'); + // if (errorCode !== undefined) { + // this.throwExactlyMatchedException(this.exceptions['exact'], errorCode, feedback); + // this.throwBroadlyMatchedException(this.exceptions['broad'], errorMessage, feedback); + // throw new ExchangeError(feedback); + // } + // } + // } + // } + return undefined; + } +} diff --git a/js/src/pro/alephx.d.ts b/js/src/pro/alephx.d.ts new file mode 100644 index 0000000000000..0f411758fd113 --- /dev/null +++ b/js/src/pro/alephx.d.ts @@ -0,0 +1,16 @@ +import alephxRest from '../alephx.js'; +import { Int, Trade, Order, Str, Dict } from '../base/types.js'; +export default class alephx extends alephxRest { + describe(): any; + subscribe(name: string, isPrivate: boolean, symbol?: any, params?: {}): Promise; + createWSAuth(): Dict; + watchMyTrades(symbol?: string, since?: Int, limit?: Int, params?: {}): Promise; + watchOrders(symbol?: Str, since?: Int, limit?: Int, params?: {}): Promise; + handleTrade(client: any, message: any): any; + parseWsTrade(trade: any, market?: any): Trade; + handleOrder(client: any, message: any): any; + parseWsOrder(order: any, market?: any): Order; + handleSubscriptionStatus(client: any, message: any): any; + handleHeartbeats(client: any, message: any): any; + handleMessage(client: any, message: any): void; +} diff --git a/js/src/pro/alephx.js b/js/src/pro/alephx.js new file mode 100644 index 0000000000000..6ef7310714f13 --- /dev/null +++ b/js/src/pro/alephx.js @@ -0,0 +1,365 @@ +// ---------------------------------------------------------------------------- + +// PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +// https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code +// EDIT THE CORRESPONDENT .ts FILE INSTEAD + +// --------------------------------------------------------------------------- +import alephxRest from '../alephx.js'; +import { ExchangeError } from '../base/errors.js'; +import { ArrayCacheBySymbolById } from '../base/ws/Cache.js'; +import { sha256 } from '../static_dependencies/noble-hashes/sha256.js'; +// --------------------------------------------------------------------------- +export default class alephx extends alephxRest { + describe() { + return this.deepExtend(super.describe(), { + 'has': { + 'ws': true, + 'cancelAllOrdersWs': false, + 'cancelOrdersWs': false, + 'cancelOrderWs': false, + 'createOrderWs': false, + 'editOrderWs': false, + 'fetchBalanceWs': false, + 'fetchOpenOrdersWs': false, + 'fetchOrderWs': false, + 'fetchTradesWs': false, + 'watchBalance': false, + 'watchMyTrades': true, + 'watchOHLCV': false, + 'watchOrderBook': false, + 'watchOrderBookForSymbols': false, + 'watchOrders': true, + 'watchTicker': false, + 'watchTickers': false, + 'watchTrades': false, + 'watchTradesForSymbols': false, + }, + 'urls': { + 'test': { + 'ws': 'wss://api-testnet.alephx.xyz/websocket', + }, + 'api': { + 'ws': 'wss://api.alephx.xyz/websocket', + }, + }, + 'options': { + 'tradesLimit': 1000, + 'ordersLimit': 1000, + 'myTradesLimit': 1000, + 'sides': { + 'bid': 'bids', + 'offer': 'asks', + }, + }, + }); + } + async subscribe(name, isPrivate, symbol = undefined, params = {}) { + /** + * @ignore + * @method + * @description subscribes to a websocket channel + * @see https://api.alephx.xyz/websocket + * @param {string} name the name of the channel + * @param {string|string[]} [symbol] unified market symbol + * @param {object} [params] extra parameters specific to the exchange API endpoint + * @returns {object} subscription to a websocket channel + */ + let url = this.urls['api']['ws']; + let messageHash = name; + if (isPrivate) { + const auth = this.createWSAuth(); + url = url + '?api_key=' + auth['api_key'] + '×tamp=' + auth['timestamp'] + '&signature=' + auth['signature']; + messageHash = messageHash + ':' + auth['api_key']; + } + const subscribe = { + 'event': 'phx_join', + 'topic': messageHash, + 'payload': {}, + 'ref': messageHash, + 'join_ref': messageHash, + }; + return await this.watch(url, messageHash, subscribe, messageHash); + } + createWSAuth() { + const subscribe = {}; + const timestamp = this.numberToString(this.seconds()); + this.checkRequiredCredentials(); + const auth = timestamp; + subscribe['api_key'] = this.apiKey; + subscribe['timestamp'] = timestamp; + subscribe['signature'] = this.hmac(this.encode(auth), this.encode(this.secret), sha256); + return subscribe; + } + async watchMyTrades(symbol = undefined, since = undefined, limit = undefined, params = {}) { + /** + * @method + * @name alephx#watchMyTrades + * @description watches information on multiple trades made by the user + * @see trades channel + * @param {string} symbol unified symbol of the market to fetch trades for + * @param {int} [since] timestamp in ms of the earliest trade to fetch + * @param {int} [limit] the maximum amount of trades to fetch + * @param {object} [params] extra parameters specific to the exchange API endpoint + * @returns {object[]} a list of [trade structures]{@link https://docs.ccxt.com/#/?id=trade-structure} + */ + const name = 'trades'; + const trades = await this.subscribe(name, true, symbol, params); + if (this.newUpdates) { + limit = trades.getLimit(symbol, limit); + } + return this.filterBySinceLimit(trades, since, limit, 'timestamp', true); + } + async watchOrders(symbol = undefined, since = undefined, limit = undefined, params = {}) { + /** + * @method + * @name alephx#watchOrders + * @description watches information on multiple orders made by the user + * @see orders channel + * @param {string} [symbol] unified market symbol of the market orders were made in + * @param {int} [since] the earliest time in ms to fetch orders for + * @param {int} [limit] the maximum number of order structures to retrieve + * @param {object} [params] extra parameters specific to the exchange API endpoint + * @returns {object[]} a list of [order structures]{@link https://docs.ccxt.com/#/?id=order-structure} + */ + const name = 'orders'; + const orders = await this.subscribe(name, true, symbol, params); + if (this.newUpdates) { + limit = orders.getLimit(symbol, limit); + } + return this.filterBySinceLimit(orders, since, limit, 'timestamp', true); + } + handleTrade(client, message) { + // { + // ref: null, + // payload: { + // timestamp: '2024-10-04T03:11:30.111216Z', + // channel: 'trades', + // trade: { + // symbol: 'CLEO-ALEO', + // price: '1.1', + // base_quantity: '0.1', + // quote_quantity: '0.11', + // buy_order_id: 'ad2066e6-a47c-449d-99be-79ac82e7d163', + // sell_order_id: '1676786b-145f-4dcf-adde-74e5cce9ebc3', + // status: 'unsettled', + // aggressor_side: 'sell', + // id: 'e0b8354a-d71a-4577-bee5-ce52d8fabcf5', + // fee: null, + // fee_asset: null, + // } + // }, + // topic: 'trades:cb77b9ab-f94d-4013-85b7-644b0b9ba9a9', + // event: 'trades' + // } + if (this.myTrades === undefined) { + const limit = this.safeInteger(this.options, 'tradesLimit', 1000); + this.myTrades = new ArrayCacheBySymbolById(limit); + } + const payload = this.safeDict(message, 'payload'); + const trade = this.safeDict(payload, 'trade'); + const parsed = this.parseWsTrade(trade); + this.myTrades.append(parsed); + const messageHash = this.safeString(message, 'topic'); + client.resolve(this.myTrades, messageHash); + return message; + } + parseWsTrade(trade, market = undefined) { + // { + // symbol: 'CLEO-ALEO', + // price: '1.1', + // base_quantity: '0.1', + // quote_quantity: '0.11', + // buy_order_id: 'ad2066e6-a47c-449d-99be-79ac82e7d163', + // sell_order_id: '1676786b-145f-4dcf-adde-74e5cce9ebc3', + // status: 'unsettled', + // aggressor_side: 'sell', + // id: 'e0b8354a-d71a-4577-bee5-ce52d8fabcf5', + // fee: null, + // fee_asset: null + // } + const createdDateTime = this.safeString(trade, 'inserted_at'); + const traderSide = this.safeString(trade, 'side'); + const traderOrderId = (traderSide === 'buy') ? this.safeString(trade, 'buy_order_id') : this.safeString(trade, 'sell_order_id'); + return this.safeTrade({ + 'id': this.safeString(trade, 'id'), + 'order': traderOrderId, + 'info': trade, + 'timestamp': this.parse8601(createdDateTime), + 'datetime': createdDateTime, + 'symbol': this.safeString(trade, 'symbol'), + 'type': 'gtc', + 'side': traderSide, + 'takerOrMaker': undefined, + 'price': this.safeString(trade, 'price'), + 'amount': this.safeString(trade, 'base_quantity'), + 'cost': this.safeString(trade, 'quote_quantity'), + 'fee': { + 'cost': this.safeString(trade, 'fee'), + 'currency': this.safeString(trade, 'fee_asset'), + }, + }, market); + } + handleOrder(client, message) { + // { + // ref: null, + // payload: { + // timestamp: '2024-10-04T02:29:36.263148Z', + // channel: 'orders', + // order: { + // id: 'eed7ce96-f34b-483d-8d87-925eef6f0702', + // status: 'new', + // type: 'limit', + // symbol: 'CLEO-ALEO', + // inserted_at: '2024-10-04T02:29:35.693172Z', + // account_id: 'cb77b9ab-f94d-4013-85b7-644b0b9ba9a9', + // updated_at: '2024-10-04T02:29:36.254349Z', + // filled_quantity: '0', + // base_quantity: '0.1', + // idempotency_key: '99888999-93ef-9831-9829-820a082bfcf8', + // price: '1.1', + // remained_quantity: '0.1', + // side: 'buy', + // time_in_force: 'gtc', + // canceled_at: null, + // average_filled_price: null, + // canceled_quantity: '0', + // cumulative_fee: '0', + // fee_asset: null, + // filled_at: null, + // filled_value: '0', + // lock_version: 3, + // quote_quantity: null, + // sequence_id: 187, + // settled_quantity: '0' + // } + // }, + // topic: 'orders:cb77b9ab-f94d-4013-85b7-644b0b9ba9a9', + // event: 'orders' + // } + if (this.orders === undefined) { + const limit = this.safeInteger(this.options, 'ordersLimit', 1000); + this.orders = new ArrayCacheBySymbolById(limit); + } + const payload = this.safeDict(message, 'payload'); + const order = this.safeDict(payload, 'order'); + const parsed = this.parseWsOrder(order); + this.orders.append(parsed); + const messageHash = this.safeString(message, 'topic'); + client.resolve(this.orders, messageHash); + return message; + } + parseWsOrder(order, market = undefined) { + // { + // id: 'eed7ce96-f34b-483d-8d87-925eef6f0702', + // status: 'new', + // type: 'limit', + // symbol: 'CLEO-ALEO', + // inserted_at: '2024-10-04T02:29:35.693172Z', + // account_id: 'cb77b9ab-f94d-4013-85b7-644b0b9ba9a9', + // updated_at: '2024-10-04T02:29:36.254349Z', + // filled_quantity: '0', + // base_quantity: '0.1', + // idempotency_key: '99888999-93ef-9831-9829-820a082bfcf8', + // price: '1.1', + // remained_quantity: '0.1', + // side: 'buy', + // time_in_force: 'gtc', + // canceled_at: null, + // average_filled_price: null, + // canceled_quantity: '0', + // cumulative_fee: '0', + // fee_asset: null, + // filled_at: null, + // filled_value: '0', + // lock_version: 3, + // quote_quantity: null, + // sequence_id: 187, + // settled_quantity: '0' + // } + const id = this.safeString(order, 'id'); + const clientOrderId = this.safeString(order, 'idempotency_key'); + const createdDateTime = this.safeString(order, 'inserted_at'); + const filledDateTime = this.safeString(order, 'filled_at'); + const updatedDateTime = this.safeString(order, 'updated_at'); + return this.safeOrder({ + 'info': order, + 'symbol': this.safeString(order, 'symbol'), + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': this.parse8601(createdDateTime), + 'datetime': createdDateTime, + 'lastTradeTimestamp': filledDateTime ? this.parse8601(filledDateTime) : undefined, + 'type': this.safeString(order, 'type'), + 'timeInForce': this.safeString(order, 'time_in_force', 'gtc'), + 'postOnly': true, + 'side': this.safeString(order, 'side'), + 'price': this.safeString(order, 'price'), + 'stopPrice': undefined, + 'triggerPrice': undefined, + 'amount': this.safeString(order, 'base_quantity'), + 'cost': undefined, + 'average': this.safeString(order, 'average_filled_price'), + 'filled': this.safeString(order, 'filled_quantity'), + 'remaining': this.safeString(order, 'remained_quantity'), + 'status': this.safeStringLower(order, 'status'), + 'fee': { + 'cost': this.safeString(order, 'cumulative_fee'), + 'currency': this.safeString(market, 'fee_asset'), + }, + 'trades': undefined, + 'lastUpdatedTimestamp': updatedDateTime ? this.parse8601(updatedDateTime) : undefined, + }); + } + handleSubscriptionStatus(client, message) { + // + // { + // "type": "subscriptions", + // "channels": [ + // { + // "name": "level2", + // "product_ids": [ "ETH-BTC" ] + // } + // ] + // } + // + return message; + } + handleHeartbeats(client, message) { + // although the subscription takes a product_ids parameter (i.e. symbol), + // there is no (clear) way of mapping the message back to the symbol. + // + // { + // "channel": "heartbeats", + // "client_id": "", + // "timestamp": "2023-06-23T20:31:26.122969572Z", + // "sequence_num": 0, + // "events": [ + // { + // "current_time": "2023-06-23 20:31:56.121961769 +0000 UTC m=+91717.525857105", + // "heartbeat_counter": "3049" + // } + // ] + // } + // + return message; + } + handleMessage(client, message) { + const channel = this.safeString(this.safeDict(message, 'payload'), 'channel'); + const methods = { + // 'subscriptions': this.handleSubscriptionStatus, + 'trades': this.handleTrade, + 'orders': this.handleOrder, + // 'heartbeats': this.handleHeartbeats, + }; + const type = this.safeString(message, 'type'); + if (type === 'error') { + const errorMessage = this.safeString(message, 'message'); + throw new ExchangeError(errorMessage); + } + const method = this.safeValue(methods, channel); + if (method) { + method.call(this, client, message); + } + } +} diff --git a/php/Exchange.php b/php/Exchange.php index af3dcf712338f..de15a70e99fe5 100644 --- a/php/Exchange.php +++ b/php/Exchange.php @@ -337,6 +337,7 @@ class Exchange { public static $exchanges = array( 'ace', + 'alephx', 'alpaca', 'ascendex', 'bequant', diff --git a/php/abstract/alephx.php b/php/abstract/alephx.php new file mode 100644 index 0000000000000..73b79b65b34d3 --- /dev/null +++ b/php/abstract/alephx.php @@ -0,0 +1,52 @@ +request('system/status', array('v1', 'public'), 'GET', $params, null, null, array("cost" => 0)); + } + public function v1_private_get_assets_balances($params = array()) { + return $this->request('assets/balances', array('v1', 'private'), 'GET', $params, null, null, array("cost" => 0)); + } + public function v1_private_get_orders($params = array()) { + return $this->request('orders', array('v1', 'private'), 'GET', $params, null, null, array("cost" => 0)); + } + public function v1_private_get_orders_id($params = array()) { + return $this->request('orders/{id}', array('v1', 'private'), 'GET', $params, null, null, array("cost" => 0)); + } + public function v1_private_get_trades($params = array()) { + return $this->request('trades', array('v1', 'private'), 'GET', $params, null, null, array("cost" => 0)); + } + public function v1_private_post_orders($params = array()) { + return $this->request('orders', array('v1', 'private'), 'POST', $params, null, null, array("cost" => 0)); + } + public function v1_private_patch_orders_id_cancel($params = array()) { + return $this->request('orders/{id}/cancel', array('v1', 'private'), 'PATCH', $params, null, null, array("cost" => 0)); + } + public function v1PublicGetSystemStatus($params = array()) { + return $this->request('system/status', array('v1', 'public'), 'GET', $params, null, null, array("cost" => 0)); + } + public function v1PrivateGetAssetsBalances($params = array()) { + return $this->request('assets/balances', array('v1', 'private'), 'GET', $params, null, null, array("cost" => 0)); + } + public function v1PrivateGetOrders($params = array()) { + return $this->request('orders', array('v1', 'private'), 'GET', $params, null, null, array("cost" => 0)); + } + public function v1PrivateGetOrdersId($params = array()) { + return $this->request('orders/{id}', array('v1', 'private'), 'GET', $params, null, null, array("cost" => 0)); + } + public function v1PrivateGetTrades($params = array()) { + return $this->request('trades', array('v1', 'private'), 'GET', $params, null, null, array("cost" => 0)); + } + public function v1PrivatePostOrders($params = array()) { + return $this->request('orders', array('v1', 'private'), 'POST', $params, null, null, array("cost" => 0)); + } + public function v1PrivatePatchOrdersIdCancel($params = array()) { + return $this->request('orders/{id}/cancel', array('v1', 'private'), 'PATCH', $params, null, null, array("cost" => 0)); + } +} diff --git a/php/alephx.php b/php/alephx.php new file mode 100644 index 0000000000000..4ac79518aaf08 --- /dev/null +++ b/php/alephx.php @@ -0,0 +1,635 @@ +deep_extend(parent::describe(), array( + 'id' => 'alephx', + 'name' => 'AlephX', + 'countries' => array( 'CA' ), + 'pro' => true, + 'certified' => false, + // rate-limits => N/A + 'rateLimit' => 1000, + 'version' => 'v1', + 'userAgent' => $this->userAgents['chrome'], + // 'headers' => array( + // 'ZKX-VERSION' => '2018-05-30', + // ), + 'has' => array( + 'CORS' => true, + 'spot' => true, + 'margin' => false, + 'swap' => false, + 'future' => false, + 'option' => false, + 'addMargin' => false, + 'cancelOrder' => true, + 'cancelOrders' => false, + 'createDepositAddress' => false, + 'createLimitBuyOrder' => false, + 'createLimitSellOrder' => false, + 'createMarketBuyOrder' => false, + 'createMarketBuyOrderWithCost' => false, + 'createMarketOrderWithCost' => false, + 'createMarketSellOrder' => false, + 'createMarketSellOrderWithCost' => false, + 'createOrder' => true, + 'createPostOnlyOrder' => false, + 'createReduceOnlyOrder' => false, + 'createStopLimitOrder' => false, + 'createStopMarketOrder' => false, + 'createStopOrder' => true, + 'deposit' => false, + 'editOrder' => false, + 'fetchAccounts' => false, + 'fetchBalance' => true, + 'fetchBidsAsks' => false, + 'fetchCanceledOrders' => false, + 'fetchCurrencies' => false, + 'fetchDeposit' => false, + 'fetchDepositAddress' => false, + 'fetchDepositAddresses' => false, + 'fetchDepositAddressesByNetwork' => false, + 'fetchDeposits' => false, + 'fetchFundingHistory' => false, + 'fetchFundingRate' => false, + 'fetchFundingRateHistory' => false, + 'fetchFundingRates' => false, + 'fetchIndexOHLCV' => false, + 'fetchIsolatedBorrowRate' => false, + 'fetchIsolatedBorrowRates' => false, + 'fetchL2OrderBook' => false, + 'fetchLedger' => false, + 'fetchLeverage' => false, + 'fetchLeverageTiers' => false, + 'fetchMarginMode' => false, + 'fetchMarkets' => false, + 'fetchMarkOHLCV' => false, + 'fetchMyBuys' => false, + 'fetchMySells' => false, + 'fetchMyTrades' => true, + 'fetchOHLCV' => false, + 'fetchOpenInterestHistory' => false, + 'fetchOpenOrders' => false, + 'fetchOrder' => true, + 'fetchOrderBook' => false, + 'fetchOrders' => true, + 'fetchOrderTrades' => true, + 'fetchPosition' => false, + 'fetchPositionMode' => false, + 'fetchPositions' => false, + 'fetchPositionsRisk' => false, + 'fetchPremiumIndexOHLCV' => false, + 'fetchStatus' => true, + 'fetchTicker' => false, + 'fetchTickers' => false, + 'fetchTime' => false, + 'fetchTrades' => false, + 'fetchTradingFee' => 'emulated', + 'fetchTradingFees' => false, + 'fetchWithdrawals' => false, + 'reduceMargin' => false, + 'sandbox' => true, + 'setLeverage' => false, + 'setMarginMode' => false, + 'setPositionMode' => false, + 'withdraw' => false, + ), + 'urls' => array( + // 'logo' => 'https://user-images.githubusercontent.com/1294454/40811661-b6eceae2-653a-11e8-829e-10bfadb078cf.jpg', + 'test' => array( + 'rest' => 'https://api-testnet.alephx.xyz', + ), + 'api' => array( + 'rest' => 'https://api.alephx.xyz', + ), + 'www' => 'https://demo.alephx.xyz', + // 'doc' => array( + // 'https://developers.alephx.com/api/v1', + // ), + // 'fees' => array( + // 'https://support.alephx.com/customer/portal/articles/2109597-buy-sell-bank-transfer-fees', + // ), + ), + 'requiredCredentials' => array( + 'apiKey' => true, + 'secret' => true, + ), + 'api' => array( + 'v1' => array( + 'public' => array( + 'get' => array( + 'system/status' => 0, + ), + ), + 'private' => array( + 'get' => array( + 'assets/balances' => 0, + 'orders' => 0, + 'orders/{id}' => 0, + 'trades' => 0, + ), + 'post' => array( + 'orders' => 0, + ), + 'patch' => array( + 'orders/{id}/cancel' => 0, + ), + ), + ), + ), + 'exceptions' => array( + 'exact' => array(), + 'broad' => array( + 'Wallet not allowed' => '\\ccxt\\AuthenticationError', + 'Invalid signature' => '\\ccxt\\AuthenticationError', + 'Unauthorized' => '\\ccxt\\PermissionDenied', + 'Order is not cancellable' => '\\ccxt\\BadRequest', + 'Asset is not supported' => '\\ccxt\\BadRequest', + 'Not Found' => '\\ccxt\\OrderNotFound', + ), + ), + )); + } + + public function create_order(string $symbol, string $type, string $side, float $amount, ?float $price = null, $params = array ()) { + /** + * create an order + * @see POST https://api.alephx.xyz/api/v1/orders + * @param {string} $symbol unified $symbol of the market to create an order in + * @param {string} $type 'market' or 'limit' + * @param {string} $side 'buy' or 'sell' + * @param {float} $amount how much you want to trade in units of the base currency, quote currency for 'market' 'buy' orders + * @param {float} [$price] the $price to fulfill the order, in units of the quote currency, ignored in market orders + * @param {array} [$params] extra parameters specific to the exchange API endpoint + * @param {string} [$params->timeInForce] 'gtc' + * @param {string} [$params->idempotencyKey] uuid for idempotency key + * @return {array} an ~@link https://docs.ccxt.com/#/?id=order-structure order structure~ + */ + $request = array( + 'symbol' => $symbol, + 'type' => $type, + 'side' => $side, + 'quantity' => (string) $amount, + 'price' => (string) $price, + 'time_in_force' => $this->safe_string_2($params, 'timeInForce', 'gtc'), + 'idempotency_key' => $this->safe_string_2($params, 'idempotencyKey', $this->uuid()), + ); + $response = $this->v1PrivatePostOrders ($request); + // + // successful order + // + // + // failed order + // + // + $errorResponse = $this->safe_dict($response, 'error'); + if ($errorResponse !== null) { + $errorReason = $this->safe_string($errorResponse, 'reason'); + $errorMessage = $this->safe_string($errorResponse, 'message'); + throw new ExchangeError($errorReason . '' . $errorMessage); + } + return $this->parse_order($response); + } + + public function parse_order(array $order, ?array $market = null): array { + // + // createOrder + // + // { + // "order_id" => "52cfe5e2-0b29-4c19-a245-a6a773de5030", + // "status" => "pending_new" + // } + // + // + // fetchOrder, fetchOrders, cancelOrder + // + // { + // "id" => "0da4eb8d-c108-4e6c-8c45-0b42fabd3a72", + // "status" => "partially_filled", + // "type" => "limit", + // "symbol" => "CLEO-ALEO", + // "account_id" => "cb77b9ab-f94d-4013-85b7-644b0b9ba9a9", + // "settled_quantity" => "0", + // "base_quantity" => "0.1", + // "filled_quantity" => "0.04", + // "side" => "buy", + // "price" => "12.3", + // "remained_quantity" => "0.06", + // "idempotency_key" => "99888999-93ef-9831-9829-120a082bfcf2", + // "inserted_at" => "2024-09-16T23:47:45.161888Z", + // "fee_asset":null, + // "filled_at" => "2024-09-26T20:08:11.350542Z", + // "average_filled_price" => "12.3", + // "canceled_at":null,"cumulative_fee" => "0", + // "time_in_force" => "gtc", + // "internal_status" => "partially_filled" + // } + // + $createdDateTime = $this->safe_string($order, 'inserted_at'); + $filledDateTime = $this->safe_string($order, 'filled_at'); + return $this->safe_order(array( + 'info' => $order, + 'id' => $this->safe_string($order, 'order_id') || $this->safe_string($order, 'id'), + 'clientOrderId' => $this->safe_string($order, 'idempotency_key'), + 'timestamp' => $createdDateTime ? $this->parse8601($createdDateTime) : null, + 'datetime' => $createdDateTime, + 'lastTradeTimestamp' => $filledDateTime ? $this->parse8601($filledDateTime) : null, + 'symbol' => $this->safe_string($order, 'symbol'), + 'type' => $this->safe_string($order, 'type'), + 'timeInForce' => $this->safe_string($order, 'time_in_force', 'gtc'), + 'postOnly' => true, + 'side' => $this->safe_string_lower($order, 'side'), + 'price' => $this->safe_string($order, 'price'), + 'stopPrice' => null, + 'triggerPrice' => null, + 'amount' => $this->safe_string($order, 'base_quantity'), + 'filled' => $this->safe_string($order, 'filled_quantity'), + 'remaining' => $this->safe_string($order, 'remained_quantity'), + 'cost' => null, + 'average' => $this->safe_string($order, 'average_filled_price'), + 'status' => $this->safe_string($order, 'status'), + 'fee' => array( + 'cost' => $this->safe_string($order, 'cumulative_fee'), + 'currency' => $this->safe_string($order, 'fee_asset'), + ), + 'trades' => null, + ), $market); + } + + public function cancel_order(string $id, ?string $symbol = null, $params = array ()) { + /** + * cancels an open order + * @see PATCH https://api.alephx.xyz/api/v1/orders/{order_id}/cancel + * @param {string} $id order $id + * @param {string} $symbol not used by alephx cancelOrder() + * @param {array} [$params] extra parameters specific to the exchange API endpoint + * @return {array} An ~@link https://docs.ccxt.com/#/?$id=order-structure order structure~ + */ + $request = array( + 'id' => $id, // order $id + ); + $response = $this->v1PrivatePatchOrdersIdCancel ($this->extend($request, $params)); + $errorResponse = $this->safe_dict($response, 'error'); + if ($errorResponse !== null) { + $errorReason = $this->safe_string($errorResponse, 'reason'); + $errorMessage = $this->safe_string($errorResponse, 'message'); + if ($errorReason === 'Not Found') { + throw new OrderNotFound($this->id . ' cancelOrder() error ' . $errorReason); + } elseif ($errorReason === 'Bad Request') { + throw new BadRequest($this->id . ' cancelOrder() error ' . $errorReason . ' ' . $errorMessage); + } else { + throw new ExchangeError($errorReason . '' . $errorMessage); + } + } + return $this->parse_order($response); + } + + public function fetch_order(string $id, ?string $symbol = null, $params = array ()) { + /** + * fetches information on an order made by the user + * @see GET https://api.alephx.xyz/api/v1/orders/{order_id} + * @param {string} $id the order $id + * @param {string} $symbol unified market $symbol that the order was made in + * @param {array} [$params] extra parameters specific to the exchange API endpoint + * @return {array} An ~@link https://docs.ccxt.com/#/?$id=order-structure order structure~ + */ + $request = array( + 'id' => $id, // order $id + ); + $response = $this->v1PrivateGetOrdersId ($this->extend($request, $params)); + $errorResponse = $this->safe_dict($response, 'error'); + if ($errorResponse !== null) { + $errorReason = $this->safe_string($errorResponse, 'reason'); + if ($errorReason === 'Not Found') { + throw new OrderNotFound($this->id . ' fetchOrder() error ' . $errorReason); + } + } + return $this->parse_order($response); + } + + public function fetch_orders(?string $symbol = null, ?int $since = null, ?int $limit = 100, $params = array ()): array { + /** + * fetches information on multiple orders made by the user + * @see GET https://api.alephx.xyz/api/v1/orders/ + * @param {string} $symbol unified $market $symbol that the orders were made in + * @param {int} [$since] the earliest time in ms to fetch orders + * @param {int} [$limit] the maximum number of order structures to retrieve + * @param {array} [$params] extra parameters specific to the exchange API endpoint + * @param {int} [$params->until] the latest time in ms to fetch trades for + * @param {boolean} [$params->paginate] default false, when true will automatically paginate by calling this endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-$params) + * @return {Order[]} a list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~ + */ + $response = $this->v1PrivateGetOrders (); + $market = null; + return $this->parse_orders($response, $market, $since, $limit); + } + + public function fetch_my_trades(?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()) { + /** + * fetch all $trades made by the user + * @see GET https://api.alephx.xyz/api/v1/trades + * @param {string} $symbol unified $market $symbol of the $trades + * @param {int} [$since] timestamp in ms of the earliest order, default is null + * @param {int} [$limit] the maximum number of trade structures to fetch + * @param {array} [$params] extra parameters specific to the exchange API endpoint + * @param {int} [$params->until] the latest time in ms to fetch $trades for + * @param {boolean} [$params->paginate] default false, when true will automatically paginate by calling this endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-$params) + * @return {Trade[]} a list of ~@link https://docs.ccxt.com/#/?id=trade-structure trade structures~ + */ + $response = $this->v1PrivateGetTrades (); + $trades = $this->safe_list($response, 'data'); + $market = null; + // + // { "data" => [ + // array( "id" => "32672029-b46b-4139-9779-95444053f40a", + // "status" => "unsettled", + // "symbol" => "CLEO-ALEO", + // "base_quantity" => "0.01", + // "side" => "buy", + // "price" => "12.3", + // "buy_order_id" => "0da4eb8d-c108-4e6c-8c45-0b42fabd3a72", + // "sell_order_id" => "86c61562-ff14-43c9-9a03-4be804d184d0", + // "quote_quantity" => "0.123", + // "inserted_at" => "2024-09-26T15:18:06.603489Z", + // "aggressor_side" => "sell", + // "fee" => null, + // "fee_asset" => null, + // "updated_at" => "2024-09-26T15:18:06.603489Z" + // )]} + // + return $this->parse_trades($trades, $market, $since, $limit); + } + + public function parse_trade(array $trade, ?array $market = null): array { + // returned $trade + // + // array( + // { + // id => '32672029-b46b-4139-9779-95444053f40a', + // order => '0da4eb8d-c108-4e6c-8c45-0b42fabd3a72', + // info => array( + // id => '32672029-b46b-4139-9779-95444053f40a', + // status => 'unsettled', + // symbol => 'CLEO-ALEO', + // base_quantity => '0.01', + // side => 'buy', + // price => '12.3', + // buy_order_id => '0da4eb8d-c108-4e6c-8c45-0b42fabd3a72', + // sell_order_id => '86c61562-ff14-43c9-9a03-4be804d184d0', + // quote_quantity => '0.123', + // inserted_at => '2024-09-26T15:18:06.603489Z', + // aggressor_side => 'sell', + // fee => null, + // fee_asset => null, + // updated_at => '2024-09-26T15:18:06.603489Z' + // ), + // timestamp => 1727363886603, + // datetime => '2024-09-26T15:18:06.603489Z', + // symbol => 'CLEO-ALEO', + // type => null, + // side => 'buy', + // takerOrMaker => null, + // price => 12.3, + // amount => 0.01, + // cost => 0.123, + // fee => array( cost => null, currency => null ), + // fees => array() + // } + // ) + $createdDateTime = $this->safe_string($trade, 'inserted_at'); + $traderSide = $this->safe_string($trade, 'side'); + $traderOrderId = ($traderSide === 'buy') ? $this->safe_string($trade, 'buy_order_id') : $this->safe_string($trade, 'sell_order_id'); + return $this->safe_trade(array( + 'id' => $this->safe_string($trade, 'id'), + 'order' => $traderOrderId, + 'info' => $trade, + 'timestamp' => $this->parse8601($createdDateTime), + 'datetime' => $createdDateTime, + 'symbol' => $this->safe_string($trade, 'symbol'), + 'type' => 'gtc', + 'side' => $traderSide, + 'takerOrMaker' => null, + 'price' => $this->safe_string($trade, 'price'), + 'amount' => $this->safe_string($trade, 'base_quantity'), + 'cost' => $this->safe_string($trade, 'quote_quantity'), + 'fee' => array( + 'cost' => $this->safe_string($trade, 'fee'), + 'currency' => $this->safe_string($trade, 'fee_asset'), + ), + ), $market); + } + + public function fetch_order_trades(string $id, ?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()) { + /** + * fetch all the $trades made from a single order + * @see https://api.alephx.xyz/api/v1/trades?$filters=[array("field":"order_id","op":"==","value":"order_id")] + * @param {string} $id order $id + * @param {string} $symbol unified $market $symbol + * @param {int} [$since] the earliest time in ms to fetch $trades for + * @param {int} [$limit] the maximum number of $trades to retrieve + * @param {array} [$params] extra parameters specific to the exchange API endpoint + * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?$id=trade-structure trade structures~ + */ + $filters = array(); + $filter = array( + 'field' => 'order_id', + 'op' => '==', + 'value' => $id, + ); + $filters[] = $filter; + $request = array( + 'filters' => json_encode ($filters), + ); + $response = $this->v1PrivateGetTrades ($request); + $trades = $this->safe_list($response, 'data'); + $market = null; + // + // { "data" => [ + // array( "id" => "32672029-b46b-4139-9779-95444053f40a", + // "status" => "unsettled", + // "symbol" => "CLEO-ALEO", + // "base_quantity" => "0.01", + // "side" => "buy", + // "price" => "12.3", + // "buy_order_id" => "0da4eb8d-c108-4e6c-8c45-0b42fabd3a72", + // "sell_order_id" => "86c61562-ff14-43c9-9a03-4be804d184d0", + // "quote_quantity" => "0.123", + // "inserted_at" => "2024-09-26T15:18:06.603489Z", + // "aggressor_side" => "sell", + // "fee" => null, + // "fee_asset" => null, + // "updated_at" => "2024-09-26T15:18:06.603489Z" + // )]} + // + return $this->parse_trades($trades, $market, $since, $limit); + } + + public function fetch_status($params = array ()) { + /** + * the latest known information on the availability of the exchange API + * @see https://api.alephx.xyz/api/v1/system/status + * @param {array} [$params] extra parameters specific to the exchange API endpoint + * @return {array} a ~@link https://docs.ccxt.com/#/?id=exchange-status-structure status structure~ + */ + $response = $this->v1PublicGetSystemStatus ($params); + // + // OK + // + return array( + 'status' => ($response === 'OK') ? 'ok' : 'maintenance', + 'updated' => null, + 'eta' => null, + 'url' => null, + 'info' => $response, + ); + } + + public function fetch_balance($params = array ()): array { + /** + * query for balance and get the amount of funds available for trading or funds locked in orders + * @see https://api.alephx.xyz/api/v1/assets/balances + * @param {array} [$params] extra parameters specific to the exchange API endpoint + * @return {array} a ~@link https://docs.ccxt.com/#/?id=balance-structure balance structure~ + */ + // $this->load_markets(); + $response = $this->v1PrivateGetAssetsBalances ($params); + // array( + // array( + // "total" => "19.996900", + // "available" => "14.756900", + // "asset" => "CLEO", + // "locked" => "5.240000" + // ), + // { + // "total" => "10.054720", + // "available" => "-52.145280", + // "asset" => "ALEO", + // "locked" => "62.200000" + // } + // ) + return $this->parse_balance($response); + } + + public function parse_balance($response): array { + $balances = $this->to_array($response); + $result = array( + 'info' => $response, + 'timestamp' => null, + 'datetime' => null, + ); + for ($i = 0; $i < count($balances); $i++) { + $balance = $balances[$i]; + $code = $this->safe_string($balance, 'asset'); + $account = $this->account(); + $account['free'] = $this->safe_string($balance, 'available'); + $account['used'] = $this->safe_string($balance, 'locked'); + $account['total'] = $this->safe_string($balance, 'total'); + $result[$code] = $account; + } + return $this->safe_balance($result); + } + + public function sign($path, $api = [], $method = 'GET', $params = array (), $headers = null, $body = null) { + $version = $api[0]; + $signed = $api[1] === 'private'; + $fullPath = '/api/' . $version . '/' . $this->implode_params($path, $params); + $query = $this->omit($params, $this->extract_params($path)); + $savedPath = $fullPath; + if ($method === 'GET') { + if ($query) { + $fullPath .= '?' . $this->urlencode_with_array_repeat($query); + } + } + $url = $this->urls['api']['rest'] . $fullPath; + if ($signed) { + $authorization = $this->safe_string($this->headers, 'Authorization'); + $authorizationString = null; + if ($authorization !== null) { + $authorizationString = $authorization; + } elseif ($this->token && !$this->check_required_credentials(false)) { + $authorizationString = 'Bearer ' . $this->token; + } else { + $this->check_required_credentials(); + if ($method !== 'GET') { + if ($query) { + $body = $this->json($query); + } + } + // doesn't need payload in the $signature-> inside $url is enough + $timestampString = (string) $this->seconds(); + $auth = $timestampString . $method . $savedPath; + $signature = $this->hmac($this->encode($auth), $this->encode($this->secret), 'sha256'); + $headers = array( + 'ZKX_ACCESS_KEY' => $this->apiKey, + 'ZKX_ACCESS_SIGN' => $signature, + 'ZKX_ACCESS_TIMESTAMP' => $timestampString, + 'Content-Type' => 'application/json', + ); + } + if ($authorizationString !== null) { + $headers = array( + 'Authorization' => $authorizationString, + 'Content-Type' => 'application/json', + ); + if ($method !== 'GET') { + if ($query) { + $body = $this->json($query); + } + } + } + } + return array( 'url' => $url, 'method' => $method, 'body' => $body, 'headers' => $headers ); + } + + public function handle_errors(int $code, string $reason, string $url, string $method, array $headers, string $body, $response, $requestHeaders, $requestBody) { + if ($response === null) { + return null; // fallback to default error handler + } + $feedback = $this->id . ' ' . $body; + // + // + // { + // "error" => { + // { + // "reason" => "Bad Request", + // "message" => "Order is not cancellable" + // } + // } + // } + // + $errorResponse = $this->safe_dict($response, 'error'); + $errorCode = $this->safe_string($errorResponse, 'reason'); + if ($errorCode !== null) { + $errorMessage = $this->safe_string($errorResponse, 'message'); + $this->throw_exactly_matched_exception($this->exceptions['exact'], $errorCode, $feedback); + $this->throw_broadly_matched_exception($this->exceptions['broad'], $errorMessage, $feedback); + throw new ExchangeError($feedback); + } + // $errors = $this->safe_list($response, 'errors'); + // if ($errors !== null) { + // if (gettype($errors) === 'array' && array_keys($errors) === array_keys(array_keys($errors))) { + // $numErrors = count($errors); + // if ($numErrors > 0) { + // $errorCode = $this->safe_string($errors[0], 'id'); + // $errorMessage = $this->safe_string($errors[0], 'message'); + // if ($errorCode !== null) { + // $this->throw_exactly_matched_exception($this->exceptions['exact'], $errorCode, $feedback); + // $this->throw_broadly_matched_exception($this->exceptions['broad'], $errorMessage, $feedback); + // throw new ExchangeError($feedback); + // } + // } + // } + // } + return null; + } +} diff --git a/php/async/abstract/alephx.php b/php/async/abstract/alephx.php new file mode 100644 index 0000000000000..a689f3bd2ba2d --- /dev/null +++ b/php/async/abstract/alephx.php @@ -0,0 +1,52 @@ +request('system/status', array('v1', 'public'), 'GET', $params, null, null, array("cost" => 0)); + } + public function v1_private_get_assets_balances($params = array()) { + return $this->request('assets/balances', array('v1', 'private'), 'GET', $params, null, null, array("cost" => 0)); + } + public function v1_private_get_orders($params = array()) { + return $this->request('orders', array('v1', 'private'), 'GET', $params, null, null, array("cost" => 0)); + } + public function v1_private_get_orders_id($params = array()) { + return $this->request('orders/{id}', array('v1', 'private'), 'GET', $params, null, null, array("cost" => 0)); + } + public function v1_private_get_trades($params = array()) { + return $this->request('trades', array('v1', 'private'), 'GET', $params, null, null, array("cost" => 0)); + } + public function v1_private_post_orders($params = array()) { + return $this->request('orders', array('v1', 'private'), 'POST', $params, null, null, array("cost" => 0)); + } + public function v1_private_patch_orders_id_cancel($params = array()) { + return $this->request('orders/{id}/cancel', array('v1', 'private'), 'PATCH', $params, null, null, array("cost" => 0)); + } + public function v1PublicGetSystemStatus($params = array()) { + return $this->request('system/status', array('v1', 'public'), 'GET', $params, null, null, array("cost" => 0)); + } + public function v1PrivateGetAssetsBalances($params = array()) { + return $this->request('assets/balances', array('v1', 'private'), 'GET', $params, null, null, array("cost" => 0)); + } + public function v1PrivateGetOrders($params = array()) { + return $this->request('orders', array('v1', 'private'), 'GET', $params, null, null, array("cost" => 0)); + } + public function v1PrivateGetOrdersId($params = array()) { + return $this->request('orders/{id}', array('v1', 'private'), 'GET', $params, null, null, array("cost" => 0)); + } + public function v1PrivateGetTrades($params = array()) { + return $this->request('trades', array('v1', 'private'), 'GET', $params, null, null, array("cost" => 0)); + } + public function v1PrivatePostOrders($params = array()) { + return $this->request('orders', array('v1', 'private'), 'POST', $params, null, null, array("cost" => 0)); + } + public function v1PrivatePatchOrdersIdCancel($params = array()) { + return $this->request('orders/{id}/cancel', array('v1', 'private'), 'PATCH', $params, null, null, array("cost" => 0)); + } +} diff --git a/php/async/alephx.php b/php/async/alephx.php new file mode 100644 index 0000000000000..61d9317f9eadd --- /dev/null +++ b/php/async/alephx.php @@ -0,0 +1,652 @@ +deep_extend(parent::describe(), array( + 'id' => 'alephx', + 'name' => 'AlephX', + 'countries' => array( 'CA' ), + 'pro' => true, + 'certified' => false, + // rate-limits => N/A + 'rateLimit' => 1000, + 'version' => 'v1', + 'userAgent' => $this->userAgents['chrome'], + // 'headers' => array( + // 'ZKX-VERSION' => '2018-05-30', + // ), + 'has' => array( + 'CORS' => true, + 'spot' => true, + 'margin' => false, + 'swap' => false, + 'future' => false, + 'option' => false, + 'addMargin' => false, + 'cancelOrder' => true, + 'cancelOrders' => false, + 'createDepositAddress' => false, + 'createLimitBuyOrder' => false, + 'createLimitSellOrder' => false, + 'createMarketBuyOrder' => false, + 'createMarketBuyOrderWithCost' => false, + 'createMarketOrderWithCost' => false, + 'createMarketSellOrder' => false, + 'createMarketSellOrderWithCost' => false, + 'createOrder' => true, + 'createPostOnlyOrder' => false, + 'createReduceOnlyOrder' => false, + 'createStopLimitOrder' => false, + 'createStopMarketOrder' => false, + 'createStopOrder' => true, + 'deposit' => false, + 'editOrder' => false, + 'fetchAccounts' => false, + 'fetchBalance' => true, + 'fetchBidsAsks' => false, + 'fetchCanceledOrders' => false, + 'fetchCurrencies' => false, + 'fetchDeposit' => false, + 'fetchDepositAddress' => false, + 'fetchDepositAddresses' => false, + 'fetchDepositAddressesByNetwork' => false, + 'fetchDeposits' => false, + 'fetchFundingHistory' => false, + 'fetchFundingRate' => false, + 'fetchFundingRateHistory' => false, + 'fetchFundingRates' => false, + 'fetchIndexOHLCV' => false, + 'fetchIsolatedBorrowRate' => false, + 'fetchIsolatedBorrowRates' => false, + 'fetchL2OrderBook' => false, + 'fetchLedger' => false, + 'fetchLeverage' => false, + 'fetchLeverageTiers' => false, + 'fetchMarginMode' => false, + 'fetchMarkets' => false, + 'fetchMarkOHLCV' => false, + 'fetchMyBuys' => false, + 'fetchMySells' => false, + 'fetchMyTrades' => true, + 'fetchOHLCV' => false, + 'fetchOpenInterestHistory' => false, + 'fetchOpenOrders' => false, + 'fetchOrder' => true, + 'fetchOrderBook' => false, + 'fetchOrders' => true, + 'fetchOrderTrades' => true, + 'fetchPosition' => false, + 'fetchPositionMode' => false, + 'fetchPositions' => false, + 'fetchPositionsRisk' => false, + 'fetchPremiumIndexOHLCV' => false, + 'fetchStatus' => true, + 'fetchTicker' => false, + 'fetchTickers' => false, + 'fetchTime' => false, + 'fetchTrades' => false, + 'fetchTradingFee' => 'emulated', + 'fetchTradingFees' => false, + 'fetchWithdrawals' => false, + 'reduceMargin' => false, + 'setLeverage' => false, + 'setMarginMode' => false, + 'setPositionMode' => false, + 'withdraw' => false, + ), + 'urls' => array( + // 'logo' => 'https://user-images.githubusercontent.com/1294454/40811661-b6eceae2-653a-11e8-829e-10bfadb078cf.jpg', + 'api' => array( + 'rest' => 'https://api.alephx.xyz', + ), + 'www' => 'https://demo.alephx.xyz', + // 'doc' => array( + // 'https://developers.alephx.com/api/v1', + // ), + // 'fees' => array( + // 'https://support.alephx.com/customer/portal/articles/2109597-buy-sell-bank-transfer-fees', + // ), + ), + 'requiredCredentials' => array( + 'apiKey' => true, + 'secret' => true, + ), + 'api' => array( + 'v1' => array( + 'public' => array( + 'get' => array( + 'system/status' => 0, + ), + ), + 'private' => array( + 'get' => array( + 'assets/balances' => 0, + 'orders' => 0, + 'orders/{id}' => 0, + 'trades' => 0, + ), + 'post' => array( + 'orders' => 0, + ), + 'patch' => array( + 'orders/{id}/cancel' => 0, + ), + ), + ), + ), + 'exceptions' => array( + 'exact' => array(), + 'broad' => array( + 'Wallet not allowed' => '\\ccxt\\AuthenticationError', + 'Invalid signature' => '\\ccxt\\AuthenticationError', + 'Unauthorized' => '\\ccxt\\PermissionDenied', + 'Order is not cancellable' => '\\ccxt\\BadRequest', + 'Asset is not supported' => '\\ccxt\\BadRequest', + 'Not Found' => '\\ccxt\\OrderNotFound', + ), + ), + )); + } + + public function create_order(string $symbol, string $type, string $side, float $amount, ?float $price = null, $params = array ()) { + return Async\async(function () use ($symbol, $type, $side, $amount, $price, $params) { + /** + * create an order + * @see POST https://api.alephx.xyz/api/v1/orders + * @param {string} $symbol unified $symbol of the market to create an order in + * @param {string} $type 'market' or 'limit' + * @param {string} $side 'buy' or 'sell' + * @param {float} $amount how much you want to trade in units of the base currency, quote currency for 'market' 'buy' orders + * @param {float} [$price] the $price to fulfill the order, in units of the quote currency, ignored in market orders + * @param {array} [$params] extra parameters specific to the exchange API endpoint + * @param {string} [$params->timeInForce] 'gtc' + * @param {string} [$params->idempotencyKey] uuid for idempotency key + * @return {array} an ~@link https://docs.ccxt.com/#/?id=order-structure order structure~ + */ + $request = array( + 'symbol' => $symbol, + 'type' => $type, + 'side' => $side, + 'quantity' => (string) $amount, + 'price' => (string) $price, + 'time_in_force' => $this->safe_string_2($params, 'timeInForce', 'gtc'), + 'idempotency_key' => $this->safe_string_2($params, 'idempotencyKey', $this->uuid()), + ); + $response = Async\await($this->v1PrivatePostOrders ($request)); + // + // successful order + // + // + // failed order + // + // + $errorResponse = $this->safe_dict($response, 'error'); + if ($errorResponse !== null) { + $errorReason = $this->safe_string($errorResponse, 'reason'); + $errorMessage = $this->safe_string($errorResponse, 'message'); + throw new ExchangeError($errorReason . '' . $errorMessage); + } + return $this->parse_order($response); + }) (); + } + + public function parse_order(array $order, ?array $market = null): array { + // + // createOrder + // + // { + // "order_id" => "52cfe5e2-0b29-4c19-a245-a6a773de5030", + // "status" => "pending_new" + // } + // + // + // fetchOrder, fetchOrders, cancelOrder + // + // { + // "id" => "0da4eb8d-c108-4e6c-8c45-0b42fabd3a72", + // "status" => "partially_filled", + // "type" => "limit", + // "symbol" => "CLEO-ALEO", + // "account_id" => "cb77b9ab-f94d-4013-85b7-644b0b9ba9a9", + // "settled_quantity" => "0", + // "base_quantity" => "0.1", + // "filled_quantity" => "0.04", + // "side" => "buy", + // "price" => "12.3", + // "remained_quantity" => "0.06", + // "idempotency_key" => "99888999-93ef-9831-9829-120a082bfcf2", + // "inserted_at" => "2024-09-16T23:47:45.161888Z", + // "fee_asset":null, + // "filled_at" => "2024-09-26T20:08:11.350542Z", + // "average_filled_price" => "12.3", + // "canceled_at":null,"cumulative_fee" => "0", + // "time_in_force" => "gtc", + // "internal_status" => "partially_filled" + // } + // + $createdDateTime = $this->safe_string($order, 'inserted_at'); + $filledDateTime = $this->safe_string($order, 'filled_at'); + return $this->safe_order(array( + 'info' => $order, + 'id' => $this->safe_string($order, 'order_id') || $this->safe_string($order, 'id'), + 'clientOrderId' => $this->safe_string($order, 'idempotency_key'), + 'timestamp' => $createdDateTime ? $this->parse8601($createdDateTime) : null, + 'datetime' => $createdDateTime, + 'lastTradeTimestamp' => $filledDateTime ? $this->parse8601($filledDateTime) : null, + 'symbol' => $this->safe_string($order, 'symbol'), + 'type' => $this->safe_string($order, 'type'), + 'timeInForce' => $this->safe_string($order, 'time_in_force', 'gtc'), + 'postOnly' => true, + 'side' => $this->safe_string_lower($order, 'side'), + 'price' => $this->safe_string($order, 'price'), + 'stopPrice' => null, + 'triggerPrice' => null, + 'amount' => $this->safe_string($order, 'base_quantity'), + 'filled' => $this->safe_string($order, 'filled_quantity'), + 'remaining' => $this->safe_string($order, 'remained_quantity'), + 'cost' => null, + 'average' => $this->safe_string($order, 'average_filled_price'), + 'status' => $this->safe_string($order, 'status'), + 'fee' => array( + 'cost' => $this->safe_string($order, 'cumulative_fee'), + 'currency' => $this->safe_string($order, 'fee_asset'), + ), + 'trades' => null, + ), $market); + } + + public function cancel_order(string $id, ?string $symbol = null, $params = array ()) { + return Async\async(function () use ($id, $symbol, $params) { + /** + * cancels an open order + * @see PATCH https://api.alephx.xyz/api/v1/orders/{order_id}/cancel + * @param {string} $id order $id + * @param {string} $symbol not used by alephx cancelOrder() + * @param {array} [$params] extra parameters specific to the exchange API endpoint + * @return {array} An ~@link https://docs.ccxt.com/#/?$id=order-structure order structure~ + */ + $request = array( + 'id' => $id, // order $id + ); + $response = Async\await($this->v1PrivatePatchOrdersIdCancel ($this->extend($request, $params))); + $errorResponse = $this->safe_dict($response, 'error'); + if ($errorResponse !== null) { + $errorReason = $this->safe_string($errorResponse, 'reason'); + $errorMessage = $this->safe_string($errorResponse, 'message'); + if ($errorReason === 'Not Found') { + throw new OrderNotFound($this->id . ' cancelOrder() error ' . $errorReason); + } elseif ($errorReason === 'Bad Request') { + throw new BadRequest($this->id . ' cancelOrder() error ' . $errorReason . ' ' . $errorMessage); + } else { + throw new ExchangeError($errorReason . '' . $errorMessage); + } + } + return $this->parse_order($response); + }) (); + } + + public function fetch_order(string $id, ?string $symbol = null, $params = array ()) { + return Async\async(function () use ($id, $symbol, $params) { + /** + * fetches information on an order made by the user + * @see GET https://api.alephx.xyz/api/v1/orders/{order_id} + * @param {string} $id the order $id + * @param {string} $symbol unified market $symbol that the order was made in + * @param {array} [$params] extra parameters specific to the exchange API endpoint + * @return {array} An ~@link https://docs.ccxt.com/#/?$id=order-structure order structure~ + */ + $request = array( + 'id' => $id, // order $id + ); + $response = Async\await($this->v1PrivateGetOrdersId ($this->extend($request, $params))); + $errorResponse = $this->safe_dict($response, 'error'); + if ($errorResponse !== null) { + $errorReason = $this->safe_string($errorResponse, 'reason'); + if ($errorReason === 'Not Found') { + throw new OrderNotFound($this->id . ' fetchOrder() error ' . $errorReason); + } + } + return $this->parse_order($response); + }) (); + } + + public function fetch_orders(?string $symbol = null, ?int $since = null, ?int $limit = 100, $params = array ()): PromiseInterface { + return Async\async(function () use ($symbol, $since, $limit, $params) { + /** + * fetches information on multiple orders made by the user + * @see GET https://api.alephx.xyz/api/v1/orders/ + * @param {string} $symbol unified $market $symbol that the orders were made in + * @param {int} [$since] the earliest time in ms to fetch orders + * @param {int} [$limit] the maximum number of order structures to retrieve + * @param {array} [$params] extra parameters specific to the exchange API endpoint + * @param {int} [$params->until] the latest time in ms to fetch trades for + * @param {boolean} [$params->paginate] default false, when true will automatically paginate by calling this endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-$params) + * @return {Order[]} a list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~ + */ + $response = Async\await($this->v1PrivateGetOrders ()); + $market = null; + return $this->parse_orders($response, $market, $since, $limit); + }) (); + } + + public function fetch_my_trades(?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()) { + return Async\async(function () use ($symbol, $since, $limit, $params) { + /** + * fetch all $trades made by the user + * @see GET https://api.alephx.xyz/api/v1/trades + * @param {string} $symbol unified $market $symbol of the $trades + * @param {int} [$since] timestamp in ms of the earliest order, default is null + * @param {int} [$limit] the maximum number of trade structures to fetch + * @param {array} [$params] extra parameters specific to the exchange API endpoint + * @param {int} [$params->until] the latest time in ms to fetch $trades for + * @param {boolean} [$params->paginate] default false, when true will automatically paginate by calling this endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-$params) + * @return {Trade[]} a list of ~@link https://docs.ccxt.com/#/?id=trade-structure trade structures~ + */ + $response = Async\await($this->v1PrivateGetTrades ()); + $trades = $this->safe_list($response, 'data'); + $market = null; + // + // { "data" => [ + // array( "id" => "32672029-b46b-4139-9779-95444053f40a", + // "status" => "unsettled", + // "symbol" => "CLEO-ALEO", + // "base_quantity" => "0.01", + // "side" => "buy", + // "price" => "12.3", + // "buy_order_id" => "0da4eb8d-c108-4e6c-8c45-0b42fabd3a72", + // "sell_order_id" => "86c61562-ff14-43c9-9a03-4be804d184d0", + // "quote_quantity" => "0.123", + // "inserted_at" => "2024-09-26T15:18:06.603489Z", + // "aggressor_side" => "sell", + // "fee" => null, + // "fee_asset" => null, + // "updated_at" => "2024-09-26T15:18:06.603489Z" + // )]} + // + return $this->parse_trades($trades, $market, $since, $limit); + }) (); + } + + public function parse_trade(array $trade, ?array $market = null): array { + // returned $trade + // + // array( + // { + // id => '32672029-b46b-4139-9779-95444053f40a', + // order => '0da4eb8d-c108-4e6c-8c45-0b42fabd3a72', + // info => array( + // id => '32672029-b46b-4139-9779-95444053f40a', + // status => 'unsettled', + // symbol => 'CLEO-ALEO', + // base_quantity => '0.01', + // side => 'buy', + // price => '12.3', + // buy_order_id => '0da4eb8d-c108-4e6c-8c45-0b42fabd3a72', + // sell_order_id => '86c61562-ff14-43c9-9a03-4be804d184d0', + // quote_quantity => '0.123', + // inserted_at => '2024-09-26T15:18:06.603489Z', + // aggressor_side => 'sell', + // fee => null, + // fee_asset => null, + // updated_at => '2024-09-26T15:18:06.603489Z' + // ), + // timestamp => 1727363886603, + // datetime => '2024-09-26T15:18:06.603489Z', + // symbol => 'CLEO-ALEO', + // type => null, + // side => 'buy', + // takerOrMaker => null, + // price => 12.3, + // amount => 0.01, + // cost => 0.123, + // fee => array( cost => null, currency => null ), + // fees => array() + // } + // ) + $createdDateTime = $this->safe_string($trade, 'inserted_at'); + $traderSide = $this->safe_string($trade, 'side'); + $traderOrderId = $traderSide === 'buy' ? $this->safe_string($trade, 'buy_order_id') : $this->safe_string($trade, 'sell_order_id'); + return $this->safe_trade(array( + 'id' => $this->safe_string($trade, 'id'), + 'order' => $traderOrderId, + 'info' => $trade, + 'timestamp' => $this->parse8601($createdDateTime), + 'datetime' => $createdDateTime, + 'symbol' => $this->safe_string($trade, 'symbol'), + 'type' => 'gtc', + 'side' => $traderSide, + 'takerOrMaker' => null, + 'price' => $this->safe_string($trade, 'price'), + 'amount' => $this->safe_string($trade, 'base_quantity'), + 'cost' => $this->safe_string($trade, 'quote_quantity'), + 'fee' => array( + 'cost' => $this->safe_string($trade, 'fee'), + 'currency' => $this->safe_string($trade, 'fee_asset'), + ), + ), $market); + } + + public function fetch_order_trades(string $id, ?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()) { + return Async\async(function () use ($id, $symbol, $since, $limit, $params) { + /** + * fetch all the $trades made from a single order + * @see https://api.alephx.xyz/api/v1/trades?$filters=[array("field":"order_id","op":"==","value":"order_id")] + * @param {string} $id order $id + * @param {string} $symbol unified $market $symbol + * @param {int} [$since] the earliest time in ms to fetch $trades for + * @param {int} [$limit] the maximum number of $trades to retrieve + * @param {array} [$params] extra parameters specific to the exchange API endpoint + * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?$id=trade-structure trade structures~ + */ + $filters = array(); + $filter = array( + 'field' => 'order_id', + 'op' => '==', + 'value' => $id, + ); + $filters[] = $filter; + $request = array( + 'filters' => json_encode ($filters), + ); + $response = Async\await($this->v1PrivateGetTrades ($request)); + $trades = $this->safe_list($response, 'data'); + $market = null; + // + // { "data" => [ + // array( "id" => "32672029-b46b-4139-9779-95444053f40a", + // "status" => "unsettled", + // "symbol" => "CLEO-ALEO", + // "base_quantity" => "0.01", + // "side" => "buy", + // "price" => "12.3", + // "buy_order_id" => "0da4eb8d-c108-4e6c-8c45-0b42fabd3a72", + // "sell_order_id" => "86c61562-ff14-43c9-9a03-4be804d184d0", + // "quote_quantity" => "0.123", + // "inserted_at" => "2024-09-26T15:18:06.603489Z", + // "aggressor_side" => "sell", + // "fee" => null, + // "fee_asset" => null, + // "updated_at" => "2024-09-26T15:18:06.603489Z" + // )]} + // + return $this->parse_trades($trades, $market, $since, $limit); + }) (); + } + + public function fetch_status($params = array ()) { + return Async\async(function () use ($params) { + /** + * the latest known information on the availability of the exchange API + * @see https://api.alephx.xyz/api/v1/system/status + * @param {array} [$params] extra parameters specific to the exchange API endpoint + * @return {array} a ~@link https://docs.ccxt.com/#/?id=exchange-status-structure status structure~ + */ + $response = Async\await($this->v1PublicGetSystemStatus ($params)); + // + // OK + // + return array( + 'status' => ($response === 'OK') ? 'ok' : 'maintenance', + 'updated' => null, + 'eta' => null, + 'url' => null, + 'info' => $response, + ); + }) (); + } + + public function fetch_balance($params = array ()): PromiseInterface { + return Async\async(function () use ($params) { + /** + * query for balance and get the amount of funds available for trading or funds locked in orders + * @see https://api.alephx.xyz/api/v1/assets/balances + * @param {array} [$params] extra parameters specific to the exchange API endpoint + * @return {array} a ~@link https://docs.ccxt.com/#/?id=balance-structure balance structure~ + */ + // Async\await($this->load_markets()); + $response = Async\await($this->v1PrivateGetAssetsBalances ($params)); + // array( + // array( + // "total" => "19.996900", + // "available" => "14.756900", + // "asset" => "CLEO", + // "locked" => "5.240000" + // ), + // { + // "total" => "10.054720", + // "available" => "-52.145280", + // "asset" => "ALEO", + // "locked" => "62.200000" + // } + // ) + return $this->parse_balance($response); + }) (); + } + + public function parse_balance($response): array { + $balances = $this->to_array($response); + $result = array( + 'info' => $response, + 'timestamp' => null, + 'datetime' => null, + ); + for ($i = 0; $i < count($balances); $i++) { + $balance = $balances[$i]; + $code = $this->safe_string($balance, 'asset'); + $account = $this->account(); + $account['free'] = $this->safe_string($balance, 'available'); + $account['used'] = $this->safe_string($balance, 'locked'); + $account['total'] = $this->safe_string($balance, 'total'); + $result[$code] = $account; + } + return $this->safe_balance($result); + } + + public function sign($path, $api = [], $method = 'GET', $params = array (), $headers = null, $body = null) { + $version = $api[0]; + $signed = $api[1] === 'private'; + $fullPath = '/api/' . $version . '/' . $this->implode_params($path, $params); + $query = $this->omit($params, $this->extract_params($path)); + $savedPath = $fullPath; + if ($method === 'GET') { + if ($query) { + $fullPath .= '?' . $this->urlencode_with_array_repeat($query); + } + } + $url = $this->urls['api']['rest'] . $fullPath; + if ($signed) { + $authorization = $this->safe_string($this->headers, 'Authorization'); + $authorizationString = null; + if ($authorization !== null) { + $authorizationString = $authorization; + } elseif ($this->token && !$this->check_required_credentials(false)) { + $authorizationString = 'Bearer ' . $this->token; + } else { + $this->check_required_credentials(); + if ($method !== 'GET') { + if ($query) { + $body = $this->json($query); + } + } + // doesn't need payload in the $signature-> inside $url is enough + $timestampString = (string) $this->seconds(); + $auth = $timestampString . $method . $savedPath; + $signature = $this->hmac($this->encode($auth), $this->encode($this->secret), 'sha256'); + $headers = array( + 'ZKX_ACCESS_KEY' => $this->apiKey, + 'ZKX_ACCESS_SIGN' => $signature, + 'ZKX_ACCESS_TIMESTAMP' => $timestampString, + 'Content-Type' => 'application/json', + ); + } + if ($authorizationString !== null) { + $headers = array( + 'Authorization' => $authorizationString, + 'Content-Type' => 'application/json', + ); + if ($method !== 'GET') { + if ($query) { + $body = $this->json($query); + } + } + } + } + return array( 'url' => $url, 'method' => $method, 'body' => $body, 'headers' => $headers ); + } + + public function handle_errors(int $code, string $reason, string $url, string $method, array $headers, string $body, $response, $requestHeaders, $requestBody) { + if ($response === null) { + return null; // fallback to default error handler + } + $feedback = $this->id . ' ' . $body; + // + // + // { + // "error" => { + // { + // "reason" => "Bad Request", + // "message" => "Order is not cancellable" + // } + // } + // } + // + $errorResponse = $this->safe_dict($response, 'error'); + $errorCode = $this->safe_string($errorResponse, 'reason'); + if ($errorCode !== null) { + $errorMessage = $this->safe_string($errorResponse, 'message'); + $this->throw_exactly_matched_exception($this->exceptions['exact'], $errorCode, $feedback); + $this->throw_broadly_matched_exception($this->exceptions['broad'], $errorMessage, $feedback); + throw new ExchangeError($feedback); + } + // $errors = $this->safe_list($response, 'errors'); + // if ($errors !== null) { + // if (gettype($errors) === 'array' && array_keys($errors) === array_keys(array_keys($errors))) { + // $numErrors = count($errors); + // if ($numErrors > 0) { + // $errorCode = $this->safe_string($errors[0], 'id'); + // $errorMessage = $this->safe_string($errors[0], 'message'); + // if ($errorCode !== null) { + // $this->throw_exactly_matched_exception($this->exceptions['exact'], $errorCode, $feedback); + // $this->throw_broadly_matched_exception($this->exceptions['broad'], $errorMessage, $feedback); + // throw new ExchangeError($feedback); + // } + // } + // } + // } + return null; + } +} diff --git a/php/pro/Exchange.php b/php/pro/Exchange.php index 608961d3c88ae..a1d731de2f0a9 100644 --- a/php/pro/Exchange.php +++ b/php/pro/Exchange.php @@ -19,6 +19,7 @@ class Exchange extends \ccxt\async\Exchange { // todo: fix the conflict of ccxt.exchanges vs ccxtpro.exchanges Exchange::$exchanges = array( + 'alephx', 'alpaca', 'ascendex', 'bequant', diff --git a/php/pro/alephx.php b/php/pro/alephx.php new file mode 100644 index 0000000000000..64adee84d4e8c --- /dev/null +++ b/php/pro/alephx.php @@ -0,0 +1,375 @@ +deep_extend(parent::describe(), array( + 'has' => array( + 'ws' => true, + 'cancelAllOrdersWs' => false, + 'cancelOrdersWs' => false, + 'cancelOrderWs' => false, + 'createOrderWs' => false, + 'editOrderWs' => false, + 'fetchBalanceWs' => false, + 'fetchOpenOrdersWs' => false, + 'fetchOrderWs' => false, + 'fetchTradesWs' => false, + 'watchBalance' => false, + 'watchMyTrades' => true, + 'watchOHLCV' => false, + 'watchOrderBook' => false, + 'watchOrderBookForSymbols' => false, + 'watchOrders' => true, + 'watchTicker' => false, + 'watchTickers' => false, + 'watchTrades' => false, + 'watchTradesForSymbols' => false, + ), + 'urls' => array( + 'api' => array( + 'ws' => 'wss://api.alephx.xyz/websocket', + ), + ), + 'options' => array( + 'tradesLimit' => 1000, + 'ordersLimit' => 1000, + 'myTradesLimit' => 1000, + 'sides' => array( + 'bid' => 'bids', + 'offer' => 'asks', + ), + ), + )); + } + + public function subscribe(string $name, bool $isPrivate, $symbol = null, $params = array ()) { + return Async\async(function () use ($name, $isPrivate, $symbol, $params) { + /** + * @ignore + * subscribes to a websocket channel + * @see https://api.alephx.xyz/websocket + * @param {string} $name the $name of the channel + * @param {string|string[]} [$symbol] unified market $symbol + * @param {array} [$params] extra parameters specific to the exchange API endpoint + * @return {array} subscription to a websocket channel + */ + $url = $this->urls['api']['ws']; + $messageHash = $name; + if ($isPrivate) { + $auth = $this->create_ws_auth(); + $url = $url . '?api_key=' . $auth['api_key'] . '×tamp=' . $auth['timestamp'] . '&signature=' . $auth['signature']; + $messageHash = $messageHash . ':' . $auth['api_key']; + } + $subscribe = array( + 'event' => 'phx_join', + 'topic' => $messageHash, + 'payload' => array(), + 'ref' => $messageHash, + 'join_ref' => $messageHash, + ); + return Async\await($this->watch($url, $messageHash, $subscribe, $messageHash)); + }) (); + } + + public function create_ws_auth() { + $subscribe = array(); + $timestamp = $this->number_to_string($this->seconds()); + $this->check_required_credentials(); + $auth = $timestamp; + $subscribe['api_key'] = $this->apiKey; + $subscribe['timestamp'] = $timestamp; + $subscribe['signature'] = $this->hmac($this->encode($auth), $this->encode($this->secret), 'sha256'); + return $subscribe; + } + + public function watch_my_trades(?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface { + return Async\async(function () use ($symbol, $since, $limit, $params) { + /** + * watches information on multiple $trades made by the user + * @see $trades channel + * @param {string} $symbol unified $symbol of the market to fetch $trades for + * @param {int} [$since] timestamp in ms of the earliest trade to fetch + * @param {int} [$limit] the maximum amount of $trades to fetch + * @param {array} [$params] extra parameters specific to the exchange API endpoint + * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=trade-structure trade structures~ + */ + $name = 'trades'; + $trades = Async\await($this->subscribe($name, true, $symbol, $params)); + if ($this->newUpdates) { + $limit = $trades->getLimit ($symbol, $limit); + } + return $this->filter_by_since_limit($trades, $since, $limit, 'timestamp', true); + }) (); + } + + public function watch_orders(?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface { + return Async\async(function () use ($symbol, $since, $limit, $params) { + /** + * watches information on multiple $orders made by the user + * @see $orders channel + * @param {string} [$symbol] unified market $symbol of the market $orders were made in + * @param {int} [$since] the earliest time in ms to fetch $orders for + * @param {int} [$limit] the maximum number of order structures to retrieve + * @param {array} [$params] extra parameters specific to the exchange API endpoint + * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~ + */ + $name = 'orders'; + $orders = Async\await($this->subscribe($name, true, $symbol, $params)); + if ($this->newUpdates) { + $limit = $orders->getLimit ($symbol, $limit); + } + return $this->filter_by_since_limit($orders, $since, $limit, 'timestamp', true); + }) (); + } + + public function handle_trade($client, $message) { + // { + // ref => null, + // $payload => { + // timestamp => '2024-10-04T03:11:30.111216Z', + // channel => 'trades', + // $trade => array( + // symbol => 'CLEO-ALEO', + // price => '1.1', + // base_quantity => '0.1', + // quote_quantity => '0.11', + // buy_order_id => 'ad2066e6-a47c-449d-99be-79ac82e7d163', + // sell_order_id => '1676786b-145f-4dcf-adde-74e5cce9ebc3', + // status => 'unsettled', + // aggressor_side => 'sell', + // id => 'e0b8354a-d71a-4577-bee5-ce52d8fabcf5', + // fee => null, + // fee_asset => null + // } + // ), + // topic => 'trades:cb77b9ab-f94d-4013-85b7-644b0b9ba9a9', + // event => 'trades' + // } + if ($this->myTrades === null) { + $limit = $this->safe_integer($this->options, 'tradesLimit', 1000); + $this->myTrades = new ArrayCacheBySymbolById ($limit); + } + $payload = $this->safe_dict($message, 'payload'); + $trade = $this->safe_dict($payload, 'trade'); + $parsed = $this->parse_ws_trade($trade); + $this->myTrades.append ($parsed); + $messageHash = $this->safe_string($message, 'topic'); + $client->resolve ($this->myTrades, $messageHash); + return $message; + } + + public function parse_ws_trade($trade, $market = null) { + // { + // symbol => 'CLEO-ALEO', + // price => '1.1', + // base_quantity => '0.1', + // quote_quantity => '0.11', + // buy_order_id => 'ad2066e6-a47c-449d-99be-79ac82e7d163', + // sell_order_id => '1676786b-145f-4dcf-adde-74e5cce9ebc3', + // status => 'unsettled', + // aggressor_side => 'sell', + // id => 'e0b8354a-d71a-4577-bee5-ce52d8fabcf5', + // fee => null, + // fee_asset => null + // } + $createdDateTime = $this->safe_string($trade, 'inserted_at'); + $traderSide = $this->safe_string($trade, 'side'); + $traderOrderId = $traderSide === 'buy' ? $this->safe_string($trade, 'buy_order_id') : $this->safe_string($trade, 'sell_order_id'); + return $this->safe_trade(array( + 'id' => $this->safe_string($trade, 'id'), + 'order' => $traderOrderId, + 'info' => $trade, + 'timestamp' => $this->parse8601($createdDateTime), + 'datetime' => $createdDateTime, + 'symbol' => $this->safe_string($trade, 'symbol'), + 'type' => 'gtc', + 'side' => $traderSide, + 'takerOrMaker' => null, + 'price' => $this->safe_string($trade, 'price'), + 'amount' => $this->safe_string($trade, 'base_quantity'), + 'cost' => $this->safe_string($trade, 'quote_quantity'), + 'fee' => array( + 'cost' => $this->safe_string($trade, 'fee'), + 'currency' => $this->safe_string($trade, 'fee_asset'), + ), + ), $market); + } + + public function handle_order($client, $message) { + // { + // ref => null, + // $payload => { + // timestamp => '2024-10-04T02:29:36.263148Z', + // channel => 'orders', + // $order => array( + // id => 'eed7ce96-f34b-483d-8d87-925eef6f0702', + // status => 'new', + // type => 'limit', + // symbol => 'CLEO-ALEO', + // inserted_at => '2024-10-04T02:29:35.693172Z', + // account_id => 'cb77b9ab-f94d-4013-85b7-644b0b9ba9a9', + // updated_at => '2024-10-04T02:29:36.254349Z', + // filled_quantity => '0', + // base_quantity => '0.1', + // idempotency_key => '99888999-93ef-9831-9829-820a082bfcf8', + // price => '1.1', + // remained_quantity => '0.1', + // side => 'buy', + // time_in_force => 'gtc', + // canceled_at => null, + // average_filled_price => null, + // canceled_quantity => '0', + // cumulative_fee => '0', + // fee_asset => null, + // filled_at => null, + // filled_value => '0', + // lock_version => 3, + // quote_quantity => null, + // sequence_id => 187, + // settled_quantity => '0' + // } + // ), + // topic => 'orders:cb77b9ab-f94d-4013-85b7-644b0b9ba9a9', + // event => 'orders' + // } + if ($this->orders === null) { + $limit = $this->safe_integer($this->options, 'ordersLimit', 1000); + $this->orders = new ArrayCacheBySymbolById ($limit); + } + $payload = $this->safe_dict($message, 'payload'); + $order = $this->safe_dict($payload, 'order'); + $parsed = $this->parse_ws_order($order); + $this->orders.append ($parsed); + $messageHash = $this->safe_string($message, 'topic'); + $client->resolve ($this->orders, $messageHash); + return $message; + } + + public function parse_ws_order($order, $market = null) { + // { + // $id => 'eed7ce96-f34b-483d-8d87-925eef6f0702', + // status => 'new', + // type => 'limit', + // symbol => 'CLEO-ALEO', + // inserted_at => '2024-10-04T02:29:35.693172Z', + // account_id => 'cb77b9ab-f94d-4013-85b7-644b0b9ba9a9', + // updated_at => '2024-10-04T02:29:36.254349Z', + // filled_quantity => '0', + // base_quantity => '0.1', + // idempotency_key => '99888999-93ef-9831-9829-820a082bfcf8', + // price => '1.1', + // remained_quantity => '0.1', + // side => 'buy', + // time_in_force => 'gtc', + // canceled_at => null, + // average_filled_price => null, + // canceled_quantity => '0', + // cumulative_fee => '0', + // fee_asset => null, + // filled_at => null, + // filled_value => '0', + // lock_version => 3, + // quote_quantity => null, + // sequence_id => 187, + // settled_quantity => '0' + // } + $id = $this->safe_string($order, 'id'); + $clientOrderId = $this->safe_string($order, 'idempotency_key'); + $createdDateTime = $this->safe_string($order, 'inserted_at'); + $filledDateTime = $this->safe_string($order, 'filled_at'); + $updatedDateTime = $this->safe_string($order, 'updated_at'); + return $this->safe_order(array( + 'info' => $order, + 'symbol' => $this->safe_string($order, 'symbol'), + 'id' => $id, + 'clientOrderId' => $clientOrderId, + 'timestamp' => $this->parse8601($createdDateTime), + 'datetime' => $createdDateTime, + 'lastTradeTimestamp' => $filledDateTime ? $this->parse8601($filledDateTime) : null, + 'type' => $this->safe_string($order, 'type'), + 'timeInForce' => $this->safe_string($order, 'time_in_force', 'gtc'), + 'postOnly' => true, + 'side' => $this->safe_string($order, 'side'), + 'price' => $this->safe_string($order, 'price'), + 'stopPrice' => null, + 'triggerPrice' => null, + 'amount' => $this->safe_string($order, 'base_quantity'), + 'cost' => null, + 'average' => $this->safe_string($order, 'average_filled_price'), + 'filled' => $this->safe_string($order, 'filled_quantity'), + 'remaining' => $this->safe_string($order, 'remained_quantity'), + 'status' => $this->safe_string_lower($order, 'status'), + 'fee' => array( + 'cost' => $this->safe_string($order, 'cumulative_fee'), + 'currency' => $this->safe_string($market, 'fee_asset'), + ), + 'trades' => null, + 'lastUpdatedTimestamp' => $updatedDateTime ? $this->parse8601($updatedDateTime) : null, + )); + } + + public function handle_subscription_status($client, $message) { + // + // { + // "type" => "subscriptions", + // "channels" => array( + // { + // "name" => "level2", + // "product_ids" => array( "ETH-BTC" ) + // } + // ) + // } + // + return $message; + } + + public function handle_heartbeats($client, $message) { + // although the subscription takes a product_ids parameter (i.e. symbol), + // there is no (clear) way of mapping the $message back to the symbol. + // + // { + // "channel" => "heartbeats", + // "client_id" => "", + // "timestamp" => "2023-06-23T20:31:26.122969572Z", + // "sequence_num" => 0, + // "events" => array( + // { + // "current_time" => "2023-06-23 20:31:56.121961769 +0000 UTC m=+91717.525857105", + // "heartbeat_counter" => "3049" + // } + // ) + // } + // + return $message; + } + + public function handle_message($client, $message) { + $channel = $this->safe_string($this->safe_dict($message, 'payload'), 'channel'); + $methods = array( + // 'subscriptions' => array($this, 'handle_subscription_status'), + 'trades' => array($this, 'handle_trade'), + 'orders' => array($this, 'handle_order'), + // 'heartbeats' => array($this, 'handle_heartbeats'), + ); + $type = $this->safe_string($message, 'type'); + if ($type === 'error') { + $errorMessage = $this->safe_string($message, 'message'); + throw new ExchangeError($errorMessage); + } + $method = $this->safe_value($methods, $channel); + if ($method) { + $method($client, $message); + } + } +} diff --git a/python/ccxt/__init__.py b/python/ccxt/__init__.py index 45c76f2a165a7..ac0bfd72d9d65 100644 --- a/python/ccxt/__init__.py +++ b/python/ccxt/__init__.py @@ -84,6 +84,7 @@ from ccxt.base.errors import error_hierarchy # noqa: F401 from ccxt.ace import ace # noqa: F401 +from ccxt.alephx import alephx # noqa: F401 from ccxt.alpaca import alpaca # noqa: F401 from ccxt.ascendex import ascendex # noqa: F401 from ccxt.bequant import bequant # noqa: F401 @@ -196,6 +197,7 @@ exchanges = [ 'ace', + 'alephx', 'alpaca', 'ascendex', 'bequant', diff --git a/python/ccxt/abstract/alephx.py b/python/ccxt/abstract/alephx.py new file mode 100644 index 0000000000000..9cc21904fd16b --- /dev/null +++ b/python/ccxt/abstract/alephx.py @@ -0,0 +1,11 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + v1_public_get_system_status = v1PublicGetSystemStatus = Entry('system/status', ['v1', 'public'], 'GET', {'cost': 0}) + v1_private_get_assets_balances = v1PrivateGetAssetsBalances = Entry('assets/balances', ['v1', 'private'], 'GET', {'cost': 0}) + v1_private_get_orders = v1PrivateGetOrders = Entry('orders', ['v1', 'private'], 'GET', {'cost': 0}) + v1_private_get_orders_id = v1PrivateGetOrdersId = Entry('orders/{id}', ['v1', 'private'], 'GET', {'cost': 0}) + v1_private_get_trades = v1PrivateGetTrades = Entry('trades', ['v1', 'private'], 'GET', {'cost': 0}) + v1_private_post_orders = v1PrivatePostOrders = Entry('orders', ['v1', 'private'], 'POST', {'cost': 0}) + v1_private_patch_orders_id_cancel = v1PrivatePatchOrdersIdCancel = Entry('orders/{id}/cancel', ['v1', 'private'], 'PATCH', {'cost': 0}) diff --git a/python/ccxt/alephx.py b/python/ccxt/alephx.py new file mode 100644 index 0000000000000..619fda9873757 --- /dev/null +++ b/python/ccxt/alephx.py @@ -0,0 +1,611 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.alephx import ImplicitAPI +import hashlib +import json +from ccxt.base.types import Balances, Int, Market, Num, Order, OrderSide, OrderType, Str, Trade +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import BadRequest +from ccxt.base.errors import OrderNotFound + + +class alephx(Exchange, ImplicitAPI): + + def describe(self): + return self.deep_extend(super(alephx, self).describe(), { + 'id': 'alephx', + 'name': 'AlephX', + 'countries': ['CA'], + 'pro': True, + 'certified': False, + # rate-limits: N/A + 'rateLimit': 1000, + 'version': 'v1', + 'userAgent': self.userAgents['chrome'], + # 'headers': { + # 'ZKX-VERSION': '2018-05-30', + # }, + 'has': { + 'CORS': True, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'cancelOrder': True, + 'cancelOrders': False, + 'createDepositAddress': False, + 'createLimitBuyOrder': False, + 'createLimitSellOrder': False, + 'createMarketBuyOrder': False, + 'createMarketBuyOrderWithCost': False, + 'createMarketOrderWithCost': False, + 'createMarketSellOrder': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createPostOnlyOrder': False, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': True, + 'deposit': False, + 'editOrder': False, + 'fetchAccounts': False, + 'fetchBalance': True, + 'fetchBidsAsks': False, + 'fetchCanceledOrders': False, + 'fetchCurrencies': False, + 'fetchDeposit': False, + 'fetchDepositAddress': False, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': False, + 'fetchFundingHistory': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchL2OrderBook': False, + 'fetchLedger': False, + 'fetchLeverage': False, + 'fetchLeverageTiers': False, + 'fetchMarginMode': False, + 'fetchMarkets': False, + 'fetchMarkOHLCV': False, + 'fetchMyBuys': False, + 'fetchMySells': False, + 'fetchMyTrades': True, + 'fetchOHLCV': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrders': False, + 'fetchOrder': True, + 'fetchOrderBook': False, + 'fetchOrders': True, + 'fetchOrderTrades': True, + 'fetchPosition': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': True, + 'fetchTicker': False, + 'fetchTickers': False, + 'fetchTime': False, + 'fetchTrades': False, + 'fetchTradingFee': 'emulated', + 'fetchTradingFees': False, + 'fetchWithdrawals': False, + 'reduceMargin': False, + 'sandbox': True, + 'setLeverage': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'withdraw': False, + }, + 'urls': { + # 'logo': 'https://user-images.githubusercontent.com/1294454/40811661-b6eceae2-653a-11e8-829e-10bfadb078cf.jpg', + 'test': { + 'rest': 'https://api-testnet.alephx.xyz', + }, + 'api': { + 'rest': 'https://api.alephx.xyz', + }, + 'www': 'https://demo.alephx.xyz', + # 'doc': [ + # 'https://developers.alephx.com/api/v1', + # ], + # 'fees': [ + # 'https://support.alephx.com/customer/portal/articles/2109597-buy-sell-bank-transfer-fees', + # ], + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + }, + 'api': { + 'v1': { + 'public': { + 'get': { + 'system/status': 0, + }, + }, + 'private': { + 'get': { + 'assets/balances': 0, + 'orders': 0, + 'orders/{id}': 0, + 'trades': 0, + }, + 'post': { + 'orders': 0, + }, + 'patch': { + 'orders/{id}/cancel': 0, + }, + }, + }, + }, + 'exceptions': { + 'exact': {}, + 'broad': { + 'Wallet not allowed': AuthenticationError, + 'Invalid signature': AuthenticationError, + 'Unauthorized': PermissionDenied, + 'Order is not cancellable': BadRequest, + 'Asset is not supported': BadRequest, + 'Not Found': OrderNotFound, + }, + }, + }) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create an order + POST https://api.alephx.xyz/api/v1/orders + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency, quote currency for 'market' 'buy' orders + :param float [price]: the price to fulfill the order, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.timeInForce]: 'gtc' + :param str [params.idempotencyKey]: uuid for idempotency key + :returns dict: an `order structure ` + """ + request: dict = { + 'symbol': symbol, + 'type': type, + 'side': side, + 'quantity': str(amount), + 'price': str(price), + 'time_in_force': self.safe_string_2(params, 'timeInForce', 'gtc'), + 'idempotency_key': self.safe_string_2(params, 'idempotencyKey', self.uuid()), + } + response = self.v1PrivatePostOrders(request) + # + # successful order + # + # + # failed order + # + # + errorResponse = self.safe_dict(response, 'error') + if errorResponse is not None: + errorReason = self.safe_string(errorResponse, 'reason') + errorMessage = self.safe_string(errorResponse, 'message') + raise ExchangeError(errorReason + '' + errorMessage) + return self.parse_order(response) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # + # { + # "order_id": "52cfe5e2-0b29-4c19-a245-a6a773de5030", + # "status": "pending_new" + # } + # + # + # fetchOrder, fetchOrders, cancelOrder + # + # { + # "id": "0da4eb8d-c108-4e6c-8c45-0b42fabd3a72", + # "status": "partially_filled", + # "type": "limit", + # "symbol": "CLEO-ALEO", + # "account_id": "cb77b9ab-f94d-4013-85b7-644b0b9ba9a9", + # "settled_quantity": "0", + # "base_quantity": "0.1", + # "filled_quantity": "0.04", + # "side": "buy", + # "price": "12.3", + # "remained_quantity": "0.06", + # "idempotency_key": "99888999-93ef-9831-9829-120a082bfcf2", + # "inserted_at": "2024-09-16T23:47:45.161888Z", + # "fee_asset":null, + # "filled_at": "2024-09-26T20:08:11.350542Z", + # "average_filled_price": "12.3", + # "canceled_at":null,"cumulative_fee": "0", + # "time_in_force": "gtc", + # "internal_status": "partially_filled" + # } + # + createdDateTime = self.safe_string(order, 'inserted_at') + filledDateTime = self.safe_string(order, 'filled_at') + return self.safe_order({ + 'info': order, + 'id': self.safe_string(order, 'order_id') or self.safe_string(order, 'id'), + 'clientOrderId': self.safe_string(order, 'idempotency_key'), + 'timestamp': self.parse8601(createdDateTime) if createdDateTime else None, + 'datetime': createdDateTime, + 'lastTradeTimestamp': self.parse8601(filledDateTime) if filledDateTime else None, + 'symbol': self.safe_string(order, 'symbol'), + 'type': self.safe_string(order, 'type'), + 'timeInForce': self.safe_string(order, 'time_in_force', 'gtc'), + 'postOnly': True, + 'side': self.safe_string_lower(order, 'side'), + 'price': self.safe_string(order, 'price'), + 'stopPrice': None, + 'triggerPrice': None, + 'amount': self.safe_string(order, 'base_quantity'), + 'filled': self.safe_string(order, 'filled_quantity'), + 'remaining': self.safe_string(order, 'remained_quantity'), + 'cost': None, + 'average': self.safe_string(order, 'average_filled_price'), + 'status': self.safe_string(order, 'status'), + 'fee': { + 'cost': self.safe_string(order, 'cumulative_fee'), + 'currency': self.safe_string(order, 'fee_asset'), + }, + 'trades': None, + }, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + PATCH https://api.alephx.xyz/api/v1/orders/{order_id}/cancel + :param str id: order id + :param str symbol: not used by alephx cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + request: dict = { + 'id': id, # order id + } + response = self.v1PrivatePatchOrdersIdCancel(self.extend(request, params)) + errorResponse = self.safe_dict(response, 'error') + if errorResponse is not None: + errorReason = self.safe_string(errorResponse, 'reason') + errorMessage = self.safe_string(errorResponse, 'message') + if errorReason == 'Not Found': + raise OrderNotFound(self.id + ' cancelOrder() error ' + errorReason) + elif errorReason == 'Bad Request': + raise BadRequest(self.id + ' cancelOrder() error ' + errorReason + ' ' + errorMessage) + else: + raise ExchangeError(errorReason + '' + errorMessage) + return self.parse_order(response) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + GET https://api.alephx.xyz/api/v1/orders/{order_id} + :param str id: the order id + :param str symbol: unified market symbol that the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + request: dict = { + 'id': id, # order id + } + response = self.v1PrivateGetOrdersId(self.extend(request, params)) + errorResponse = self.safe_dict(response, 'error') + if errorResponse is not None: + errorReason = self.safe_string(errorResponse, 'reason') + if errorReason == 'Not Found': + raise OrderNotFound(self.id + ' fetchOrder() error ' + errorReason) + return self.parse_order(response) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = 100, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + GET https://api.alephx.xyz/api/v1/orders/ + :param str symbol: unified market symbol that the orders were made in + :param int [since]: the earliest time in ms to fetch orders + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Order[]: a list of `order structures ` + """ + response = self.v1PrivateGetOrders() + market = None + return self.parse_orders(response, market, since, limit) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + GET https://api.alephx.xyz/api/v1/trades + :param str symbol: unified market symbol of the trades + :param int [since]: timestamp in ms of the earliest order, default is None + :param int [limit]: the maximum number of trade structures to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + response = self.v1PrivateGetTrades() + trades = self.safe_list(response, 'data') + market = None + # + # {"data": [ + # {"id": "32672029-b46b-4139-9779-95444053f40a", + # "status": "unsettled", + # "symbol": "CLEO-ALEO", + # "base_quantity": "0.01", + # "side": "buy", + # "price": "12.3", + # "buy_order_id": "0da4eb8d-c108-4e6c-8c45-0b42fabd3a72", + # "sell_order_id": "86c61562-ff14-43c9-9a03-4be804d184d0", + # "quote_quantity": "0.123", + # "inserted_at": "2024-09-26T15:18:06.603489Z", + # "aggressor_side": "sell", + # "fee": null, + # "fee_asset": null, + # "updated_at": "2024-09-26T15:18:06.603489Z" + # }]} + # + return self.parse_trades(trades, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # returned trade + # + # [ + # { + # id: '32672029-b46b-4139-9779-95444053f40a', + # order: '0da4eb8d-c108-4e6c-8c45-0b42fabd3a72', + # info: { + # id: '32672029-b46b-4139-9779-95444053f40a', + # status: 'unsettled', + # symbol: 'CLEO-ALEO', + # base_quantity: '0.01', + # side: 'buy', + # price: '12.3', + # buy_order_id: '0da4eb8d-c108-4e6c-8c45-0b42fabd3a72', + # sell_order_id: '86c61562-ff14-43c9-9a03-4be804d184d0', + # quote_quantity: '0.123', + # inserted_at: '2024-09-26T15:18:06.603489Z', + # aggressor_side: 'sell', + # fee: null, + # fee_asset: null, + # updated_at: '2024-09-26T15:18:06.603489Z' + # }, + # timestamp: 1727363886603, + # datetime: '2024-09-26T15:18:06.603489Z', + # symbol: 'CLEO-ALEO', + # type: None, + # side: 'buy', + # takerOrMaker: None, + # price: 12.3, + # amount: 0.01, + # cost: 0.123, + # fee: {cost: None, currency: None}, + # fees: [] + # } + # ] + createdDateTime = self.safe_string(trade, 'inserted_at') + traderSide = self.safe_string(trade, 'side') + traderOrderId = self.safe_string(trade, 'buy_order_id') if (traderSide == 'buy') else self.safe_string(trade, 'sell_order_id') + return self.safe_trade({ + 'id': self.safe_string(trade, 'id'), + 'order': traderOrderId, + 'info': trade, + 'timestamp': self.parse8601(createdDateTime), + 'datetime': createdDateTime, + 'symbol': self.safe_string(trade, 'symbol'), + 'type': 'gtc', + 'side': traderSide, + 'takerOrMaker': None, + 'price': self.safe_string(trade, 'price'), + 'amount': self.safe_string(trade, 'base_quantity'), + 'cost': self.safe_string(trade, 'quote_quantity'), + 'fee': { + 'cost': self.safe_string(trade, 'fee'), + 'currency': self.safe_string(trade, 'fee_asset'), + }, + }, market) + + def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + https://api.alephx.xyz/api/v1/trades?filters=[{"field":"order_id","op":"==","value":"order_id"}] + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + filters = [] + filter: dict = { + 'field': 'order_id', + 'op': '==', + 'value': id, + } + filters.append(filter) + request: dict = { + 'filters': json.dumps(filters), + } + response = self.v1PrivateGetTrades(request) + trades = self.safe_list(response, 'data') + market = None + # + # {"data": [ + # {"id": "32672029-b46b-4139-9779-95444053f40a", + # "status": "unsettled", + # "symbol": "CLEO-ALEO", + # "base_quantity": "0.01", + # "side": "buy", + # "price": "12.3", + # "buy_order_id": "0da4eb8d-c108-4e6c-8c45-0b42fabd3a72", + # "sell_order_id": "86c61562-ff14-43c9-9a03-4be804d184d0", + # "quote_quantity": "0.123", + # "inserted_at": "2024-09-26T15:18:06.603489Z", + # "aggressor_side": "sell", + # "fee": null, + # "fee_asset": null, + # "updated_at": "2024-09-26T15:18:06.603489Z" + # }]} + # + return self.parse_trades(trades, market, since, limit) + + def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + https://api.alephx.xyz/api/v1/system/status + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = self.v1PublicGetSystemStatus(params) + # + # OK + # + return { + 'status': 'ok' if (response == 'OK') else 'maintenance', + 'updated': None, + 'eta': None, + 'url': None, + 'info': response, + } + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + https://api.alephx.xyz/api/v1/assets/balances + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + # self.load_markets() + response = self.v1PrivateGetAssetsBalances(params) + # [ + # { + # "total": "19.996900", + # "available": "14.756900", + # "asset": "CLEO", + # "locked": "5.240000" + # }, + # { + # "total": "10.054720", + # "available": "-52.145280", + # "asset": "ALEO", + # "locked": "62.200000" + # } + # ] + return self.parse_balance(response) + + def parse_balance(self, response) -> Balances: + balances = self.to_array(response) + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + for i in range(0, len(balances)): + balance = balances[i] + code = self.safe_string(balance, 'asset') + account = self.account() + account['free'] = self.safe_string(balance, 'available') + account['used'] = self.safe_string(balance, 'locked') + account['total'] = self.safe_string(balance, 'total') + result[code] = account + return self.safe_balance(result) + + def sign(self, path, api=[], method='GET', params={}, headers=None, body=None): + version = api[0] + signed = api[1] == 'private' + fullPath = '/api/' + version + '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + savedPath = fullPath + if method == 'GET': + if query: + fullPath += '?' + self.urlencode_with_array_repeat(query) + url = self.urls['api']['rest'] + fullPath + if signed: + authorization = self.safe_string(self.headers, 'Authorization') + authorizationString = None + if authorization is not None: + authorizationString = authorization + elif self.token and not self.check_required_credentials(False): + authorizationString = 'Bearer ' + self.token + else: + self.check_required_credentials() + if method != 'GET': + if query: + body = self.json(query) + # doesn't need payload in the signature. inside url is enough + timestampString = str(self.seconds()) + auth = timestampString + method + savedPath + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + headers = { + 'ZKX_ACCESS_KEY': self.apiKey, + 'ZKX_ACCESS_SIGN': signature, + 'ZKX_ACCESS_TIMESTAMP': timestampString, + 'Content-Type': 'application/json', + } + if authorizationString is not None: + headers = { + 'Authorization': authorizationString, + 'Content-Type': 'application/json', + } + if method != 'GET': + if query: + body = self.json(query) + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + feedback = self.id + ' ' + body + # + # + # { + # "error": { + # { + # "reason": "Bad Request", + # "message": "Order is not cancellable" + # } + # } + # } + # + errorResponse = self.safe_dict(response, 'error') + errorCode = self.safe_string(errorResponse, 'reason') + if errorCode is not None: + errorMessage = self.safe_string(errorResponse, 'message') + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], errorMessage, feedback) + raise ExchangeError(feedback) + # errors = self.safe_list(response, 'errors') + # if errors is not None: + # if isinstance(errors, list): + # numErrors = len(errors) + # if numErrors > 0: + # errorCode = self.safe_string(errors[0], 'id') + # errorMessage = self.safe_string(errors[0], 'message') + # if errorCode is not None: + # self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + # self.throw_broadly_matched_exception(self.exceptions['broad'], errorMessage, feedback) + # raise new ExchangeError(feedback) + # } + # } + # } + # } + return None diff --git a/python/ccxt/async_support/__init__.py b/python/ccxt/async_support/__init__.py index 82e45f44f7030..e5e5a188f8671 100644 --- a/python/ccxt/async_support/__init__.py +++ b/python/ccxt/async_support/__init__.py @@ -64,6 +64,7 @@ from ccxt.async_support.ace import ace # noqa: F401 +from ccxt.async_support.alephx import alephx # noqa: F401 from ccxt.async_support.alpaca import alpaca # noqa: F401 from ccxt.async_support.ascendex import ascendex # noqa: F401 from ccxt.async_support.bequant import bequant # noqa: F401 @@ -176,6 +177,7 @@ exchanges = [ 'ace', + 'alephx', 'alpaca', 'ascendex', 'bequant', diff --git a/python/ccxt/async_support/alephx.py b/python/ccxt/async_support/alephx.py new file mode 100644 index 0000000000000..1ae6c6e49b3ef --- /dev/null +++ b/python/ccxt/async_support/alephx.py @@ -0,0 +1,611 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.alephx import ImplicitAPI +import hashlib +import json +from ccxt.base.types import Balances, Int, Market, Num, Order, OrderSide, OrderType, Str, Trade +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import BadRequest +from ccxt.base.errors import OrderNotFound + + +class alephx(Exchange, ImplicitAPI): + + def describe(self): + return self.deep_extend(super(alephx, self).describe(), { + 'id': 'alephx', + 'name': 'AlephX', + 'countries': ['CA'], + 'pro': True, + 'certified': False, + # rate-limits: N/A + 'rateLimit': 1000, + 'version': 'v1', + 'userAgent': self.userAgents['chrome'], + # 'headers': { + # 'ZKX-VERSION': '2018-05-30', + # }, + 'has': { + 'CORS': True, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'cancelOrder': True, + 'cancelOrders': False, + 'createDepositAddress': False, + 'createLimitBuyOrder': False, + 'createLimitSellOrder': False, + 'createMarketBuyOrder': False, + 'createMarketBuyOrderWithCost': False, + 'createMarketOrderWithCost': False, + 'createMarketSellOrder': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createPostOnlyOrder': False, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': True, + 'deposit': False, + 'editOrder': False, + 'fetchAccounts': False, + 'fetchBalance': True, + 'fetchBidsAsks': False, + 'fetchCanceledOrders': False, + 'fetchCurrencies': False, + 'fetchDeposit': False, + 'fetchDepositAddress': False, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': False, + 'fetchFundingHistory': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchL2OrderBook': False, + 'fetchLedger': False, + 'fetchLeverage': False, + 'fetchLeverageTiers': False, + 'fetchMarginMode': False, + 'fetchMarkets': False, + 'fetchMarkOHLCV': False, + 'fetchMyBuys': False, + 'fetchMySells': False, + 'fetchMyTrades': True, + 'fetchOHLCV': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrders': False, + 'fetchOrder': True, + 'fetchOrderBook': False, + 'fetchOrders': True, + 'fetchOrderTrades': True, + 'fetchPosition': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': True, + 'fetchTicker': False, + 'fetchTickers': False, + 'fetchTime': False, + 'fetchTrades': False, + 'fetchTradingFee': 'emulated', + 'fetchTradingFees': False, + 'fetchWithdrawals': False, + 'reduceMargin': False, + 'sandbox': True, + 'setLeverage': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'withdraw': False, + }, + 'urls': { + # 'logo': 'https://user-images.githubusercontent.com/1294454/40811661-b6eceae2-653a-11e8-829e-10bfadb078cf.jpg', + 'test': { + 'rest': 'https://api-testnet.alephx.xyz', + }, + 'api': { + 'rest': 'https://api.alephx.xyz', + }, + 'www': 'https://demo.alephx.xyz', + # 'doc': [ + # 'https://developers.alephx.com/api/v1', + # ], + # 'fees': [ + # 'https://support.alephx.com/customer/portal/articles/2109597-buy-sell-bank-transfer-fees', + # ], + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + }, + 'api': { + 'v1': { + 'public': { + 'get': { + 'system/status': 0, + }, + }, + 'private': { + 'get': { + 'assets/balances': 0, + 'orders': 0, + 'orders/{id}': 0, + 'trades': 0, + }, + 'post': { + 'orders': 0, + }, + 'patch': { + 'orders/{id}/cancel': 0, + }, + }, + }, + }, + 'exceptions': { + 'exact': {}, + 'broad': { + 'Wallet not allowed': AuthenticationError, + 'Invalid signature': AuthenticationError, + 'Unauthorized': PermissionDenied, + 'Order is not cancellable': BadRequest, + 'Asset is not supported': BadRequest, + 'Not Found': OrderNotFound, + }, + }, + }) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create an order + POST https://api.alephx.xyz/api/v1/orders + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency, quote currency for 'market' 'buy' orders + :param float [price]: the price to fulfill the order, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.timeInForce]: 'gtc' + :param str [params.idempotencyKey]: uuid for idempotency key + :returns dict: an `order structure ` + """ + request: dict = { + 'symbol': symbol, + 'type': type, + 'side': side, + 'quantity': str(amount), + 'price': str(price), + 'time_in_force': self.safe_string_2(params, 'timeInForce', 'gtc'), + 'idempotency_key': self.safe_string_2(params, 'idempotencyKey', self.uuid()), + } + response = await self.v1PrivatePostOrders(request) + # + # successful order + # + # + # failed order + # + # + errorResponse = self.safe_dict(response, 'error') + if errorResponse is not None: + errorReason = self.safe_string(errorResponse, 'reason') + errorMessage = self.safe_string(errorResponse, 'message') + raise ExchangeError(errorReason + '' + errorMessage) + return self.parse_order(response) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # + # { + # "order_id": "52cfe5e2-0b29-4c19-a245-a6a773de5030", + # "status": "pending_new" + # } + # + # + # fetchOrder, fetchOrders, cancelOrder + # + # { + # "id": "0da4eb8d-c108-4e6c-8c45-0b42fabd3a72", + # "status": "partially_filled", + # "type": "limit", + # "symbol": "CLEO-ALEO", + # "account_id": "cb77b9ab-f94d-4013-85b7-644b0b9ba9a9", + # "settled_quantity": "0", + # "base_quantity": "0.1", + # "filled_quantity": "0.04", + # "side": "buy", + # "price": "12.3", + # "remained_quantity": "0.06", + # "idempotency_key": "99888999-93ef-9831-9829-120a082bfcf2", + # "inserted_at": "2024-09-16T23:47:45.161888Z", + # "fee_asset":null, + # "filled_at": "2024-09-26T20:08:11.350542Z", + # "average_filled_price": "12.3", + # "canceled_at":null,"cumulative_fee": "0", + # "time_in_force": "gtc", + # "internal_status": "partially_filled" + # } + # + createdDateTime = self.safe_string(order, 'inserted_at') + filledDateTime = self.safe_string(order, 'filled_at') + return self.safe_order({ + 'info': order, + 'id': self.safe_string(order, 'order_id') or self.safe_string(order, 'id'), + 'clientOrderId': self.safe_string(order, 'idempotency_key'), + 'timestamp': self.parse8601(createdDateTime) if createdDateTime else None, + 'datetime': createdDateTime, + 'lastTradeTimestamp': self.parse8601(filledDateTime) if filledDateTime else None, + 'symbol': self.safe_string(order, 'symbol'), + 'type': self.safe_string(order, 'type'), + 'timeInForce': self.safe_string(order, 'time_in_force', 'gtc'), + 'postOnly': True, + 'side': self.safe_string_lower(order, 'side'), + 'price': self.safe_string(order, 'price'), + 'stopPrice': None, + 'triggerPrice': None, + 'amount': self.safe_string(order, 'base_quantity'), + 'filled': self.safe_string(order, 'filled_quantity'), + 'remaining': self.safe_string(order, 'remained_quantity'), + 'cost': None, + 'average': self.safe_string(order, 'average_filled_price'), + 'status': self.safe_string(order, 'status'), + 'fee': { + 'cost': self.safe_string(order, 'cumulative_fee'), + 'currency': self.safe_string(order, 'fee_asset'), + }, + 'trades': None, + }, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + PATCH https://api.alephx.xyz/api/v1/orders/{order_id}/cancel + :param str id: order id + :param str symbol: not used by alephx cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + request: dict = { + 'id': id, # order id + } + response = await self.v1PrivatePatchOrdersIdCancel(self.extend(request, params)) + errorResponse = self.safe_dict(response, 'error') + if errorResponse is not None: + errorReason = self.safe_string(errorResponse, 'reason') + errorMessage = self.safe_string(errorResponse, 'message') + if errorReason == 'Not Found': + raise OrderNotFound(self.id + ' cancelOrder() error ' + errorReason) + elif errorReason == 'Bad Request': + raise BadRequest(self.id + ' cancelOrder() error ' + errorReason + ' ' + errorMessage) + else: + raise ExchangeError(errorReason + '' + errorMessage) + return self.parse_order(response) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + GET https://api.alephx.xyz/api/v1/orders/{order_id} + :param str id: the order id + :param str symbol: unified market symbol that the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + request: dict = { + 'id': id, # order id + } + response = await self.v1PrivateGetOrdersId(self.extend(request, params)) + errorResponse = self.safe_dict(response, 'error') + if errorResponse is not None: + errorReason = self.safe_string(errorResponse, 'reason') + if errorReason == 'Not Found': + raise OrderNotFound(self.id + ' fetchOrder() error ' + errorReason) + return self.parse_order(response) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = 100, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + GET https://api.alephx.xyz/api/v1/orders/ + :param str symbol: unified market symbol that the orders were made in + :param int [since]: the earliest time in ms to fetch orders + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Order[]: a list of `order structures ` + """ + response = await self.v1PrivateGetOrders() + market = None + return self.parse_orders(response, market, since, limit) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + GET https://api.alephx.xyz/api/v1/trades + :param str symbol: unified market symbol of the trades + :param int [since]: timestamp in ms of the earliest order, default is None + :param int [limit]: the maximum number of trade structures to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + response = await self.v1PrivateGetTrades() + trades = self.safe_list(response, 'data') + market = None + # + # {"data": [ + # {"id": "32672029-b46b-4139-9779-95444053f40a", + # "status": "unsettled", + # "symbol": "CLEO-ALEO", + # "base_quantity": "0.01", + # "side": "buy", + # "price": "12.3", + # "buy_order_id": "0da4eb8d-c108-4e6c-8c45-0b42fabd3a72", + # "sell_order_id": "86c61562-ff14-43c9-9a03-4be804d184d0", + # "quote_quantity": "0.123", + # "inserted_at": "2024-09-26T15:18:06.603489Z", + # "aggressor_side": "sell", + # "fee": null, + # "fee_asset": null, + # "updated_at": "2024-09-26T15:18:06.603489Z" + # }]} + # + return self.parse_trades(trades, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # returned trade + # + # [ + # { + # id: '32672029-b46b-4139-9779-95444053f40a', + # order: '0da4eb8d-c108-4e6c-8c45-0b42fabd3a72', + # info: { + # id: '32672029-b46b-4139-9779-95444053f40a', + # status: 'unsettled', + # symbol: 'CLEO-ALEO', + # base_quantity: '0.01', + # side: 'buy', + # price: '12.3', + # buy_order_id: '0da4eb8d-c108-4e6c-8c45-0b42fabd3a72', + # sell_order_id: '86c61562-ff14-43c9-9a03-4be804d184d0', + # quote_quantity: '0.123', + # inserted_at: '2024-09-26T15:18:06.603489Z', + # aggressor_side: 'sell', + # fee: null, + # fee_asset: null, + # updated_at: '2024-09-26T15:18:06.603489Z' + # }, + # timestamp: 1727363886603, + # datetime: '2024-09-26T15:18:06.603489Z', + # symbol: 'CLEO-ALEO', + # type: None, + # side: 'buy', + # takerOrMaker: None, + # price: 12.3, + # amount: 0.01, + # cost: 0.123, + # fee: {cost: None, currency: None}, + # fees: [] + # } + # ] + createdDateTime = self.safe_string(trade, 'inserted_at') + traderSide = self.safe_string(trade, 'side') + traderOrderId = self.safe_string(trade, 'buy_order_id') if (traderSide == 'buy') else self.safe_string(trade, 'sell_order_id') + return self.safe_trade({ + 'id': self.safe_string(trade, 'id'), + 'order': traderOrderId, + 'info': trade, + 'timestamp': self.parse8601(createdDateTime), + 'datetime': createdDateTime, + 'symbol': self.safe_string(trade, 'symbol'), + 'type': 'gtc', + 'side': traderSide, + 'takerOrMaker': None, + 'price': self.safe_string(trade, 'price'), + 'amount': self.safe_string(trade, 'base_quantity'), + 'cost': self.safe_string(trade, 'quote_quantity'), + 'fee': { + 'cost': self.safe_string(trade, 'fee'), + 'currency': self.safe_string(trade, 'fee_asset'), + }, + }, market) + + async def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + https://api.alephx.xyz/api/v1/trades?filters=[{"field":"order_id","op":"==","value":"order_id"}] + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + filters = [] + filter: dict = { + 'field': 'order_id', + 'op': '==', + 'value': id, + } + filters.append(filter) + request: dict = { + 'filters': json.dumps(filters), + } + response = await self.v1PrivateGetTrades(request) + trades = self.safe_list(response, 'data') + market = None + # + # {"data": [ + # {"id": "32672029-b46b-4139-9779-95444053f40a", + # "status": "unsettled", + # "symbol": "CLEO-ALEO", + # "base_quantity": "0.01", + # "side": "buy", + # "price": "12.3", + # "buy_order_id": "0da4eb8d-c108-4e6c-8c45-0b42fabd3a72", + # "sell_order_id": "86c61562-ff14-43c9-9a03-4be804d184d0", + # "quote_quantity": "0.123", + # "inserted_at": "2024-09-26T15:18:06.603489Z", + # "aggressor_side": "sell", + # "fee": null, + # "fee_asset": null, + # "updated_at": "2024-09-26T15:18:06.603489Z" + # }]} + # + return self.parse_trades(trades, market, since, limit) + + async def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + https://api.alephx.xyz/api/v1/system/status + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = await self.v1PublicGetSystemStatus(params) + # + # OK + # + return { + 'status': 'ok' if (response == 'OK') else 'maintenance', + 'updated': None, + 'eta': None, + 'url': None, + 'info': response, + } + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + https://api.alephx.xyz/api/v1/assets/balances + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + # await self.load_markets() + response = await self.v1PrivateGetAssetsBalances(params) + # [ + # { + # "total": "19.996900", + # "available": "14.756900", + # "asset": "CLEO", + # "locked": "5.240000" + # }, + # { + # "total": "10.054720", + # "available": "-52.145280", + # "asset": "ALEO", + # "locked": "62.200000" + # } + # ] + return self.parse_balance(response) + + def parse_balance(self, response) -> Balances: + balances = self.to_array(response) + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + for i in range(0, len(balances)): + balance = balances[i] + code = self.safe_string(balance, 'asset') + account = self.account() + account['free'] = self.safe_string(balance, 'available') + account['used'] = self.safe_string(balance, 'locked') + account['total'] = self.safe_string(balance, 'total') + result[code] = account + return self.safe_balance(result) + + def sign(self, path, api=[], method='GET', params={}, headers=None, body=None): + version = api[0] + signed = api[1] == 'private' + fullPath = '/api/' + version + '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + savedPath = fullPath + if method == 'GET': + if query: + fullPath += '?' + self.urlencode_with_array_repeat(query) + url = self.urls['api']['rest'] + fullPath + if signed: + authorization = self.safe_string(self.headers, 'Authorization') + authorizationString = None + if authorization is not None: + authorizationString = authorization + elif self.token and not self.check_required_credentials(False): + authorizationString = 'Bearer ' + self.token + else: + self.check_required_credentials() + if method != 'GET': + if query: + body = self.json(query) + # doesn't need payload in the signature. inside url is enough + timestampString = str(self.seconds()) + auth = timestampString + method + savedPath + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + headers = { + 'ZKX_ACCESS_KEY': self.apiKey, + 'ZKX_ACCESS_SIGN': signature, + 'ZKX_ACCESS_TIMESTAMP': timestampString, + 'Content-Type': 'application/json', + } + if authorizationString is not None: + headers = { + 'Authorization': authorizationString, + 'Content-Type': 'application/json', + } + if method != 'GET': + if query: + body = self.json(query) + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + feedback = self.id + ' ' + body + # + # + # { + # "error": { + # { + # "reason": "Bad Request", + # "message": "Order is not cancellable" + # } + # } + # } + # + errorResponse = self.safe_dict(response, 'error') + errorCode = self.safe_string(errorResponse, 'reason') + if errorCode is not None: + errorMessage = self.safe_string(errorResponse, 'message') + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], errorMessage, feedback) + raise ExchangeError(feedback) + # errors = self.safe_list(response, 'errors') + # if errors is not None: + # if isinstance(errors, list): + # numErrors = len(errors) + # if numErrors > 0: + # errorCode = self.safe_string(errors[0], 'id') + # errorMessage = self.safe_string(errors[0], 'message') + # if errorCode is not None: + # self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + # self.throw_broadly_matched_exception(self.exceptions['broad'], errorMessage, feedback) + # raise new ExchangeError(feedback) + # } + # } + # } + # } + return None diff --git a/python/ccxt/pro/__init__.py b/python/ccxt/pro/__init__.py index c3170a8e5ffac..b049b9b433205 100644 --- a/python/ccxt/pro/__init__.py +++ b/python/ccxt/pro/__init__.py @@ -12,6 +12,7 @@ # CCXT Pro exchanges (now this is mainly used for importing exchanges in WS tests) +from ccxt.pro.alephx import alephx # noqa: F401 from ccxt.pro.alpaca import alpaca # noqa: F401 from ccxt.pro.ascendex import ascendex # noqa: F401 from ccxt.pro.bequant import bequant # noqa: F401 @@ -87,6 +88,7 @@ from ccxt.pro.xt import xt # noqa: F401 exchanges = [ + 'alephx', 'alpaca', 'ascendex', 'bequant', diff --git a/python/ccxt/pro/alephx.py b/python/ccxt/pro/alephx.py new file mode 100644 index 0000000000000..478a4ffc41012 --- /dev/null +++ b/python/ccxt/pro/alephx.py @@ -0,0 +1,353 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCacheBySymbolById +import hashlib +from ccxt.base.types import Int, Order, Str, Trade +from typing import List +from ccxt.base.errors import ExchangeError + + +class alephx(ccxt.async_support.alephx): + + def describe(self): + return self.deep_extend(super(alephx, self).describe(), { + 'has': { + 'ws': True, + 'cancelAllOrdersWs': False, + 'cancelOrdersWs': False, + 'cancelOrderWs': False, + 'createOrderWs': False, + 'editOrderWs': False, + 'fetchBalanceWs': False, + 'fetchOpenOrdersWs': False, + 'fetchOrderWs': False, + 'fetchTradesWs': False, + 'watchBalance': False, + 'watchMyTrades': True, + 'watchOHLCV': False, + 'watchOrderBook': False, + 'watchOrderBookForSymbols': False, + 'watchOrders': True, + 'watchTicker': False, + 'watchTickers': False, + 'watchTrades': False, + 'watchTradesForSymbols': False, + }, + 'urls': { + 'test': { + 'ws': 'wss://api-testnet.alephx.xyz/websocket', + }, + 'api': { + 'ws': 'wss://api.alephx.xyz/websocket', + }, + }, + 'options': { + 'tradesLimit': 1000, + 'ordersLimit': 1000, + 'myTradesLimit': 1000, + 'sides': { + 'bid': 'bids', + 'offer': 'asks', + }, + }, + }) + + async def subscribe(self, name: str, isPrivate: bool, symbol=None, params={}): + """ + @ignore + subscribes to a websocket channel + https://api.alephx.xyz/websocket + :param str name: the name of the channel + :param string|str[] [symbol]: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: subscription to a websocket channel + """ + url = self.urls['api']['ws'] + messageHash = name + if isPrivate: + auth = self.create_ws_auth() + url = url + '?api_key=' + auth['api_key'] + '×tamp=' + auth['timestamp'] + '&signature=' + auth['signature'] + messageHash = messageHash + ':' + auth['api_key'] + subscribe = { + 'event': 'phx_join', + 'topic': messageHash, + 'payload': {}, + 'ref': messageHash, + 'join_ref': messageHash, + } + return await self.watch(url, messageHash, subscribe, messageHash) + + def create_ws_auth(self): + subscribe: dict = {} + timestamp = self.number_to_string(self.seconds()) + self.check_required_credentials() + auth = timestamp + subscribe['api_key'] = self.apiKey + subscribe['timestamp'] = timestamp + subscribe['signature'] = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + return subscribe + + async def watch_my_trades(self, symbol: str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made by the user + trades channel + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + name = 'trades' + trades = await self.subscribe(name, True, symbol, params) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + orders channel + :param str [symbol]: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + name = 'orders' + orders = await self.subscribe(name, True, symbol, params) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_since_limit(orders, since, limit, 'timestamp', True) + + def handle_trade(self, client, message): + # { + # ref: null, + # payload: { + # timestamp: '2024-10-04T03:11:30.111216Z', + # channel: 'trades', + # trade: { + # symbol: 'CLEO-ALEO', + # price: '1.1', + # base_quantity: '0.1', + # quote_quantity: '0.11', + # buy_order_id: 'ad2066e6-a47c-449d-99be-79ac82e7d163', + # sell_order_id: '1676786b-145f-4dcf-adde-74e5cce9ebc3', + # status: 'unsettled', + # aggressor_side: 'sell', + # id: 'e0b8354a-d71a-4577-bee5-ce52d8fabcf5', + # fee: null, + # fee_asset: null, + # } + # }, + # topic: 'trades:cb77b9ab-f94d-4013-85b7-644b0b9ba9a9', + # event: 'trades' + # } + if self.myTrades is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + self.myTrades = ArrayCacheBySymbolById(limit) + payload = self.safe_dict(message, 'payload') + trade = self.safe_dict(payload, 'trade') + parsed = self.parse_ws_trade(trade) + self.myTrades.append(parsed) + messageHash = self.safe_string(message, 'topic') + client.resolve(self.myTrades, messageHash) + return message + + def parse_ws_trade(self, trade, market=None): + # { + # symbol: 'CLEO-ALEO', + # price: '1.1', + # base_quantity: '0.1', + # quote_quantity: '0.11', + # buy_order_id: 'ad2066e6-a47c-449d-99be-79ac82e7d163', + # sell_order_id: '1676786b-145f-4dcf-adde-74e5cce9ebc3', + # status: 'unsettled', + # aggressor_side: 'sell', + # id: 'e0b8354a-d71a-4577-bee5-ce52d8fabcf5', + # fee: null, + # fee_asset: null + # } + createdDateTime = self.safe_string(trade, 'inserted_at') + traderSide = self.safe_string(trade, 'side') + traderOrderId = self.safe_string(trade, 'buy_order_id') if (traderSide == 'buy') else self.safe_string(trade, 'sell_order_id') + return self.safe_trade({ + 'id': self.safe_string(trade, 'id'), + 'order': traderOrderId, + 'info': trade, + 'timestamp': self.parse8601(createdDateTime), + 'datetime': createdDateTime, + 'symbol': self.safe_string(trade, 'symbol'), + 'type': 'gtc', + 'side': traderSide, + 'takerOrMaker': None, + 'price': self.safe_string(trade, 'price'), + 'amount': self.safe_string(trade, 'base_quantity'), + 'cost': self.safe_string(trade, 'quote_quantity'), + 'fee': { + 'cost': self.safe_string(trade, 'fee'), + 'currency': self.safe_string(trade, 'fee_asset'), + }, + }, market) + + def handle_order(self, client, message): + # { + # ref: null, + # payload: { + # timestamp: '2024-10-04T02:29:36.263148Z', + # channel: 'orders', + # order: { + # id: 'eed7ce96-f34b-483d-8d87-925eef6f0702', + # status: 'new', + # type: 'limit', + # symbol: 'CLEO-ALEO', + # inserted_at: '2024-10-04T02:29:35.693172Z', + # account_id: 'cb77b9ab-f94d-4013-85b7-644b0b9ba9a9', + # updated_at: '2024-10-04T02:29:36.254349Z', + # filled_quantity: '0', + # base_quantity: '0.1', + # idempotency_key: '99888999-93ef-9831-9829-820a082bfcf8', + # price: '1.1', + # remained_quantity: '0.1', + # side: 'buy', + # time_in_force: 'gtc', + # canceled_at: null, + # average_filled_price: null, + # canceled_quantity: '0', + # cumulative_fee: '0', + # fee_asset: null, + # filled_at: null, + # filled_value: '0', + # lock_version: 3, + # quote_quantity: null, + # sequence_id: 187, + # settled_quantity: '0' + # } + # }, + # topic: 'orders:cb77b9ab-f94d-4013-85b7-644b0b9ba9a9', + # event: 'orders' + # } + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + payload = self.safe_dict(message, 'payload') + order = self.safe_dict(payload, 'order') + parsed = self.parse_ws_order(order) + self.orders.append(parsed) + messageHash = self.safe_string(message, 'topic') + client.resolve(self.orders, messageHash) + return message + + def parse_ws_order(self, order, market=None): + # { + # id: 'eed7ce96-f34b-483d-8d87-925eef6f0702', + # status: 'new', + # type: 'limit', + # symbol: 'CLEO-ALEO', + # inserted_at: '2024-10-04T02:29:35.693172Z', + # account_id: 'cb77b9ab-f94d-4013-85b7-644b0b9ba9a9', + # updated_at: '2024-10-04T02:29:36.254349Z', + # filled_quantity: '0', + # base_quantity: '0.1', + # idempotency_key: '99888999-93ef-9831-9829-820a082bfcf8', + # price: '1.1', + # remained_quantity: '0.1', + # side: 'buy', + # time_in_force: 'gtc', + # canceled_at: null, + # average_filled_price: null, + # canceled_quantity: '0', + # cumulative_fee: '0', + # fee_asset: null, + # filled_at: null, + # filled_value: '0', + # lock_version: 3, + # quote_quantity: null, + # sequence_id: 187, + # settled_quantity: '0' + # } + id = self.safe_string(order, 'id') + clientOrderId = self.safe_string(order, 'idempotency_key') + createdDateTime = self.safe_string(order, 'inserted_at') + filledDateTime = self.safe_string(order, 'filled_at') + updatedDateTime = self.safe_string(order, 'updated_at') + return self.safe_order({ + 'info': order, + 'symbol': self.safe_string(order, 'symbol'), + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': self.parse8601(createdDateTime), + 'datetime': createdDateTime, + 'lastTradeTimestamp': self.parse8601(filledDateTime) if filledDateTime else None, + 'type': self.safe_string(order, 'type'), + 'timeInForce': self.safe_string(order, 'time_in_force', 'gtc'), + 'postOnly': True, + 'side': self.safe_string(order, 'side'), + 'price': self.safe_string(order, 'price'), + 'stopPrice': None, + 'triggerPrice': None, + 'amount': self.safe_string(order, 'base_quantity'), + 'cost': None, + 'average': self.safe_string(order, 'average_filled_price'), + 'filled': self.safe_string(order, 'filled_quantity'), + 'remaining': self.safe_string(order, 'remained_quantity'), + 'status': self.safe_string_lower(order, 'status'), + 'fee': { + 'cost': self.safe_string(order, 'cumulative_fee'), + 'currency': self.safe_string(market, 'fee_asset'), + }, + 'trades': None, + 'lastUpdatedTimestamp': self.parse8601(updatedDateTime) if updatedDateTime else None, + }) + + def handle_subscription_status(self, client, message): + # + # { + # "type": "subscriptions", + # "channels": [ + # { + # "name": "level2", + # "product_ids": ["ETH-BTC"] + # } + # ] + # } + # + return message + + def handle_heartbeats(self, client, message): + # although the subscription takes a product_ids parameter(i.e. symbol), + # there is no(clear) way of mapping the message back to the symbol. + # + # { + # "channel": "heartbeats", + # "client_id": "", + # "timestamp": "2023-06-23T20:31:26.122969572Z", + # "sequence_num": 0, + # "events": [ + # { + # "current_time": "2023-06-23 20:31:56.121961769 +0000 UTC m=+91717.525857105", + # "heartbeat_counter": "3049" + # } + # ] + # } + # + return message + + def handle_message(self, client, message): + channel = self.safe_string(self.safe_dict(message, 'payload'), 'channel') + methods: dict = { + # 'subscriptions': self.handle_subscription_status, + 'trades': self.handle_trade, + 'orders': self.handle_order, + # 'heartbeats': self.handle_heartbeats, + } + type = self.safe_string(message, 'type') + if type == 'error': + errorMessage = self.safe_string(message, 'message') + raise ExchangeError(errorMessage) + method = self.safe_value(methods, channel) + if method: + method(client, message) diff --git a/python/ccxt/pro/gate.py b/python/ccxt/pro/gate.py index b37d0b85ecf8f..61bf781a88a9b 100644 --- a/python/ccxt/pro/gate.py +++ b/python/ccxt/pro/gate.py @@ -374,14 +374,14 @@ async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> O market = self.market(symbol) symbol = market['symbol'] marketId = market['id'] - interval, query = self.handle_option_and_params(params, 'watchOrderBook', 'interval', '100ms') + interval, query = self.handle_option_and_params(params, 'watchOrderBook', 'interval', '1000ms') messageType = self.get_type_by_market(market) channel = messageType + '.order_book_update' messageHash = 'orderbook' + ':' + symbol url = self.get_url_by_market(market) payload = [marketId, interval] if limit is None: - limit = 100 + limit = 5 if market['contract']: stringLimit = str(limit) payload.append(stringLimit) diff --git a/ts/ccxt.ts b/ts/ccxt.ts index a4a404b85db96..db224853981ce 100644 --- a/ts/ccxt.ts +++ b/ts/ccxt.ts @@ -47,6 +47,7 @@ const version = '4.4.34'; //----------------------------------------------------------------------------- import ace from './src/ace.js' +import alephx from './src/alephx.js' import alpaca from './src/alpaca.js' import ascendex from './src/ascendex.js' import bequant from './src/bequant.js' @@ -159,6 +160,7 @@ import zonda from './src/zonda.js' // pro exchanges +import alephxPro from './src/pro/alephx.js' import alpacaPro from './src/pro/alpaca.js' import ascendexPro from './src/pro/ascendex.js' import bequantPro from './src/pro/bequant.js' @@ -235,6 +237,7 @@ import xtPro from './src/pro/xt.js' const exchanges = { 'ace': ace, + 'alephx': alephx, 'alpaca': alpaca, 'ascendex': ascendex, 'bequant': bequant, @@ -347,6 +350,7 @@ const exchanges = { } const pro = { + 'alephx': alephxPro, 'alpaca': alpacaPro, 'ascendex': ascendexPro, 'bequant': bequantPro, @@ -562,6 +566,7 @@ export { CrossBorrowRates, LeverageTiers, ace, + alephx, alpaca, ascendex, bequant, diff --git a/ts/src/abstract/alephx.ts b/ts/src/abstract/alephx.ts new file mode 100644 index 0000000000000..420c62113c738 --- /dev/null +++ b/ts/src/abstract/alephx.ts @@ -0,0 +1,22 @@ +// ------------------------------------------------------------------------------- + +// PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +// https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +// ------------------------------------------------------------------------------- + +import { implicitReturnType } from '../base/types.js'; +import { Exchange as _Exchange } from '../base/Exchange.js'; + +interface Exchange { + v1PublicGetSystemStatus (params?: {}): Promise; + v1PrivateGetAssetsBalances (params?: {}): Promise; + v1PrivateGetOrders (params?: {}): Promise; + v1PrivateGetOrdersId (params?: {}): Promise; + v1PrivateGetTrades (params?: {}): Promise; + v1PrivatePostOrders (params?: {}): Promise; + v1PrivatePatchOrdersIdCancel (params?: {}): Promise; +} +abstract class Exchange extends _Exchange {} + +export default Exchange diff --git a/ts/src/alephx.ts b/ts/src/alephx.ts new file mode 100644 index 0000000000000..c963b4a8d01b5 --- /dev/null +++ b/ts/src/alephx.ts @@ -0,0 +1,654 @@ + +// ---------------------------------------------------------------------------- + +import Exchange from './abstract/alephx.js'; +import { AuthenticationError, ExchangeError, BadRequest, OrderNotFound, PermissionDenied } from './base/errors.js'; +import { sha256 } from './static_dependencies/noble-hashes/sha256.js'; +import type { Balances, Int, OrderSide, OrderType, Order, Trade, Str, Market, Num, Dict, int } from './base/types.js'; + +// ---------------------------------------------------------------------------- + +/** + * @class alephx + * @augments Exchange + */ +export default class alephx extends Exchange { + describe () { + return this.deepExtend (super.describe (), { + 'id': 'alephx', + 'name': 'AlephX', + 'countries': [ 'CA' ], + 'pro': true, + 'certified': false, + // rate-limits: N/A + 'rateLimit': 1000, + 'version': 'v1', + 'userAgent': this.userAgents['chrome'], + // 'headers': { + // 'ZKX-VERSION': '2018-05-30', + // }, + 'has': { + 'CORS': true, + 'spot': true, + 'margin': false, + 'swap': false, + 'future': false, + 'option': false, + 'addMargin': false, + 'cancelOrder': true, + 'cancelOrders': false, + 'createDepositAddress': false, + 'createLimitBuyOrder': false, + 'createLimitSellOrder': false, + 'createMarketBuyOrder': false, + 'createMarketBuyOrderWithCost': false, + 'createMarketOrderWithCost': false, + 'createMarketSellOrder': false, + 'createMarketSellOrderWithCost': false, + 'createOrder': true, + 'createPostOnlyOrder': false, + 'createReduceOnlyOrder': false, + 'createStopLimitOrder': false, + 'createStopMarketOrder': false, + 'createStopOrder': true, + 'deposit': false, + 'editOrder': false, + 'fetchAccounts': false, + 'fetchBalance': true, + 'fetchBidsAsks': false, + 'fetchCanceledOrders': false, + 'fetchCurrencies': false, + 'fetchDeposit': false, + 'fetchDepositAddress': false, + 'fetchDepositAddresses': false, + 'fetchDepositAddressesByNetwork': false, + 'fetchDeposits': false, + 'fetchFundingHistory': false, + 'fetchFundingRate': false, + 'fetchFundingRateHistory': false, + 'fetchFundingRates': false, + 'fetchIndexOHLCV': false, + 'fetchIsolatedBorrowRate': false, + 'fetchIsolatedBorrowRates': false, + 'fetchL2OrderBook': false, + 'fetchLedger': false, + 'fetchLeverage': false, + 'fetchLeverageTiers': false, + 'fetchMarginMode': false, + 'fetchMarkets': false, + 'fetchMarkOHLCV': false, + 'fetchMyBuys': false, + 'fetchMySells': false, + 'fetchMyTrades': true, + 'fetchOHLCV': false, + 'fetchOpenInterestHistory': false, + 'fetchOpenOrders': false, + 'fetchOrder': true, + 'fetchOrderBook': false, + 'fetchOrders': true, + 'fetchOrderTrades': true, + 'fetchPosition': false, + 'fetchPositionMode': false, + 'fetchPositions': false, + 'fetchPositionsRisk': false, + 'fetchPremiumIndexOHLCV': false, + 'fetchStatus': true, + 'fetchTicker': false, + 'fetchTickers': false, + 'fetchTime': false, + 'fetchTrades': false, + 'fetchTradingFee': 'emulated', + 'fetchTradingFees': false, + 'fetchWithdrawals': false, + 'reduceMargin': false, + 'sandbox': true, + 'setLeverage': false, + 'setMarginMode': false, + 'setPositionMode': false, + 'withdraw': false, + }, + 'urls': { + // 'logo': 'https://user-images.githubusercontent.com/1294454/40811661-b6eceae2-653a-11e8-829e-10bfadb078cf.jpg', + 'test': { + 'rest': 'https://api-testnet.alephx.xyz', + }, + 'api': { + 'rest': 'https://api.alephx.xyz', + }, + 'www': 'https://demo.alephx.xyz', + // 'doc': [ + // 'https://developers.alephx.com/api/v1', + // ], + // 'fees': [ + // 'https://support.alephx.com/customer/portal/articles/2109597-buy-sell-bank-transfer-fees', + // ], + }, + 'requiredCredentials': { + 'apiKey': true, + 'secret': true, + }, + 'api': { + 'v1': { + 'public': { + 'get': { + 'system/status': 0, + }, + }, + 'private': { + 'get': { + 'assets/balances': 0, + 'orders': 0, + 'orders/{id}': 0, + 'trades': 0, + }, + 'post': { + 'orders': 0, + }, + 'patch': { + 'orders/{id}/cancel': 0, + }, + }, + }, + }, + 'exceptions': { + 'exact': {}, + 'broad': { + 'Wallet not allowed': AuthenticationError, + 'Invalid signature': AuthenticationError, + 'Unauthorized': PermissionDenied, + 'Order is not cancellable': BadRequest, + 'Asset is not supported': BadRequest, + 'Not Found': OrderNotFound, + }, + }, + }); + } + + async createOrder (symbol: string, type: OrderType, side: OrderSide, amount: number, price: Num = undefined, params = {}) { + /** + * @method + * @name alephx#createOrder + * @description create an order + * @see POST https://api.alephx.xyz/api/v1/orders + * @param {string} symbol unified symbol of the market to create an order in + * @param {string} type 'market' or 'limit' + * @param {string} side 'buy' or 'sell' + * @param {float} amount how much you want to trade in units of the base currency, quote currency for 'market' 'buy' orders + * @param {float} [price] the price to fulfill the order, in units of the quote currency, ignored in market orders + * @param {object} [params] extra parameters specific to the exchange API endpoint + * @param {string} [params.timeInForce] 'gtc' + * @param {string} [params.idempotencyKey] uuid for idempotency key + * @returns {object} an [order structure]{@link https://docs.ccxt.com/#/?id=order-structure} + */ + const request: Dict = { + 'symbol': symbol, + 'type': type, + 'side': side, + 'quantity': amount.toString (), + 'price': price.toString (), + 'time_in_force': this.safeString2 (params, 'timeInForce', 'gtc'), + 'idempotency_key': this.safeString2 (params, 'idempotencyKey', this.uuid ()), + }; + const response = await this.v1PrivatePostOrders (request); + // + // successful order + // + // + // failed order + // + // + const errorResponse = this.safeDict (response, 'error'); + if (errorResponse !== undefined) { + const errorReason = this.safeString (errorResponse, 'reason'); + const errorMessage = this.safeString (errorResponse, 'message'); + throw new ExchangeError (errorReason + '' + errorMessage); + } + return this.parseOrder (response); + } + + parseOrder (order: Dict, market: Market = undefined): Order { + // + // createOrder + // + // { + // "order_id": "52cfe5e2-0b29-4c19-a245-a6a773de5030", + // "status": "pending_new" + // } + // + // + // fetchOrder, fetchOrders, cancelOrder + // + // { + // "id": "0da4eb8d-c108-4e6c-8c45-0b42fabd3a72", + // "status": "partially_filled", + // "type": "limit", + // "symbol": "CLEO-ALEO", + // "account_id": "cb77b9ab-f94d-4013-85b7-644b0b9ba9a9", + // "settled_quantity": "0", + // "base_quantity": "0.1", + // "filled_quantity": "0.04", + // "side": "buy", + // "price": "12.3", + // "remained_quantity": "0.06", + // "idempotency_key": "99888999-93ef-9831-9829-120a082bfcf2", + // "inserted_at": "2024-09-16T23:47:45.161888Z", + // "fee_asset":null, + // "filled_at": "2024-09-26T20:08:11.350542Z", + // "average_filled_price": "12.3", + // "canceled_at":null,"cumulative_fee": "0", + // "time_in_force": "gtc", + // "internal_status": "partially_filled" + // } + // + const createdDateTime = this.safeString (order, 'inserted_at'); + const filledDateTime = this.safeString (order, 'filled_at'); + return this.safeOrder ({ + 'info': order, + 'id': this.safeString (order, 'order_id') || this.safeString (order, 'id'), + 'clientOrderId': this.safeString (order, 'idempotency_key'), + 'timestamp': createdDateTime ? this.parse8601 (createdDateTime) : undefined, + 'datetime': createdDateTime, + 'lastTradeTimestamp': filledDateTime ? this.parse8601 (filledDateTime) : undefined, + 'symbol': this.safeString (order, 'symbol'), + 'type': this.safeString (order, 'type'), + 'timeInForce': this.safeString (order, 'time_in_force', 'gtc'), + 'postOnly': true, + 'side': this.safeStringLower (order, 'side'), + 'price': this.safeString (order, 'price'), + 'stopPrice': undefined, + 'triggerPrice': undefined, + 'amount': this.safeString (order, 'base_quantity'), + 'filled': this.safeString (order, 'filled_quantity'), + 'remaining': this.safeString (order, 'remained_quantity'), + 'cost': undefined, + 'average': this.safeString (order, 'average_filled_price'), + 'status': this.safeString (order, 'status'), + 'fee': { + 'cost': this.safeString (order, 'cumulative_fee'), + 'currency': this.safeString (order, 'fee_asset'), + }, + 'trades': undefined, + }, market); + } + + async cancelOrder (id: string, symbol: Str = undefined, params = {}) { + /** + * @method + * @name alephx#cancelOrder + * @description cancels an open order + * @see PATCH https://api.alephx.xyz/api/v1/orders/{order_id}/cancel + * @param {string} id order id + * @param {string} symbol not used by alephx cancelOrder() + * @param {object} [params] extra parameters specific to the exchange API endpoint + * @returns {object} An [order structure]{@link https://docs.ccxt.com/#/?id=order-structure} + */ + const request: Dict = { + 'id': id, // order id + }; + const response = await this.v1PrivatePatchOrdersIdCancel (this.extend (request, params)); + const errorResponse = this.safeDict (response, 'error'); + if (errorResponse !== undefined) { + const errorReason = this.safeString (errorResponse, 'reason'); + const errorMessage = this.safeString (errorResponse, 'message'); + if (errorReason === 'Not Found') { + throw new OrderNotFound (this.id + ' cancelOrder() error ' + errorReason); + } else if (errorReason === 'Bad Request') { + throw new BadRequest (this.id + ' cancelOrder() error ' + errorReason + ' ' + errorMessage); + } else { + throw new ExchangeError (errorReason + '' + errorMessage); + } + } + return this.parseOrder (response); + } + + async fetchOrder (id: string, symbol: Str = undefined, params = {}) { + /** + * @method + * @name alephx#fetchOrder + * @description fetches information on an order made by the user + * @see GET https://api.alephx.xyz/api/v1/orders/{order_id} + * @param {string} id the order id + * @param {string} symbol unified market symbol that the order was made in + * @param {object} [params] extra parameters specific to the exchange API endpoint + * @returns {object} An [order structure]{@link https://docs.ccxt.com/#/?id=order-structure} + */ + const request: Dict = { + 'id': id, // order id + }; + const response = await this.v1PrivateGetOrdersId (this.extend (request, params)); + const errorResponse = this.safeDict (response, 'error'); + if (errorResponse !== undefined) { + const errorReason = this.safeString (errorResponse, 'reason'); + if (errorReason === 'Not Found') { + throw new OrderNotFound (this.id + ' fetchOrder() error ' + errorReason); + } + } + return this.parseOrder (response); + } + + async fetchOrders (symbol: Str = undefined, since: Int = undefined, limit: Int = 100, params = {}): Promise { + /** + * @method + * @name alephx#fetchOrders + * @description fetches information on multiple orders made by the user + * @see GET https://api.alephx.xyz/api/v1/orders/ + * @param {string} symbol unified market symbol that the orders were made in + * @param {int} [since] the earliest time in ms to fetch orders + * @param {int} [limit] the maximum number of order structures to retrieve + * @param {object} [params] extra parameters specific to the exchange API endpoint + * @param {int} [params.until] the latest time in ms to fetch trades for + * @param {boolean} [params.paginate] default false, when true will automatically paginate by calling this endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + * @returns {Order[]} a list of [order structures]{@link https://docs.ccxt.com/#/?id=order-structure} + */ + const response = await this.v1PrivateGetOrders (); + const market = undefined; + return this.parseOrders (response, market, since, limit); + } + + async fetchMyTrades (symbol: Str = undefined, since: Int = undefined, limit: Int = undefined, params = {}) { + /** + * @method + * @name alephx#fetchMyTrades + * @description fetch all trades made by the user + * @see GET https://api.alephx.xyz/api/v1/trades + * @param {string} symbol unified market symbol of the trades + * @param {int} [since] timestamp in ms of the earliest order, default is undefined + * @param {int} [limit] the maximum number of trade structures to fetch + * @param {object} [params] extra parameters specific to the exchange API endpoint + * @param {int} [params.until] the latest time in ms to fetch trades for + * @param {boolean} [params.paginate] default false, when true will automatically paginate by calling this endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + * @returns {Trade[]} a list of [trade structures]{@link https://docs.ccxt.com/#/?id=trade-structure} + */ + const response = await this.v1PrivateGetTrades (); + const trades = this.safeList (response, 'data'); + const market = undefined; + // + // { "data": [ + // { "id": "32672029-b46b-4139-9779-95444053f40a", + // "status": "unsettled", + // "symbol": "CLEO-ALEO", + // "base_quantity": "0.01", + // "side": "buy", + // "price": "12.3", + // "buy_order_id": "0da4eb8d-c108-4e6c-8c45-0b42fabd3a72", + // "sell_order_id": "86c61562-ff14-43c9-9a03-4be804d184d0", + // "quote_quantity": "0.123", + // "inserted_at": "2024-09-26T15:18:06.603489Z", + // "aggressor_side": "sell", + // "fee": null, + // "fee_asset": null, + // "updated_at": "2024-09-26T15:18:06.603489Z" + // }]} + // + return this.parseTrades (trades, market, since, limit); + } + + parseTrade (trade: Dict, market: Market = undefined): Trade { + // returned trade + // + // [ + // { + // id: '32672029-b46b-4139-9779-95444053f40a', + // order: '0da4eb8d-c108-4e6c-8c45-0b42fabd3a72', + // info: { + // id: '32672029-b46b-4139-9779-95444053f40a', + // status: 'unsettled', + // symbol: 'CLEO-ALEO', + // base_quantity: '0.01', + // side: 'buy', + // price: '12.3', + // buy_order_id: '0da4eb8d-c108-4e6c-8c45-0b42fabd3a72', + // sell_order_id: '86c61562-ff14-43c9-9a03-4be804d184d0', + // quote_quantity: '0.123', + // inserted_at: '2024-09-26T15:18:06.603489Z', + // aggressor_side: 'sell', + // fee: null, + // fee_asset: null, + // updated_at: '2024-09-26T15:18:06.603489Z' + // }, + // timestamp: 1727363886603, + // datetime: '2024-09-26T15:18:06.603489Z', + // symbol: 'CLEO-ALEO', + // type: undefined, + // side: 'buy', + // takerOrMaker: undefined, + // price: 12.3, + // amount: 0.01, + // cost: 0.123, + // fee: { cost: undefined, currency: undefined }, + // fees: [] + // } + // ] + const createdDateTime = this.safeString (trade, 'inserted_at'); + const traderSide = this.safeString (trade, 'side'); + const traderOrderId = (traderSide === 'buy') ? this.safeString (trade, 'buy_order_id') : this.safeString (trade, 'sell_order_id'); + return this.safeTrade ({ + 'id': this.safeString (trade, 'id'), + 'order': traderOrderId, + 'info': trade, + 'timestamp': this.parse8601 (createdDateTime), + 'datetime': createdDateTime, + 'symbol': this.safeString (trade, 'symbol'), + 'type': 'gtc', + 'side': traderSide, + 'takerOrMaker': undefined, + 'price': this.safeString (trade, 'price'), + 'amount': this.safeString (trade, 'base_quantity'), + 'cost': this.safeString (trade, 'quote_quantity'), + 'fee': { + 'cost': this.safeString (trade, 'fee'), + 'currency': this.safeString (trade, 'fee_asset'), + }, + }, market); + } + + async fetchOrderTrades (id: string, symbol: Str = undefined, since: Int = undefined, limit: Int = undefined, params = {}) { + /** + * @method + * @name alephx#fetchOrderTrades + * @description fetch all the trades made from a single order + * @see https://api.alephx.xyz/api/v1/trades?filters=[{"field":"order_id","op":"==","value":"order_id"}] + * @param {string} id order id + * @param {string} symbol unified market symbol + * @param {int} [since] the earliest time in ms to fetch trades for + * @param {int} [limit] the maximum number of trades to retrieve + * @param {object} [params] extra parameters specific to the exchange API endpoint + * @returns {object[]} a list of [trade structures]{@link https://docs.ccxt.com/#/?id=trade-structure} + */ + const filters = []; + const filter: Dict = { + 'field': 'order_id', + 'op': '==', + 'value': id, + }; + filters.push (filter); + const request: Dict = { + 'filters': JSON.stringify (filters), + }; + const response = await this.v1PrivateGetTrades (request); + const trades = this.safeList (response, 'data'); + const market = undefined; + // + // { "data": [ + // { "id": "32672029-b46b-4139-9779-95444053f40a", + // "status": "unsettled", + // "symbol": "CLEO-ALEO", + // "base_quantity": "0.01", + // "side": "buy", + // "price": "12.3", + // "buy_order_id": "0da4eb8d-c108-4e6c-8c45-0b42fabd3a72", + // "sell_order_id": "86c61562-ff14-43c9-9a03-4be804d184d0", + // "quote_quantity": "0.123", + // "inserted_at": "2024-09-26T15:18:06.603489Z", + // "aggressor_side": "sell", + // "fee": null, + // "fee_asset": null, + // "updated_at": "2024-09-26T15:18:06.603489Z" + // }]} + // + return this.parseTrades (trades, market, since, limit); + } + + async fetchStatus (params = {}) { + /** + * @method + * @name alephx#fetchStatus + * @description the latest known information on the availability of the exchange API + * @see https://api.alephx.xyz/api/v1/system/status + * @param {object} [params] extra parameters specific to the exchange API endpoint + * @returns {object} a [status structure]{@link https://docs.ccxt.com/#/?id=exchange-status-structure} + */ + const response = await this.v1PublicGetSystemStatus (params); + // + // OK + // + return { + 'status': (response === 'OK') ? 'ok' : 'maintenance', + 'updated': undefined, + 'eta': undefined, + 'url': undefined, + 'info': response, + }; + } + + async fetchBalance (params = {}): Promise { + /** + * @method + * @name alephxn#fetchBalance + * @description query for balance and get the amount of funds available for trading or funds locked in orders + * @see https://api.alephx.xyz/api/v1/assets/balances + * @param {object} [params] extra parameters specific to the exchange API endpoint + * @returns {object} a [balance structure]{@link https://docs.ccxt.com/#/?id=balance-structure} + */ + // await this.loadMarkets (); + const response = await this.v1PrivateGetAssetsBalances (params); + // [ + // { + // "total": "19.996900", + // "available": "14.756900", + // "asset": "CLEO", + // "locked": "5.240000" + // }, + // { + // "total": "10.054720", + // "available": "-52.145280", + // "asset": "ALEO", + // "locked": "62.200000" + // } + // ] + return this.parseBalance (response); + } + + parseBalance (response): Balances { + const balances = this.toArray (response); + const result: Dict = { + 'info': response, + 'timestamp': undefined, + 'datetime': undefined, + }; + for (let i = 0; i < balances.length; i++) { + const balance = balances[i]; + const code = this.safeString (balance, 'asset'); + const account = this.account (); + account['free'] = this.safeString (balance, 'available'); + account['used'] = this.safeString (balance, 'locked'); + account['total'] = this.safeString (balance, 'total'); + result[code] = account; + } + return this.safeBalance (result); + } + + sign (path, api = [], method = 'GET', params = {}, headers = undefined, body = undefined) { + const version = api[0]; + const signed = api[1] === 'private'; + let fullPath = '/api/' + version + '/' + this.implodeParams (path, params); + const query = this.omit (params, this.extractParams (path)); + const savedPath = fullPath; + if (method === 'GET') { + if (Object.keys (query).length) { + fullPath += '?' + this.urlencodeWithArrayRepeat (query); + } + } + const url = this.urls['api']['rest'] + fullPath; + if (signed) { + const authorization = this.safeString (this.headers, 'Authorization'); + let authorizationString = undefined; + if (authorization !== undefined) { + authorizationString = authorization; + } else if (this.token && !this.checkRequiredCredentials (false)) { + authorizationString = 'Bearer ' + this.token; + } else { + this.checkRequiredCredentials (); + if (method !== 'GET') { + if (Object.keys (query).length) { + body = this.json (query); + } + } + // doesn't need payload in the signature. inside url is enough + const timestampString = this.seconds ().toString (); + const auth = timestampString + method + savedPath; + const signature = this.hmac (this.encode (auth), this.encode (this.secret), sha256); + headers = { + 'ZKX_ACCESS_KEY': this.apiKey, + 'ZKX_ACCESS_SIGN': signature, + 'ZKX_ACCESS_TIMESTAMP': timestampString, + 'Content-Type': 'application/json', + }; + } + if (authorizationString !== undefined) { + headers = { + 'Authorization': authorizationString, + 'Content-Type': 'application/json', + }; + if (method !== 'GET') { + if (Object.keys (query).length) { + body = this.json (query); + } + } + } + } + return { 'url': url, 'method': method, 'body': body, 'headers': headers }; + } + + handleErrors (code: int, reason: string, url: string, method: string, headers: Dict, body: string, response, requestHeaders, requestBody) { + if (response === undefined) { + return undefined; // fallback to default error handler + } + const feedback = this.id + ' ' + body; + // + // + // { + // "error": { + // { + // "reason": "Bad Request", + // "message": "Order is not cancellable" + // } + // } + // } + // + const errorResponse = this.safeDict (response, 'error'); + const errorCode = this.safeString (errorResponse, 'reason'); + if (errorCode !== undefined) { + const errorMessage = this.safeString (errorResponse, 'message'); + this.throwExactlyMatchedException (this.exceptions['exact'], errorCode, feedback); + this.throwBroadlyMatchedException (this.exceptions['broad'], errorMessage, feedback); + throw new ExchangeError (feedback); + } + // const errors = this.safeList(response, 'errors'); + // if (errors !== undefined) { + // if (Array.isArray(errors)) { + // const numErrors = errors.length; + // if (numErrors > 0) { + // errorCode = this.safeString (errors[0], 'id'); + // const errorMessage = this.safeString (errors[0], 'message'); + // if (errorCode !== undefined) { + // this.throwExactlyMatchedException(this.exceptions['exact'], errorCode, feedback); + // this.throwBroadlyMatchedException(this.exceptions['broad'], errorMessage, feedback); + // throw new ExchangeError(feedback); + // } + // } + // } + // } + return undefined; + } +} diff --git a/ts/src/pro/alephx.ts b/ts/src/pro/alephx.ts new file mode 100644 index 0000000000000..2cdf58cadc32a --- /dev/null +++ b/ts/src/pro/alephx.ts @@ -0,0 +1,374 @@ +// --------------------------------------------------------------------------- + +import alephxRest from '../alephx.js'; +import { ExchangeError } from '../base/errors.js'; +import { ArrayCacheBySymbolById } from '../base/ws/Cache.js'; +import { sha256 } from '../static_dependencies/noble-hashes/sha256.js'; +import { Int, Trade, Order, Str, Dict } from '../base/types.js'; + +// --------------------------------------------------------------------------- + +export default class alephx extends alephxRest { + describe () { + return this.deepExtend (super.describe (), { + 'has': { + 'ws': true, + 'cancelAllOrdersWs': false, + 'cancelOrdersWs': false, + 'cancelOrderWs': false, + 'createOrderWs': false, + 'editOrderWs': false, + 'fetchBalanceWs': false, + 'fetchOpenOrdersWs': false, + 'fetchOrderWs': false, + 'fetchTradesWs': false, + 'watchBalance': false, + 'watchMyTrades': true, + 'watchOHLCV': false, + 'watchOrderBook': false, + 'watchOrderBookForSymbols': false, + 'watchOrders': true, + 'watchTicker': false, + 'watchTickers': false, + 'watchTrades': false, + 'watchTradesForSymbols': false, + }, + 'urls': { + 'test': { + 'ws': 'wss://api-testnet.alephx.xyz/websocket', + }, + 'api': { + 'ws': 'wss://api.alephx.xyz/websocket', + }, + }, + 'options': { + 'tradesLimit': 1000, + 'ordersLimit': 1000, + 'myTradesLimit': 1000, + 'sides': { + 'bid': 'bids', + 'offer': 'asks', + }, + }, + }); + } + + async subscribe (name: string, isPrivate: boolean, symbol = undefined, params = {}) { + /** + * @ignore + * @method + * @description subscribes to a websocket channel + * @see https://api.alephx.xyz/websocket + * @param {string} name the name of the channel + * @param {string|string[]} [symbol] unified market symbol + * @param {object} [params] extra parameters specific to the exchange API endpoint + * @returns {object} subscription to a websocket channel + */ + let url = this.urls['api']['ws']; + let messageHash = name; + if (isPrivate) { + const auth = this.createWSAuth (); + url = url + '?api_key=' + auth['api_key'] + '×tamp=' + auth['timestamp'] + '&signature=' + auth['signature']; + messageHash = messageHash + ':' + auth['api_key']; + } + const subscribe = { + 'event': 'phx_join', + 'topic': messageHash, + 'payload': {}, + 'ref': messageHash, + 'join_ref': messageHash, + }; + return await this.watch (url, messageHash, subscribe, messageHash); + } + + createWSAuth () { + const subscribe: Dict = {}; + const timestamp = this.numberToString (this.seconds ()); + this.checkRequiredCredentials (); + const auth = timestamp; + subscribe['api_key'] = this.apiKey; + subscribe['timestamp'] = timestamp; + subscribe['signature'] = this.hmac (this.encode (auth), this.encode (this.secret), sha256); + return subscribe; + } + + async watchMyTrades (symbol: string = undefined, since: Int = undefined, limit: Int = undefined, params = {}): Promise { + /** + * @method + * @name alephx#watchMyTrades + * @description watches information on multiple trades made by the user + * @see trades channel + * @param {string} symbol unified symbol of the market to fetch trades for + * @param {int} [since] timestamp in ms of the earliest trade to fetch + * @param {int} [limit] the maximum amount of trades to fetch + * @param {object} [params] extra parameters specific to the exchange API endpoint + * @returns {object[]} a list of [trade structures]{@link https://docs.ccxt.com/#/?id=trade-structure} + */ + const name = 'trades'; + const trades = await this.subscribe (name, true, symbol, params); + if (this.newUpdates) { + limit = trades.getLimit (symbol, limit); + } + return this.filterBySinceLimit (trades, since, limit, 'timestamp', true); + } + + async watchOrders (symbol: Str = undefined, since: Int = undefined, limit: Int = undefined, params = {}): Promise { + /** + * @method + * @name alephx#watchOrders + * @description watches information on multiple orders made by the user + * @see orders channel + * @param {string} [symbol] unified market symbol of the market orders were made in + * @param {int} [since] the earliest time in ms to fetch orders for + * @param {int} [limit] the maximum number of order structures to retrieve + * @param {object} [params] extra parameters specific to the exchange API endpoint + * @returns {object[]} a list of [order structures]{@link https://docs.ccxt.com/#/?id=order-structure} + */ + const name = 'orders'; + const orders = await this.subscribe (name, true, symbol, params); + if (this.newUpdates) { + limit = orders.getLimit (symbol, limit); + } + return this.filterBySinceLimit (orders, since, limit, 'timestamp', true); + } + + handleTrade (client, message) { + // { + // ref: null, + // payload: { + // timestamp: '2024-10-04T03:11:30.111216Z', + // channel: 'trades', + // trade: { + // symbol: 'CLEO-ALEO', + // price: '1.1', + // base_quantity: '0.1', + // quote_quantity: '0.11', + // buy_order_id: 'ad2066e6-a47c-449d-99be-79ac82e7d163', + // sell_order_id: '1676786b-145f-4dcf-adde-74e5cce9ebc3', + // status: 'unsettled', + // aggressor_side: 'sell', + // id: 'e0b8354a-d71a-4577-bee5-ce52d8fabcf5', + // fee: null, + // fee_asset: null, + // } + // }, + // topic: 'trades:cb77b9ab-f94d-4013-85b7-644b0b9ba9a9', + // event: 'trades' + // } + if (this.myTrades === undefined) { + const limit = this.safeInteger (this.options, 'tradesLimit', 1000); + this.myTrades = new ArrayCacheBySymbolById (limit); + } + const payload = this.safeDict (message, 'payload'); + const trade = this.safeDict (payload, 'trade'); + const parsed = this.parseWsTrade (trade); + this.myTrades.append (parsed); + const messageHash = this.safeString (message, 'topic'); + client.resolve (this.myTrades, messageHash); + return message; + } + + parseWsTrade (trade, market = undefined) { + // { + // symbol: 'CLEO-ALEO', + // price: '1.1', + // base_quantity: '0.1', + // quote_quantity: '0.11', + // buy_order_id: 'ad2066e6-a47c-449d-99be-79ac82e7d163', + // sell_order_id: '1676786b-145f-4dcf-adde-74e5cce9ebc3', + // status: 'unsettled', + // aggressor_side: 'sell', + // id: 'e0b8354a-d71a-4577-bee5-ce52d8fabcf5', + // fee: null, + // fee_asset: null + // } + const createdDateTime = this.safeString (trade, 'inserted_at'); + const traderSide = this.safeString (trade, 'side'); + const traderOrderId = (traderSide === 'buy') ? this.safeString (trade, 'buy_order_id') : this.safeString (trade, 'sell_order_id'); + return this.safeTrade ({ + 'id': this.safeString (trade, 'id'), + 'order': traderOrderId, + 'info': trade, + 'timestamp': this.parse8601 (createdDateTime), + 'datetime': createdDateTime, + 'symbol': this.safeString (trade, 'symbol'), + 'type': 'gtc', + 'side': traderSide, + 'takerOrMaker': undefined, + 'price': this.safeString (trade, 'price'), + 'amount': this.safeString (trade, 'base_quantity'), + 'cost': this.safeString (trade, 'quote_quantity'), + 'fee': { + 'cost': this.safeString (trade, 'fee'), + 'currency': this.safeString (trade, 'fee_asset'), + }, + }, market); + } + + handleOrder (client, message) { + // { + // ref: null, + // payload: { + // timestamp: '2024-10-04T02:29:36.263148Z', + // channel: 'orders', + // order: { + // id: 'eed7ce96-f34b-483d-8d87-925eef6f0702', + // status: 'new', + // type: 'limit', + // symbol: 'CLEO-ALEO', + // inserted_at: '2024-10-04T02:29:35.693172Z', + // account_id: 'cb77b9ab-f94d-4013-85b7-644b0b9ba9a9', + // updated_at: '2024-10-04T02:29:36.254349Z', + // filled_quantity: '0', + // base_quantity: '0.1', + // idempotency_key: '99888999-93ef-9831-9829-820a082bfcf8', + // price: '1.1', + // remained_quantity: '0.1', + // side: 'buy', + // time_in_force: 'gtc', + // canceled_at: null, + // average_filled_price: null, + // canceled_quantity: '0', + // cumulative_fee: '0', + // fee_asset: null, + // filled_at: null, + // filled_value: '0', + // lock_version: 3, + // quote_quantity: null, + // sequence_id: 187, + // settled_quantity: '0' + // } + // }, + // topic: 'orders:cb77b9ab-f94d-4013-85b7-644b0b9ba9a9', + // event: 'orders' + // } + if (this.orders === undefined) { + const limit = this.safeInteger (this.options, 'ordersLimit', 1000); + this.orders = new ArrayCacheBySymbolById (limit); + } + const payload = this.safeDict (message, 'payload'); + const order = this.safeDict (payload, 'order'); + const parsed = this.parseWsOrder (order); + this.orders.append (parsed); + const messageHash = this.safeString (message, 'topic'); + client.resolve (this.orders, messageHash); + return message; + } + + parseWsOrder (order, market = undefined) { + // { + // id: 'eed7ce96-f34b-483d-8d87-925eef6f0702', + // status: 'new', + // type: 'limit', + // symbol: 'CLEO-ALEO', + // inserted_at: '2024-10-04T02:29:35.693172Z', + // account_id: 'cb77b9ab-f94d-4013-85b7-644b0b9ba9a9', + // updated_at: '2024-10-04T02:29:36.254349Z', + // filled_quantity: '0', + // base_quantity: '0.1', + // idempotency_key: '99888999-93ef-9831-9829-820a082bfcf8', + // price: '1.1', + // remained_quantity: '0.1', + // side: 'buy', + // time_in_force: 'gtc', + // canceled_at: null, + // average_filled_price: null, + // canceled_quantity: '0', + // cumulative_fee: '0', + // fee_asset: null, + // filled_at: null, + // filled_value: '0', + // lock_version: 3, + // quote_quantity: null, + // sequence_id: 187, + // settled_quantity: '0' + // } + const id = this.safeString (order, 'id'); + const clientOrderId = this.safeString (order, 'idempotency_key'); + const createdDateTime = this.safeString (order, 'inserted_at'); + const filledDateTime = this.safeString (order, 'filled_at'); + const updatedDateTime = this.safeString (order, 'updated_at'); + return this.safeOrder ({ + 'info': order, + 'symbol': this.safeString (order, 'symbol'), + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': this.parse8601 (createdDateTime), + 'datetime': createdDateTime, + 'lastTradeTimestamp': filledDateTime ? this.parse8601 (filledDateTime) : undefined, + 'type': this.safeString (order, 'type'), + 'timeInForce': this.safeString (order, 'time_in_force', 'gtc'), + 'postOnly': true, + 'side': this.safeString (order, 'side'), + 'price': this.safeString (order, 'price'), + 'stopPrice': undefined, + 'triggerPrice': undefined, + 'amount': this.safeString (order, 'base_quantity'), + 'cost': undefined, + 'average': this.safeString (order, 'average_filled_price'), + 'filled': this.safeString (order, 'filled_quantity'), + 'remaining': this.safeString (order, 'remained_quantity'), + 'status': this.safeStringLower (order, 'status'), + 'fee': { + 'cost': this.safeString (order, 'cumulative_fee'), + 'currency': this.safeString (market, 'fee_asset'), + }, + 'trades': undefined, + 'lastUpdatedTimestamp': updatedDateTime ? this.parse8601 (updatedDateTime) : undefined, + }); + } + + handleSubscriptionStatus (client, message) { + // + // { + // "type": "subscriptions", + // "channels": [ + // { + // "name": "level2", + // "product_ids": [ "ETH-BTC" ] + // } + // ] + // } + // + return message; + } + + handleHeartbeats (client, message) { + // although the subscription takes a product_ids parameter (i.e. symbol), + // there is no (clear) way of mapping the message back to the symbol. + // + // { + // "channel": "heartbeats", + // "client_id": "", + // "timestamp": "2023-06-23T20:31:26.122969572Z", + // "sequence_num": 0, + // "events": [ + // { + // "current_time": "2023-06-23 20:31:56.121961769 +0000 UTC m=+91717.525857105", + // "heartbeat_counter": "3049" + // } + // ] + // } + // + return message; + } + + handleMessage (client, message) { + const channel = this.safeString (this.safeDict (message, 'payload'), 'channel'); + const methods: Dict = { + // 'subscriptions': this.handleSubscriptionStatus, + 'trades': this.handleTrade, + 'orders': this.handleOrder, + // 'heartbeats': this.handleHeartbeats, + }; + const type = this.safeString (message, 'type'); + if (type === 'error') { + const errorMessage = this.safeString (message, 'message'); + throw new ExchangeError (errorMessage); + } + const method = this.safeValue (methods, channel); + if (method) { + method.call (this, client, message); + } + } +}