Skip to content

Commit c077273

Browse files
authored
Merge branch 'development' into Fix/Empty_snapshot
2 parents f0b42c3 + 0c1e35f commit c077273

File tree

7 files changed

+248
-2
lines changed

7 files changed

+248
-2
lines changed

hummingbot/data_feed/candles_feed/candles_factory.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from hummingbot.data_feed.candles_feed.bybit_spot_candles.bybit_spot_candles import BybitSpotCandles
88
from hummingbot.data_feed.candles_feed.candles_base import CandlesBase
99
from hummingbot.data_feed.candles_feed.data_types import CandlesConfig
10+
from hummingbot.data_feed.candles_feed.dexalot_spot_candles.dexalot_spot_candles import DexalotSpotCandles
1011
from hummingbot.data_feed.candles_feed.gate_io_perpetual_candles import GateioPerpetualCandles
1112
from hummingbot.data_feed.candles_feed.gate_io_spot_candles import GateioSpotCandles
1213
from hummingbot.data_feed.candles_feed.hyperliquid_perpetual_candles.hyperliquid_perpetual_candles import (
@@ -52,7 +53,8 @@ class CandlesFactory:
5253
"bybit": BybitSpotCandles,
5354
"bybit_perpetual": BybitPerpetualCandles,
5455
"hyperliquid": HyperliquidSpotCandles,
55-
"hyperliquid_perpetual": HyperliquidPerpetualCandles
56+
"hyperliquid_perpetual": HyperliquidPerpetualCandles,
57+
"dexalot": DexalotSpotCandles
5658
}
5759

