Skip to content

Commit 06f1117

Browse files
committed
feat: converted candles method
1 parent 32b81ae commit 06f1117

File tree

10 files changed

+267
-73
lines changed

10 files changed

+267
-73
lines changed

cryptomarket/args.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,10 +201,10 @@ def clean_nones(a_dict: Dict[Any, Optional[Any]]) -> Dict[Any, Any]:
201201

202202
class DictBuilder:
203203
def __init__(self):
204-
self.the_dict = dict()
204+
self.the_dict = {}
205205

206206
def build(self):
207-
orderedDict = dict()
207+
orderedDict = {}
208208
for parameter in sorted(self.the_dict):
209209
orderedDict[parameter] = self.the_dict[parameter]
210210
return orderedDict

cryptomarket/client.py

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
Price, PriceHistory, SubAccount, Symbol,
1212
Ticker, Trade, Transaction, Fee)
1313
from cryptomarket.dataclasses.aclSettings import ACLSettings
14+
from cryptomarket.dataclasses.convertedCandles import ConvertedCandles
15+
from cryptomarket.dataclasses.convertedCandlesOfSymbol import ConvertedCandlesOfSymbol
1416
from cryptomarket.dataclasses.publicTrade import PublicTrade
1517
from cryptomarket.http_client import HttpClient
1618

