Files
ccxt_with_mt5/ccxt/pro/arkham.py
lz_db 0fab423a18 add
2025-11-16 12:31:03 +08:00

687 lines
27 KiB
Python

# -*- 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 ArrayCache, ArrayCacheBySymbolById, ArrayCacheBySymbolBySide, ArrayCacheByTimestamp
import hashlib
from ccxt.base.types import Any, Balances, Bool, Int, Order, OrderBook, Position, Str, Strings, Ticker, Trade
from ccxt.async_support.base.ws.client import Client
from typing import List
from ccxt.base.errors import ExchangeError
class arkham(ccxt.async_support.arkham):
def describe(self) -> Any:
return self.deep_extend(super(arkham, self).describe(), {
'has': {
'ws': True,
'watchTrades': True,
'watchTradesForSymbols': False,
'watchOrderBook': True,
'watchOrderBookForSymbols': False,
'watchOHLCV': True,
'watchOHLCVForSymbols': False,
'watchOrders': True,
'watchMyTrades': False,
'watchTicker': True,
'watchTickers': False,
'watchBalance': True,
},
'urls': {
'api': {
'ws': 'wss://arkm.com/ws',
},
},
'options': {
'watchOrderBook': {
'depth': 100, # 5, 10, 20, 50, 100
'interval': 500, # 100, 200, 500, 1000
},
},
'streaming': {
'keepAlive': 300000, # 5 minutes
},
})
def handle_message(self, client: Client, message):
#
# confirmation
#
# {channel: 'confirmations', confirmationId: 'myCustomId-123'}
if self.handle_error_message(client, message):
return
methods: dict = {
'ticker': self.handle_ticker,
'candles': self.handle_ohlcv,
'l2_updates': self.handle_order_book,
'trades': self.handle_trades,
'balances': self.handle_balance,
'positions': self.handle_positions,
'order_statuses': self.handle_order,
'trigger_orders': self.handle_order,
# 'confirmations': self.handle_ticker,
}
channel = self.safe_string(message, 'channel')
if channel == 'confirmations':
return
# type = self.safe_string(message, 'type')
# if type != 'update' and type != 'snapshot':
# debugger
# }
method = self.safe_value(methods, channel)
if method is not None:
method(client, message)
async def subscribe(self, messageHash: str, rawChannel: str, params: dict) -> Any:
subscriptionHash = messageHash
request: dict = {
'args': {
'channel': rawChannel,
'params': params,
},
'confirmationId': self.uuid(),
'method': 'subscribe',
}
return await self.watch(self.urls['api']['ws'], messageHash, request, subscriptionHash)
async def watch_ticker(self, symbol: str, params={}) -> Ticker:
"""
watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market
https://arkm.com/docs#stream/ticker
:param str symbol: unified symbol of the market to fetch the ticker for
:param dict [params]: extra parameters specific to the exchange API endpoint
:returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`
"""
await self.load_markets()
market = self.market(symbol)
requestArg = {
'symbol': market['id'],
}
messageHash = 'ticker::' + market['symbol']
return await self.subscribe(messageHash, 'ticker', self.extend(params, requestArg))
def handle_ticker(self, client: Client, message):
#
# {
# channel: 'ticker',
# type: 'update',
# data: {
# symbol: 'BTC_USDT',
# baseSymbol: 'BTC',
# quoteSymbol: 'USDT',
# price: '118962.74',
# price24hAgo: '118780.42',
# high24h: '120327.96',
# low24h: '118217.28',
# volume24h: '32.89729',
# quoteVolume24h: '3924438.7146048',
# markPrice: '0',
# indexPrice: '118963.080293501',
# fundingRate: '0',
# nextFundingRate: '0',
# nextFundingTime: 0,
# productType: 'spot',
# openInterest: '0',
# indexCurrency: 'USDT',
# usdVolume24h: '3924438.7146048',
# openInterestUSD: '0'
# }
# }
#
data = self.safe_dict(message, 'data', {})
marketId = self.safe_string(data, 'symbol')
market = self.safe_market(marketId, None)
symbol = market['symbol']
ticker = self.parse_ws_ticker(data, market)
self.tickers[symbol] = ticker
client.resolve(ticker, 'ticker::' + symbol)
# if self.safe_string(message, 'dataType') == 'all@ticker':
# client.resolve(ticker, self.getMessageHash('ticker'))
# }
def parse_ws_ticker(self, message, market=None):
# same dict api
return self.parse_ticker(message, market)
async def watch_ohlcv(self, symbol: str, timeframe='1m', since: Int = None, limit: Int = None, params={}) -> List[list]:
"""
watches historical candlestick data containing the open, high, low, and close price, and the volume of a market
https://arkm.com/docs#stream/candles
:param str symbol: unified symbol of the market to fetch OHLCV data for
:param str timeframe: the length of time each candle represents
:param int [since]: timestamp in ms of the earliest candle to fetch
:param int [limit]: the maximum amount of candles to fetch
:param dict [params]: extra parameters specific to the exchange API endpoint
:returns int[][]: A list of candles ordered, open, high, low, close, volume
"""
await self.load_markets()
market = self.market(symbol)
rawTimeframe = self.safe_string(self.timeframes, timeframe, timeframe)
requestArg = {
'symbol': market['id'],
'duration': rawTimeframe,
}
messageHash = 'ohlcv::' + market['symbol'] + '::' + rawTimeframe
result = await self.subscribe(messageHash, 'candles', self.extend(requestArg, params))
ohlcv = result
if self.newUpdates:
limit = ohlcv.getLimit(market['symbol'], limit)
return self.filter_by_since_limit(ohlcv, since, limit, 0, True)
def handle_ohlcv(self, client: Client, message):
#
# {
# channel: 'candles',
# type: 'update',
# data: {
# symbol: 'BTC_USDT',
# time: '1755076380000000',
# duration: 60000000,
# open: '120073.01',
# high: '120073.01',
# low: '120073.01',
# close: '120073.01',
# volume: '0',
# quoteVolume: '0'
# }
# }
#
data = self.safe_dict(message, 'data', {})
marketId = self.safe_string(data, 'symbol')
market = self.safe_market(marketId, None)
symbol = market['symbol']
duration = self.safe_integer(data, 'duration')
timeframe = self.findTimeframeByDuration(duration)
messageHash = 'ohlcv::' + symbol + '::' + timeframe
self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {})
if not (timeframe in self.ohlcvs[symbol]):
limit = self.handle_option('watchOHLCV', 'limit', 1000)
self.ohlcvs[symbol][timeframe] = ArrayCacheByTimestamp(limit)
stored = self.ohlcvs[symbol][timeframe]
parsed = self.parse_ws_ohlcv(data, market)
stored.append(parsed)
client.resolve(stored, messageHash)
return message
def parse_ws_ohlcv(self, ohlcv, market=None) -> list:
# same api
return self.parse_ohlcv(ohlcv, market)
async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook:
"""
watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data
https://arkm.com/docs#stream/l2_updates
:param str symbol: unified symbol of the market to fetch the order book for
:param int [limit]: the maximum amount of order book entries to return
:param dict [params]: extra parameters specific to the exchange API endpoint
:returns dict: A dictionary of `order book structures <https://docs.ccxt.com/#/?id=order-book-structure>` indexed by market symbols
"""
await self.load_markets()
market = self.market(symbol)
requestArg = {
'symbol': market['id'],
'snapshot': True,
}
messageHash = 'orderBook::' + market['symbol']
orderbook = await self.subscribe(messageHash, 'l2_updates', self.extend(requestArg, params))
return orderbook.limit()
def handle_order_book(self, client: Client, message):
#
# snapshot:
#
# {
# channel: 'l2_updates',
# type: 'snapshot',
# data: {
# symbol: 'BTC_USDT',
# group: '0.01',
# asks: [ [Object], [Object], ...],
# bids: [ [Object], [Object], ...],
# lastTime: 1755115180608299
# }
# }
#
# update:
#
# {
# channel: "l2_updates",
# type: "update",
# data: {
# symbol: "BTC_USDT",
# group: "0.01",
# side: "sell",
# size: "0.05295",
# price: "122722.76",
# revisionId: 2455511217,
# time: 1755115736475207,
# }
# }
#
data = self.safe_dict(message, 'data')
type = self.safe_string(message, 'type')
marketId = self.safe_string(data, 'symbol')
market = self.safe_market(marketId)
symbol = market['symbol']
messageHash = 'orderBook::' + symbol
if not (symbol in self.orderbooks):
ob = self.order_book({})
ob['symbol'] = symbol
self.orderbooks[symbol] = ob
orderbook = self.orderbooks[symbol]
if type == 'snapshot':
timestamp = self.safe_integer_product(data, 'lastTime', 0.001)
parsedOrderBook = self.parse_order_book(data, symbol, timestamp, 'bids', 'asks', 'price', 'size')
orderbook.reset(parsedOrderBook)
elif type == 'update':
timestamp = self.safe_integer_product(data, 'time', 0.001)
side = self.safe_string(data, 'side')
bookside = orderbook['bids'] if (side == 'buy') else orderbook['asks']
self.handle_delta(bookside, data)
orderbook['timestamp'] = timestamp
orderbook['datetime'] = self.iso8601(timestamp)
self.orderbooks[symbol] = orderbook
client.resolve(self.orderbooks[symbol], messageHash)
def handle_delta(self, bookside, delta):
bidAsk = self.parse_bid_ask(delta, 'price', 'size')
bookside.storeArray(bidAsk)
async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]:
"""
watches information on multiple trades made in a market
https://arkm.com/docs#stream/trades
:param str symbol: unified market symbol of the market trades were made in
:param int [since]: the earliest time in ms to fetch orders for
:param int [limit]: the maximum number of trade structures to retrieve
:param dict [params]: extra parameters specific to the exchange API endpoint
:returns dict[]: a list of `trade structures <https://docs.ccxt.com/#/?id=trade-structure>`
"""
await self.load_markets()
market = self.market(symbol)
requestArg = {
'symbol': market['id'],
}
messageHash = 'trade::' + market['symbol']
trades = await self.subscribe(messageHash, 'trades', self.extend(requestArg, params))
if self.newUpdates:
limit = trades.getLimit(market['symbol'], limit)
return self.filter_by_since_limit(trades, since, limit, 'timestamp', True)
def handle_trades(self, client: Client, message):
#
# {
# channel: 'trades',
# type: 'update',
# data: {
# symbol: 'BTC_USDT',
# revisionId: 2643896903,
# size: '0.00261',
# price: '118273.2',
# takerSide: 'buy',
# time: 1755200320146389
# }
# }
#
data = self.safe_dict(message, 'data')
marketId = self.safe_string(data, 'symbol')
symbol = self.safe_symbol(marketId)
if not (symbol in self.trades):
limit = self.safe_integer(self.options, 'tradesLimit', 1000)
self.trades[symbol] = ArrayCache(limit)
parsed = self.parse_ws_trade(data)
stored = self.trades[symbol]
stored.append(parsed)
client.resolve(stored, 'trade::' + symbol)
def parse_ws_trade(self, trade, market=None):
# same api
return self.parse_trade(trade, market)
async def authenticate(self, params={}):
self.check_required_credentials()
expires = (self.milliseconds() + self.safe_integer(self.options, 'requestExpiration', 5000)) * 1000 # need macroseconds
wsOptions: dict = self.safe_dict(self.options, 'ws', {})
authenticated = self.safe_string(wsOptions, 'token')
if authenticated is None:
method = 'GET'
bodyStr = ''
path = 'ws'
payload = self.apiKey + str(expires) + method.upper() + '/' + path + bodyStr
decodedSecret = self.base64_to_binary(self.secret)
signature = self.hmac(self.encode(payload), decodedSecret, hashlib.sha256, 'base64')
defaultOptions: dict = {
'ws': {
'options': {
'headers': {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Arkham-Api-Key': self.apiKey,
'Arkham-Expires': str(expires),
'Arkham-Signature': signature,
},
},
},
}
self.extend_exchange_options(defaultOptions)
self.client(self.urls['api']['ws'])
async def watch_balance(self, params={}) -> Balances:
"""
watch balance and get the amount of funds available for trading or funds locked in orders
https://arkm.com/docs#stream/balances
:param dict [params]: extra parameters specific to the exchange API endpoint
:param str [params.type]: spot or contract if not provided self.options['defaultType'] is used
:returns dict: a `balance structure <https://docs.ccxt.com/#/?id=balance-structure>`
"""
await self.authenticate()
await self.load_markets()
requestArg = {
'snapshot': True,
}
messageHash = 'balances'
result = await self.subscribe(messageHash, 'balances', self.extend(requestArg, params))
return result
def handle_balance(self, client: Client, message):
#
# snapshot:
#
# {
# channel: 'balances',
# type: 'snapshot',
# data: [
# {
# subaccountId: 0,
# symbol: 'USDT',
# balance: '7.035335375',
# free: '7.035335375',
# priceUSDT: '1',
# balanceUSDT: '7.035335375',
# freeUSDT: '7.035335375',
# lastUpdateReason: 'withdrawalFee',
# lastUpdateTime: '1753905990432678',
# lastUpdateId: 250483404,
# lastUpdateAmount: '-2'
# },
# {
# subaccountId: 0,
# symbol: 'SOL',
# balance: '0.03',
# free: '0.03',
# priceUSDT: '197.37823276',
# balanceUSDT: '5.921346982',
# freeUSDT: '5.921346982',
# lastUpdateReason: 'orderFill',
# lastUpdateTime: '1753777760560164',
# lastUpdateId: 248588190,
# lastUpdateAmount: '0.03'
# }
# ]
# }
#
# update:
#
# {
# channel: 'balances',
# type: 'update',
# data: {
# subaccountId: 0,
# symbol: 'USDT',
# balance: '7.028357615',
# free: '7.028357615',
# priceUSDT: '1',
# balanceUSDT: '7.028357615',
# freeUSDT: '7.028357615',
# lastUpdateReason: 'tradingFee',
# lastUpdateTime: '1755240882544056',
# lastUpdateId: 2697860787,
# lastUpdateAmount: '-0.00697776'
# }
# }
#
type = self.safe_string(message, 'type')
parsed = {}
if type == 'snapshot':
# response same api
data = self.safe_list(message, 'data')
parsed = self.parse_ws_balance(data)
parsed['info'] = message
self.balance = parsed
else:
data = self.safe_dict(message, 'data')
balancesArray = [data]
parsed = self.parse_ws_balance(balancesArray)
currencyId = self.safe_string(data, 'symbol')
code = self.safe_currency_code(currencyId)
self.balance[code] = parsed[code]
messageHash = 'balances'
client.resolve(self.safe_balance(self.balance), messageHash)
def parse_ws_balance(self, balance):
# same api
return self.parse_balance(balance)
async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]:
"""
https://arkm.com/docs#stream/positions
watch all open positions
:param str[] [symbols]: list of unified market symbols
:param int [since]: the earliest time in ms to fetch positions for
:param int [limit]: the maximum number of positions to retrieve
:param dict params: extra parameters specific to the exchange API endpoint
:returns dict[]: a list of `position structure <https://docs.ccxt.com/en/latest/manual.html#position-structure>`
"""
await self.authenticate()
await self.load_markets()
messageHash = 'positions'
if not self.is_empty(symbols):
symbols = self.market_symbols(symbols)
messageHash += '::' + ','.join(symbols)
self.positions = ArrayCacheBySymbolBySide()
requestArg = {
'snapshot': False, # no need for initial snapshot, it's done in REST api
}
newPositions = await self.subscribe(messageHash, 'positions', self.extend(requestArg, params))
if self.newUpdates:
return newPositions
return self.filter_by_symbols_since_limit(self.positions, symbols, since, limit, True)
def handle_positions(self, client, message):
#
# snapshot:
#
# {
# channel: 'positions',
# type: 'snapshot',
# data: [
# {
# subaccountId: 0,
# symbol: 'SOL_USDT_PERP',
# base: '0.059',
# quote: '-11.50618',
# openBuySize: '0',
# openSellSize: '0',
# openBuyNotional: '0',
# openSellNotional: '0',
# lastUpdateReason: 'orderFill',
# lastUpdateTime: '1755251065621402',
# lastUpdateId: 2709589783,
# lastUpdateBaseDelta: '0.059',
# lastUpdateQuoteDelta: '-11.50618',
# breakEvenPrice: '195.02',
# markPrice: '195',
# value: '11.505',
# pnl: '-0.00118',
# initialMargin: '1.1505',
# maintenanceMargin: '0.6903',
# averageEntryPrice: '195.02'
# }
# ]
# }
#
newPositions = []
if self.positions is None:
self.positions = {}
type = self.safe_string(message, 'type')
if type == 'snapshot':
data = self.safe_list(message, 'data', [])
for i in range(0, len(data)):
position = self.parse_ws_position(data[i])
if self.safe_integer(position, 'entryPrice') != 0:
newPositions.append(position)
symbol = self.safe_string(position, 'symbol')
self.positions[symbol] = position
else:
data = self.safe_dict(message, 'data')
position = self.parse_ws_position(data)
symbol = self.safe_string(position, 'symbol')
self.positions[symbol] = position
newPositions.append(position)
messageHashes = self.find_message_hashes(client, 'positions::')
for i in range(0, len(messageHashes)):
messageHash = messageHashes[i]
parts = messageHash.split('::')
symbolsString = parts[1]
symbols = symbolsString.split(',')
positions = self.filter_by_array(newPositions, 'symbol', symbols, False)
if not self.is_empty(positions):
client.resolve(positions, messageHash)
length = len(newPositions)
if length > 0:
client.resolve(newPositions, 'positions')
def parse_ws_positions(self, positions: List[Any], symbols: List[str] = None, params={}) -> List[Position]:
symbols = self.market_symbols(symbols)
positions = self.to_array(positions)
result = []
for i in range(0, len(positions)):
position = self.extend(self.parse_ws_position(positions[i], None), params)
result.append(position)
return self.filter_by_array_positions(result, 'symbol', symbols, False)
def parse_ws_position(self, position, market=None):
# same api
return self.parse_position(position, market)
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
https://arkm.com/docs#stream/order_statuses
: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 <https://docs.ccxt.com/#/?id=order-structure>`
"""
await self.authenticate()
await self.load_markets()
market = None
if symbol is not None:
market = self.market(symbol)
requestArg = {
'snapshot': False,
}
isTriggerOrder = False
isTriggerOrder, params = self.handle_option_and_params(params, 'watchOrders', 'trigger', False)
rawChannel = 'trigger_orders' if isTriggerOrder else 'order_statuses'
messageHash = 'orders'
if symbol is not None:
messageHash += '::' + market['symbol']
messageHash += '::' + rawChannel
orders = await self.subscribe(messageHash, rawChannel, self.extend(requestArg, params))
if self.newUpdates:
limit = orders.getLimit(symbol, limit)
return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True)
def handle_order(self, client: Client, message):
#
# {
# channel: "order_statuses",
# type: "update",
# data: {
# orderId: 4200775347657,
# userId: 2959880,
# subaccountId: 0,
# symbol: "ARKM_USDT_PERP",
# time: "1755253639782186",
# side: "buy",
# type: "limitGtc",
# size: "10",
# price: "0.5",
# postOnly: False,
# reduceOnly: False,
# executedSize: "0",
# status: "cancelled",
# avgPrice: "0",
# executedNotional: "0",
# creditFeePaid: "0",
# marginBonusFeePaid: "0",
# quoteFeePaid: "0",
# arkmFeePaid: "0",
# revisionId: 2752963990,
# lastTime: "1755272026403545",
# clientOrderId: "",
# lastSize: "0",
# lastPrice: "0",
# lastCreditFee: "0",
# lastMarginBonusFee: "0",
# lastQuoteFee: "0",
# lastArkmFee: "0",
# }
# }
#
channel = self.safe_string(message, 'channel')
data = self.safe_dict(message, 'data')
if self.orders is None:
limit = self.safe_integer(self.options, 'ordersLimit', 1000)
self.orders = ArrayCacheBySymbolById(limit)
orders = self.orders
order = self.parse_ws_order(data)
orders.append(order)
client.resolve(orders, 'orders')
client.resolve(orders, 'orders::' + order['symbol'] + '::' + channel)
client.resolve(orders, 'orders::' + channel)
def parse_ws_order(self, order, market=None) -> Order:
# same api
return self.parse_order(order, market)
def handle_error_message(self, client: Client, response) -> Bool:
#
# error example:
#
# {
# "id": "30005",
# "name": "InvalidNotional",
# "message": "order validation failed: invalid notional: notional 0.25 is less than min notional 1"
# }
#
message = self.safe_string(response, 'message')
if message is not None:
body = self.json(response)
errorCode = self.safe_string(response, 'id')
feedback = self.id + ' ' + body
self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback)
self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback)
self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback)
raise ExchangeError(self.id + ' ' + body)
return False