5860
@classmethod
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from hummingbot.data_feed.candles_feed.dexalot_spot_candles.dexalot_spot_candles import DexalotSpotCandles
2+
3+
__all__ = ["DexalotSpotCandles"]
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from bidict import bidict
2+
3+
from hummingbot.core.api_throttler.data_types import LinkedLimitWeightPair, RateLimit
4+
5+
REST_URL = "https://api.dexalot.com/privapi"
6+
HEALTH_CHECK_ENDPOINT = "/trading/environments"
7+
CANDLES_ENDPOINT = "/trading/candlechart"
8+
9+
WSS_URL = "wss://api.dexalot.com/api/ws"
10+
11+
# "M5", "M15", "M30", "H1" "H4", "D1" only these are supported
12+
INTERVALS = bidict({
13+
"5m": "M5",
14+
"15m": "M15",
15+
"30m": "M30",
16+
"1h": "H1",
17+
"4h": "H4",
18+
"1d": "D1",
19+
})
20+
21+
MAX_RESULTS_PER_CANDLESTICK_REST_REQUEST = 1000
22+
23+
RATE_LIMITS = [
24+
RateLimit(CANDLES_ENDPOINT, limit=20000, time_interval=60, linked_limits=[LinkedLimitWeightPair("raw", 1)]),
25+
RateLimit(HEALTH_CHECK_ENDPOINT, limit=20000, time_interval=60, linked_limits=[LinkedLimitWeightPair("raw", 1)])]
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import logging
2+
from datetime import datetime
3+
from typing import Any, Dict, List, Optional
4+
5+
from hummingbot.core.network_iterator import NetworkStatus
6+
from hummingbot.data_feed.candles_feed.candles_base import CandlesBase
7+
from hummingbot.data_feed.candles_feed.dexalot_spot_candles import constants as CONSTANTS
8+
from hummingbot.logger import HummingbotLogger
9+
10+
11+
class DexalotSpotCandles(CandlesBase):
12+
_logger: Optional[HummingbotLogger] = None
13+
14+
@classmethod
15+
def logger(cls) -> HummingbotLogger:
16+
if cls._logger is None:
17+
cls._logger = logging.getLogger(__name__)
18+
return cls._logger
19+
20+
def __init__(self, trading_pair: str, interval: str = "1m", max_records: int = 150):
21+
super().__init__(trading_pair, interval, max_records)
22+
23+
@property
24+
def name(self):
25+
return f"dexalot_{self._trading_pair}"
26+
27+
@property
28+
def rest_url(self):
29+
return CONSTANTS.REST_URL
30+
31+
@property
32+
def wss_url(self):
33+
return CONSTANTS.WSS_URL
34+
35+
@property
36+
def health_check_url(self):
37+
return self.rest_url + CONSTANTS.HEALTH_CHECK_ENDPOINT
38+
39+
@property
40+
def candles_url(self):
41+
return self.rest_url + CONSTANTS.CANDLES_ENDPOINT
42+
43+
@property
44+
def candles_endpoint(self):
45+
return CONSTANTS.CANDLES_ENDPOINT
46+
47+
@property
48+
def candles_max_result_per_rest_request(self):
49+
return CONSTANTS.MAX_RESULTS_PER_CANDLESTICK_REST_REQUEST
50+
51+
@property
52+
def rate_limits(self):
53+
return CONSTANTS.RATE_LIMITS
54+
55+
@property
56+
def intervals(self):
57+
return CONSTANTS.INTERVALS
58+
59+
async def check_network(self) -> NetworkStatus:
60+
rest_assistant = await self._api_factory.get_rest_assistant()
61+
await rest_assistant.execute_request(url=self.health_check_url,
62+
throttler_limit_id=CONSTANTS.HEALTH_CHECK_ENDPOINT)
63+
return NetworkStatus.CONNECTED
64+
65+
def get_exchange_trading_pair(self, trading_pair):
66+
return trading_pair.replace("-", "/")
67+
68+
@property
69+
def _is_first_candle_not_included_in_rest_request(self):
70+
return False
71+
72+
@property
73+
def _is_last_candle_not_included_in_rest_request(self):
74+
return False
75+
76+
def _get_rest_candles_params(self,
77+
start_time: Optional[int] = None,
78+
end_time: Optional[int] = None,
79+
limit: Optional[int] = CONSTANTS.MAX_RESULTS_PER_CANDLESTICK_REST_REQUEST) -> dict:
80+
"""
81+
For API documentation, please refer to:
82+
83+
startTime and endTime must be used at the same time.
84+
"""
85+
_intervalstr = self.interval[-1]
86+
if _intervalstr == 'm':
87+
intervalstr = 'minute'
88+
elif _intervalstr == 'h':
89+
intervalstr = 'hour'
90+
elif _intervalstr == 'd':
91+
intervalstr = 'day'
92+
else:
93+
intervalstr = ''
94+
params = {
95+
"pair": self._ex_trading_pair,
96+
"intervalnum": CONSTANTS.INTERVALS[self.interval][1:],
97+
"intervalstr": intervalstr,
98+
}
99+
if start_time is not None or end_time is not None:
100+
start_time = start_time if start_time is not None else end_time - limit * self.interval_in_seconds
101+
start_isotime = f"{datetime.fromtimestamp(start_time).isoformat(timespec='milliseconds')}Z"
102+
params["periodfrom"] = start_isotime
103+
end_time = end_time if end_time is not None else start_time + limit * self.interval_in_seconds
104+
end_isotiome = f"{datetime.fromtimestamp(end_time).isoformat(timespec='milliseconds')}Z"
105+
params["periodto"] = end_isotiome
106+
return params
107+
108+
def _parse_rest_candles(self, data: dict, end_time: Optional[int] = None) -> List[List[float]]:
109+
if data is not None and len(data) > 0:
110+
return [[self.ensure_timestamp_in_seconds(datetime.strptime(row["date"], '%Y-%m-%dT%H:%M:%S.%fZ').timestamp()),
111+
row["open"] if row["open"] != 'None' else None,
112+
row["high"] if row["high"] != 'None' else None,
113+
row["low"] if row["low"] != 'None' else None,
114+
row["close"] if row["close"] != 'None' else None,
115+
row["volume"] if row["volume"] != 'None' else None,
116+
0., 0., 0., 0.] for row in data]
117+
118+
def ws_subscription_payload(self):
119+
interval = CONSTANTS.INTERVALS[self.interval]
120+
trading_pair = self.get_exchange_trading_pair(self._trading_pair)
121+
122+
payload = {
123+
"pair": trading_pair,
124+
"chart": interval,
125+
"type": "chart-v2-subscribe"
126+
}
127+
return payload
128+
129+
def _parse_websocket_message(self, data):
130+
candles_row_dict: Dict[str, Any] = {}
131+
if data is not None and data.get("type") == 'liveCandle':
132+
candle = data.get("data")[-1]
133+
timestamp = datetime.strptime(candle["date"], '%Y-%m-%dT%H:%M:%SZ').timestamp()
134+
candles_row_dict["timestamp"] = self.ensure_timestamp_in_seconds(timestamp)
135+
candles_row_dict["open"] = candle["open"]
136+
candles_row_dict["low"] = candle["low"]
137+
candles_row_dict["high"] = candle["high"]
138+
candles_row_dict["close"] = candle["close"]
139+
candles_row_dict["volume"] = candle["volume"]
140+
candles_row_dict["quote_asset_volume"] = 0.
141+
candles_row_dict["n_trades"] = 0.
142+
candles_row_dict["taker_buy_base_volume"] = 0.
143+
candles_row_dict["taker_buy_quote_volume"] = 0.
144+
return candles_row_dict