@@ -30,7 +32,6 @@ def __init__(self, api_key: Optional[str] = None, secret_key: Optional[str] = No
3032
self.get_trades_by_symbol = self.get_trades_of_symbol
3133
# aliases of orders
3234
self.create_new_order = self.create_spot_order
33-
self.create_spot_order = self.create_spot_order
3435
self.create_new_spot_order = self.create_spot_order
3536

3637
def close(self):
@@ -458,6 +459,92 @@ def get_candles_of_symbol(
458459
return [from_dict(data_class=Candle, data=candle_data)
459460
for candle_data in response]
460461

462+
def get_converted_candles(
463+
self,
464+
target_currency: str,
465+
symbols: Optional[List[str]] = None,
466+
period: Optional[Union[
467+
args.Period, Literal[
468+
'M1', 'M3', 'M15', 'M30', 'H1', 'H4', 'D1', 'D7', '1M'
469+
]
470+
]] = None,
471+
sort: Optional[Union[args.Sort, Literal['ASC', 'DESC']]] = None,
472+
since: str = None,
473+
till: str = None,
474+
limit: int = None
475+
) -> ConvertedCandles:
476+
"""Gets candles regarding the last price converted to the target currency for all symbols or for the specified symbols
477+
478+
Candles are used for OHLC representation
479+
480+
The result contains candles with non-zero volume only (no trades = no candles).
481+
482+
Conversion from the symbol quote currency to the target currency is the mean of "best" bid price and "best" ask price in the order book. If there is no "best" bid of ask price, the last price is returned.
483+
484+
Requires no API key Access Rights.
485+
486+
https://api.exchange.cryptomkt.com/#candles
487+
488+
:param target_currency: Target currency for conversion
489+
:param symbols: Optional. A list of symbol ids. If empty then gets for all symbols
490+
:param period: Optional. A valid tick interval. 'M1' (one minute), 'M3', 'M5', 'M15', 'M30', 'H1' (one hour), 'H4', 'D1' (one day), 'D7', '1M' (one month). Default is 'M30'
491+
:param sort: Optional. Sort direction. 'ASC' or 'DESC'. Default is 'DESC'
492+
:param from: Optional. Initial value of the queried interval. As DateTime
493+
:param till: Optional. Last value of the queried interval. As DateTime
494+
:param limit: Optional. Prices per currency pair. Defaul is 100. Min is 1. Max is 1000
495+
496+
:returns: A list of candles of a symbol
497+
"""
498+
params = args.DictBuilder().target_currency(target_currency).symbols(symbols).period(period).sort(sort).since(
499+
since).till(till).limit(limit).build()
500+
response = self._get(
501+
endpoint="public/converted/candles", params=params)
502+
return from_dict(ConvertedCandles, response)
503+
504+
def get_converted_candles_of_symbol(
505+
self,
506+
target_currency: str,
507+
symbol: str,
508+
period: Optional[Union[
509+
args.Period, Literal[
510+
'M1', 'M3', 'M15', 'M30', 'H1', 'H4', 'D1', 'D7', '1M'
511+
]
512+
]] = None,
513+
sort: Optional[Union[args.Sort, Literal['ASC', 'DESC']]] = None,
514+
since: str = None,
515+
till: str = None,
516+
limit: int = None,
517+
offset: int = None
518+
) -> ConvertedCandlesOfSymbol:
519+
"""Gets candles regarding the last price converted to the target currency for the specified symbols
520+
521+
Candles are used for OHLC representation
522+
523+
The result contains candles with non-zero volume only (no trades = no candles).
524+
525+
Conversion from the symbol quote currency to the target currency is the mean of "best" bid price and "best" ask price in the order book. If there is no "best" bid of ask price, the last price is returned.
526+
527+
Requires no API key Access Rights.
528+
529+
https://api.exchange.cryptomkt.com/#candles
530+
531+
:param target_currency: Target currency for conversion
532+
:param symbol: A symbol id
533+
:param period: Optional. A valid tick interval. 'M1' (one minute), 'M3', 'M5', 'M15', 'M30', 'H1' (one hour), 'H4', 'D1' (one day), 'D7', '1M' (one month). Default is 'M30'
534+
:param sort: Optional. Sort direction. 'ASC' or 'DESC'. Default is 'DESC'
535+
:param from: Optional. Initial value of the queried interval. As DateTime
536+
:param till: Optional. Last value of the queried interval. As DateTime
537+
:param limit: Optional. Prices per currency pair. Defaul is 100. Min is 1. Max is 1000
538+
:param offset: Optional. Default is 0. Min is 0. Max is 100000
539+
540+
:returns: A list of candles of a symbol
541+
"""
542+
params = args.DictBuilder().target_currency(target_currency).period(period).sort(sort).since(
543+
since).till(till).limit(limit).offset(offset).build()
544+
response = self._get(
545+
endpoint=f"public/converted/candles/{symbol}", params=params)
546+
return from_dict(ConvertedCandlesOfSymbol, response)
547+
461548
#################
462549
# AUTHENTICATED #
463550
#################
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from dataclasses import dataclass
2+
from typing import Dict, List
3+
from cryptomarket.dataclasses.candle import Candle
4+
5+
6+
@dataclass
7+
class ConvertedCandles:
8+
target_currency: str
9+
data: Dict[str, List[Candle]]
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from dataclasses import dataclass
2+
from typing import List
3+
4+
from cryptomarket.dataclasses.candle import Candle
5+
6+
7+
@dataclass
8+
class ConvertedCandlesOfSymbol:
9+
target_currency: str
10+
data: List[Candle]

cryptomarket/websockets/callback_cache.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
class CallbackCache:
66
def __init__(self):
7-
self.callbacks: Dict[str, ReusableCallback] = dict()
7+
self.callbacks: Dict[str, ReusableCallback] = {}
88
self._id = 1
99

1010
def next_id(self):

cryptomarket/websockets/market_data_client.py

Lines changed: 89 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def subscribe_to_trades(
102102
params = args.DictBuilder().symbols_as_list(symbols).limit(limit).build()
103103

104104
def intercept_feed(feed, feed_type):
105-
result = dict()
105+
result = {}
106106
for key in feed:
107107
result[key] = [from_dict(data_class=WSTrade, data=data)
108108
for data in feed[key]]
@@ -129,7 +129,7 @@ def subscribe_to_candles(
129129
):
130130
"""subscribe to a feed of candles
131131
132-
subscription is for the specified symbols
132+
subscription is only for the specified symbols
133133
134134
normal subscriptions have one update message per symbol
135135
@@ -140,12 +140,12 @@ def subscribe_to_candles(
140140
141141
:param callback: callable that recieves a dict of candles, indexed by symbol.
142142
:param symbols: A list of symbol ids to subscribe to
143-
:param period: Optional. A valid tick interval. 'M1' (one minute), 'M3', 'M5', 'M15', 'M30', 'H1' (one hour), 'H4', 'D1' (one day), 'D7', '1M' (one month). Default is 'M30'
143+
:param period: Optional. A valid tick interval. 'M1' (one minute), 'M3', 'M5', 'M15', 'M30', 'H1' (one hour), 'H4', 'D1' (one day), 'D7', '1M' (one month).
144144
:param limit: Number of historical entries returned in the first feed. Min is 0. Max is 1000. Default is 0
145145
:param result_callback: A callable of two arguments, takes either a CryptomarketAPIException, or the list of correctly subscribed symbols
146146
"""
147147
def intercept_feed(feed, feed_type):
148-
result = dict()
148+
result = {}
149149
for key in feed:
150150
result[key] = [
151151
from_dict(data_class=WSCandle, data=data) for data in feed[key]]
@@ -158,6 +158,51 @@ def intercept_feed(feed, feed_type):
158158
result_callback=result_callback,
159159
)
160160

161+
def subscribe_to_converted_candles(
162+
self,
163+
callback: Callable[[Dict[str, List[WSCandle]], Literal['snapshot', 'update']], None],
164+
target_currency: str,
165+
symbols: List[str],
166+
period: Union[args.Period,
167+
Literal['M1', 'M3', 'M15', 'M30', 'H1', 'H4', 'D1', 'D7', '1M']] = None,
168+
limit: Optional[int] = None,
169+
result_callback: Optional[Callable[[
170+
Union[CryptomarketAPIException, None], Union[List[str], None]], None]] = None,
171+
):
172+
"""subscribes to a feed of candles regarding the last price converted to the target currency for the specified symbols
173+
174+
subscription is only for the specified symbols
175+
176+
normal subscriptions have one update message per symbol
177+
178+
Conversion from the symbol quote currency to the target currency is the mean of "best" bid price and "best" ask price in the order book. If there is no "best" bid or ask price, the last price is returned.
179+
180+
Requires no API key Access Rights
181+
182+
https://api.exchange.cryptomkt.com/#subscribe-to-converted-candles
183+
184+
185+
:param callback: callable that recieves a dict of candles, indexed by symbol.
186+
:param symbols: A list of symbol ids to subscribe to
187+
:param period: A valid tick interval. 'M1' (one minute), 'M3', 'M5', 'M15', 'M30', 'H1' (one hour), 'H4', 'D1' (one day), 'D7', '1M' (one month).
188+
:param limit: Limit of returned entries. Min is 0. Max is 1000. Default is 0
189+
:param result_callback: A callable of two arguments, takes either a CryptomarketAPIException, or the list of correctly subscribed symbols
190+
"""
191+
def intercept_feed(feed, feed_type):
192+
result = {}
193+
for key in feed:
194+
result[key] = [
195+
from_dict(data_class=WSCandle, data=data) for data in feed[key]]
196+
callback(result, feed_type)
197+
params = args.DictBuilder().target_currency(
198+
target_currency).symbols_as_list(symbols).limit(limit).build()
199+
self._send_channeled_subscription(
200+
channel=f'converted/candles/{period}',
201+
callback=intercept_feed,
202+
params=params,
203+
result_callback=result_callback,
204+
)
205+
161206
def subscribe_to_mini_ticker(
162207
self,
163208
callback: Callable[[Dict[str, WSMiniTicker]], None],
@@ -510,7 +555,8 @@ def subscribe_to_price_rates(
510555
"""
511556
if currencies is None:
512557
currencies = ['*']
513-
params = args.DictBuilder().currencies_as_list(currencies).speed(speed).target_currency(target_currency).build()
558+
params = args.DictBuilder().currencies_as_list(currencies).speed(
559+
speed).target_currency(target_currency).build()
514560

515561
def intercept_feed(feed, feed_type):
516562
callback({key: from_dict(data_class=WSPriceRate, data=feed[key])
@@ -521,3 +567,41 @@ def intercept_feed(feed, feed_type):
521567
params=params,
522568
result_callback=result_callback
523569
)
570+
571+
def subscribe_to_price_rates_in_batches(
572+
self,
573+
callback: Callable[[Dict[str, WSPriceRate]], None],
574+
speed: Union[args.PriceRateSpeed, Literal['1s', '3s']],
575+
target_currency: Optional[str],
576+
currencies: Optional[List[str]] = None,
577+
result_callback: Optional[Callable[[
578+
Union[CryptomarketAPIException, None], Union[List[str], None]], None]] = None,
579+
):
580+
"""subscribe to a feed of price rates
581+
582+
subscription is for all currencies or specified currencies (bases), against a target currency (quote). indexed by currency id (bases)
583+
584+
batch subscriptions have a joined update for all symbols
585+
586+
https://api.exchange.cryptomkt.com/#subscribe-to-price-rates
587+
588+
:param callback: callable that recieves a dict of mini tickers, indexed by symbol.
589+
:param speed: The speed of the feed. '1s' or '3s'
590+
:param target_currency: quote currency for the price rates
591+
:param currencies: Optional. A list of currencies ids (as bases) to subscribe to. If not provided it subscribes to all currencies
592+
:param result_callback: A callable of two arguments, takes either a CryptomarketAPIException, or the list of correctly subscribed currencies
593+
"""
594+
if currencies is None:
595+
currencies = ['*']
596+
params = args.DictBuilder().currencies_as_list(currencies).speed(
597+
speed).target_currency(target_currency).build()
598+
599+
def intercept_feed(feed, feed_type):
600+
callback({key: from_dict(data_class=WSPriceRate, data=feed[key])
601+
for key in feed})
602+
self._send_channeled_subscription(
603+
channel=f'price/rate/{speed}/batch',
604+
callback=intercept_feed,
605+
params=params,
606+
result_callback=result_callback
607+
)

cryptomarket/websockets/orderbook_cache.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ class SideOrder(Enum):
1616

1717
class OrderbookCache:
1818
def __init__(self):
19-
self.orderbooks = dict()
20-
self.ob_states = dict()
19+
self.orderbooks = {}
20+
self.ob_states = {}
2121

2222
def update(self, method: str, key:str, new_data: dict):
2323
if method == methodSnapshotOrderbook:
Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -213,11 +213,7 @@ def test_get_ticker_price(self):
213213

214214
class GetTrades(PublicCallsTestCase):
215215
def test_get_trades_of_all_symbols(self):
216-
symbols = self.client.get_symbols()
217-
n_symbols = len(symbols)
218216
trades = self.client.get_trades()
219-
self.assertEqual(len(trades), n_symbols,
220-
'should have trades from all symbols')
221217
for key in trades:
222218
if not good_list(good_public_trade, trades[key]):
223219
self.fail("not a good trade")
@@ -262,11 +258,7 @@ def test_since_id(self):
262258

263259
class GetOrderBooks(PublicCallsTestCase):
264260
def test_get_all_symbols(self):
265-
symbols = self.client.get_symbols()
266-
n_symbols = len(symbols)
267261
orderbooks = self.client.get_order_books()
268-
self.assertEqual(len(orderbooks), n_symbols,
269-
"should have one orderbook per symbol")
270262
for key in orderbooks:
271263
orderbook_of_symbol = orderbooks[key]
272264
if not good_orderbook(orderbook_of_symbol):
@@ -309,11 +301,7 @@ def test_get_orderbook(self):
309301

310302
class GetCandles(PublicCallsTestCase):
311303
def test_get_all_symbols(self):
312-
symbols = self.client.get_candles()
313-
n_symbols = len(symbols)
314304
candles = self.client.get_candles()
315-
self.assertEqual(len(candles), n_symbols,
316-
"should have one candle per symbol")
317305
for key in candles:
318306
if not good_list(good_candle, candles[key]):
319307
self.fail("not good candles")
@@ -328,5 +316,47 @@ def test_one_symbol(self):
328316
self.assertEqual(len(candles), 1, "should have one candles")
329317

330318

319+
class GetConvertedCandles(PublicCallsTestCase):
320+
def test_get_all_symbols(self):
321+
target_currency = "BTC"
322+
converted_candles = self.client.get_converted_candles(target_currency)
323+
self.assertEqual(converted_candles.target_currency, target_currency)
324+
for key in converted_candles.data:
325+
if not good_list(good_candle, converted_candles.data[key]):
326+
self.fail("not good candles")
327+
328+
def test_get_many_symbols(self):
329+
target_currency = "BTC"
330+
converted_candles = self.client.get_converted_candles(
331+
target_currency, ["EOSETH", "ETHBTC", "CROBTC"])
332+
self.assertEqual(converted_candles.target_currency, target_currency)
333+
for key in converted_candles.data:
334+
if not good_list(good_candle, converted_candles.data[key]):
335+
self.fail("not good candles")
336+
self.assertEqual(len(converted_candles.data),
337+
3, "should have two candles")
338+
339+
def test_one_symbol(self):
340+
target_currency = "BTC"
341+
converted_candles = self.client.get_converted_candles(
342+
target_currency, ["CROBTC"])
343+
self.assertEqual(converted_candles.target_currency, target_currency)
344+
for key in converted_candles.data:
345+
if not good_list(good_candle, converted_candles.data[key]):
346+
self.fail("not good candles")
347+
self.assertEqual(len(converted_candles.data),
348+
1, "should have two candles")
349+
350+
351+
class GetConvertedCandlesOfSymbol(PublicCallsTestCase):
352+
def test_get_all_symbols(self):
353+
target_currency = "BTC"
354+
converted_candles = self.client.get_converted_candles_of_symbol(
355+
target_currency, symbol="CROETH")
356+
self.assertEqual(converted_candles.target_currency, target_currency)
357+
if not good_list(good_candle, converted_candles.data):
358+
self.fail("not good candles")
359+
360+
331361
if __name__ == '__main__':
332362
unittest.main()

0 commit comments

Comments
 (0)