scripts/utility/candles_example.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def format_status(self) -> str:
7676
candles_df.ta.rsi(length=14, append=True)
7777
candles_df.ta.bbands(length=20, std=2, append=True)
7878
candles_df.ta.ema(length=14, offset=None, append=True)
79-
candles_df["timestamp"] = pd.to_datetime(candles_df["timestamp"], unit="ms")
79+
candles_df["timestamp"] = pd.to_datetime(candles_df["timestamp"], unit="s")
8080
lines.extend([f"Candles: {candles.name} | Interval: {candles.interval}"])
8181
lines.extend([" " + line for line in candles_df.tail().to_string(index=False).split("\n")])
8282
lines.extend(["\n-----------------------------------------------------------------------------------------------------------\n"])

test/hummingbot/data_feed/candles_feed/dexalot_spot_candles/__init__.py

Whitespace-only changes.
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import asyncio
2+
from test.hummingbot.data_feed.candles_feed.test_candles_base import TestCandlesBase
3+
4+
from hummingbot.connector.test_support.network_mocking_assistant import NetworkMockingAssistant
5+
from hummingbot.data_feed.candles_feed.dexalot_spot_candles import DexalotSpotCandles
6+
7+
8+
class TestDexalotSpotCandles(TestCandlesBase):
9+
__test__ = True
10+
level = 0
11+
12+
@classmethod
13+
def setUpClass(cls) -> None:
14+
super().setUpClass()
15+
cls.ev_loop = asyncio.get_event_loop()
16+
cls.base_asset = "ALOT"
17+
cls.quote_asset = "USDC"
18+
cls.interval = "5m"
19+
cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}"
20+
cls.ex_trading_pair = cls.base_asset + "/" + cls.quote_asset
21+
cls.max_records = 150
22+
23+
def setUp(self) -> None:
24+
super().setUp()
25+
self.start_time = 1734619800
26+
self.end_time = 1734620700
27+
self.mocking_assistant = NetworkMockingAssistant()
28+
self.data_feed = DexalotSpotCandles(trading_pair=self.trading_pair, interval=self.interval)
29+
30+
self.log_records = []
31+
self.data_feed.logger().setLevel(1)
32+
self.data_feed.logger().addHandler(self)
33+
self.resume_test_event = asyncio.Event()
34+
35+
def get_fetch_candles_data_mock(self):
36+
return [[1734619800.0, None, None, None, None, None, 0.0, 0.0, 0.0, 0.0],
37+
[1734620100.0, '1.0128', '1.0128', '1.0128', '1.0128', '4.94', 0.0, 0.0, 0.0, 0.0],
38+
[1734620400.0, None, None, None, None, None, 0.0, 0.0, 0.0, 0.0],
39+
[1734620700.0, '1.0074', '1.0073', '1.0074', '1.0073', '68.91', 0.0, 0.0, 0.0,
40+
0.0]]
41+
42+
def get_candles_rest_data_mock(self):
43+
return [
44+
{'pair': 'ALOT/USDC', 'date': '2024-12-19T22:50:00.000Z', 'low': None, 'high': None, 'open': None,
45+
'close': None, 'volume': None, 'change': None},
46+
{'pair': 'ALOT/USDC', 'date': '2024-12-19T22:55:00.000Z', 'low': '1.0128', 'high': '1.0128',
47+
'open': '1.0128', 'close': '1.0128', 'volume': '4.94', 'change': '0.0000'},
48+
{'pair': 'ALOT/USDC', 'date': '2024-12-19T23:00:00.000Z', 'low': None, 'high': None, 'open': None,
49+
'close': None, 'volume': None, 'change': None},
50+
{'pair': 'ALOT/USDC', 'date': '2024-12-19T23:05:00.000Z', 'low': '1.0073', 'high': '1.0074',
51+
'open': '1.0074', 'close': '1.0073', 'volume': '68.91', 'change': '-0.0001'},
52+
]
53+
54+
def get_candles_ws_data_mock_1(self):
55+
return {'data': [
56+
{'date': '2025-01-11T17:25:00Z', 'low': '0.834293', 'high': '0.8343', 'open': '0.834293',
57+
'close': '0.8343',
58+
'volume': '74.858252584002608541', 'change': '0.00', 'active': True, 'updated': True}],
59+
'type': 'liveCandle',
60+
'pair': 'ALOT/USDC'}
61+
62+
def get_candles_ws_data_mock_2(self):
63+
return {'data': [
64+
{'date': '2025-01-11T17:30:00Z', 'low': '0.834293', 'high': '0.8343', 'open': '0.834293',
65+
'close': '0.8343',
66+
'volume': '74.858252584002608541', 'change': '0.00', 'active': True, 'updated': True}],
67+
'type': 'liveCandle',
68+
'pair': 'ALOT/USDC'}
69+
70+
@staticmethod
71+
def _success_subscription_mock():
72+
return {'data': 'Dexalot websocket server...', 'type': 'info'}

0 commit comments

Comments
 (0)