dccd 2.3.0__tar.gz → 2.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. {dccd-2.3.0 → dccd-2.3.1}/CHANGELOG.md +12 -0
  2. {dccd-2.3.0 → dccd-2.3.1}/PKG-INFO +1 -1
  3. {dccd-2.3.0 → dccd-2.3.1}/dccd/histo_dl/coinbase.py +6 -3
  4. {dccd-2.3.0 → dccd-2.3.1}/dccd/histo_dl/exchange.py +16 -3
  5. {dccd-2.3.0 → dccd-2.3.1}/dccd/histo_dl/okx.py +5 -2
  6. {dccd-2.3.0 → dccd-2.3.1}/dccd/storage.py +14 -9
  7. {dccd-2.3.0 → dccd-2.3.1}/dccd/tests/test_coinbase.py +1 -1
  8. {dccd-2.3.0 → dccd-2.3.1}/dccd/tests/test_storage.py +46 -0
  9. {dccd-2.3.0 → dccd-2.3.1}/dccd.egg-info/PKG-INFO +1 -1
  10. {dccd-2.3.0 → dccd-2.3.1}/pyproject.toml +1 -1
  11. {dccd-2.3.0 → dccd-2.3.1}/CONTRIBUTING.md +0 -0
  12. {dccd-2.3.0 → dccd-2.3.1}/LICENSE.txt +0 -0
  13. {dccd-2.3.0 → dccd-2.3.1}/MANIFEST.in +0 -0
  14. {dccd-2.3.0 → dccd-2.3.1}/README.rst +0 -0
  15. {dccd-2.3.0 → dccd-2.3.1}/dccd/__init__.py +0 -0
  16. {dccd-2.3.0 → dccd-2.3.1}/dccd/continuous_dl/__init__.py +0 -0
  17. {dccd-2.3.0 → dccd-2.3.1}/dccd/continuous_dl/binance.py +0 -0
  18. {dccd-2.3.0 → dccd-2.3.1}/dccd/continuous_dl/bitfinex.py +0 -0
  19. {dccd-2.3.0 → dccd-2.3.1}/dccd/continuous_dl/bitmex.py +0 -0
  20. {dccd-2.3.0 → dccd-2.3.1}/dccd/continuous_dl/bybit.py +0 -0
  21. {dccd-2.3.0 → dccd-2.3.1}/dccd/continuous_dl/exchange.py +0 -0
  22. {dccd-2.3.0 → dccd-2.3.1}/dccd/continuous_dl/kraken.py +0 -0
  23. {dccd-2.3.0 → dccd-2.3.1}/dccd/continuous_dl/okx.py +0 -0
  24. {dccd-2.3.0 → dccd-2.3.1}/dccd/daemon/__init__.py +0 -0
  25. {dccd-2.3.0 → dccd-2.3.1}/dccd/daemon/backfill.py +0 -0
  26. {dccd-2.3.0 → dccd-2.3.1}/dccd/daemon/cli.py +0 -0
  27. {dccd-2.3.0 → dccd-2.3.1}/dccd/daemon/config.py +0 -0
  28. {dccd-2.3.0 → dccd-2.3.1}/dccd/daemon/health.py +0 -0
  29. {dccd-2.3.0 → dccd-2.3.1}/dccd/daemon/scheduler.py +0 -0
  30. {dccd-2.3.0 → dccd-2.3.1}/dccd/daemon/storage.py +0 -0
  31. {dccd-2.3.0 → dccd-2.3.1}/dccd/daemon/stream_manager.py +0 -0
  32. {dccd-2.3.0 → dccd-2.3.1}/dccd/histo_dl/__init__.py +0 -0
  33. {dccd-2.3.0 → dccd-2.3.1}/dccd/histo_dl/binance.py +0 -0
  34. {dccd-2.3.0 → dccd-2.3.1}/dccd/histo_dl/bybit.py +0 -0
  35. {dccd-2.3.0 → dccd-2.3.1}/dccd/histo_dl/kraken.py +0 -0
  36. {dccd-2.3.0 → dccd-2.3.1}/dccd/models.py +0 -0
  37. {dccd-2.3.0 → dccd-2.3.1}/dccd/process_data.py +0 -0
  38. {dccd-2.3.0 → dccd-2.3.1}/dccd/tests/__init__.py +0 -0
  39. {dccd-2.3.0 → dccd-2.3.1}/dccd/tests/conftest.py +0 -0
  40. {dccd-2.3.0 → dccd-2.3.1}/dccd/tests/test_backfill.py +0 -0
  41. {dccd-2.3.0 → dccd-2.3.1}/dccd/tests/test_binance.py +0 -0
  42. {dccd-2.3.0 → dccd-2.3.1}/dccd/tests/test_binance_ws.py +0 -0
  43. {dccd-2.3.0 → dccd-2.3.1}/dccd/tests/test_bitfinex.py +0 -0
  44. {dccd-2.3.0 → dccd-2.3.1}/dccd/tests/test_bitmex.py +0 -0
  45. {dccd-2.3.0 → dccd-2.3.1}/dccd/tests/test_bybit.py +0 -0
  46. {dccd-2.3.0 → dccd-2.3.1}/dccd/tests/test_bybit_ws.py +0 -0
  47. {dccd-2.3.0 → dccd-2.3.1}/dccd/tests/test_daemon_cli.py +0 -0
  48. {dccd-2.3.0 → dccd-2.3.1}/dccd/tests/test_daemon_config.py +0 -0
  49. {dccd-2.3.0 → dccd-2.3.1}/dccd/tests/test_daemon_health.py +0 -0
  50. {dccd-2.3.0 → dccd-2.3.1}/dccd/tests/test_daemon_scheduler.py +0 -0
  51. {dccd-2.3.0 → dccd-2.3.1}/dccd/tests/test_daemon_storage.py +0 -0
  52. {dccd-2.3.0 → dccd-2.3.1}/dccd/tests/test_daemon_stream_manager.py +0 -0
  53. {dccd-2.3.0 → dccd-2.3.1}/dccd/tests/test_date_time.py +0 -0
  54. {dccd-2.3.0 → dccd-2.3.1}/dccd/tests/test_histo_dl.py +0 -0
  55. {dccd-2.3.0 → dccd-2.3.1}/dccd/tests/test_io.py +0 -0
  56. {dccd-2.3.0 → dccd-2.3.1}/dccd/tests/test_kraken.py +0 -0
  57. {dccd-2.3.0 → dccd-2.3.1}/dccd/tests/test_kraken_ws.py +0 -0
  58. {dccd-2.3.0 → dccd-2.3.1}/dccd/tests/test_models.py +0 -0
  59. {dccd-2.3.0 → dccd-2.3.1}/dccd/tests/test_okx.py +0 -0
  60. {dccd-2.3.0 → dccd-2.3.1}/dccd/tests/test_okx_ws.py +0 -0
  61. {dccd-2.3.0 → dccd-2.3.1}/dccd/tests/test_process_data.py +0 -0
  62. {dccd-2.3.0 → dccd-2.3.1}/dccd/tests/test_websocket.py +0 -0
  63. {dccd-2.3.0 → dccd-2.3.1}/dccd/tools/__init__.py +0 -0
  64. {dccd-2.3.0 → dccd-2.3.1}/dccd/tools/date_time.py +0 -0
  65. {dccd-2.3.0 → dccd-2.3.1}/dccd/tools/io.py +0 -0
  66. {dccd-2.3.0 → dccd-2.3.1}/dccd/tools/websocket.py +0 -0
  67. {dccd-2.3.0 → dccd-2.3.1}/dccd.egg-info/SOURCES.txt +0 -0
  68. {dccd-2.3.0 → dccd-2.3.1}/dccd.egg-info/dependency_links.txt +0 -0
  69. {dccd-2.3.0 → dccd-2.3.1}/dccd.egg-info/entry_points.txt +0 -0
  70. {dccd-2.3.0 → dccd-2.3.1}/dccd.egg-info/requires.txt +0 -0
  71. {dccd-2.3.0 → dccd-2.3.1}/dccd.egg-info/top_level.txt +0 -0
  72. {dccd-2.3.0 → dccd-2.3.1}/setup.cfg +0 -0
@@ -6,6 +6,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [2.3.1] - 2026-05-24
10
+
11
+ ### Fixed
12
+
13
+ - `dccd/storage.py` — `DataStore.missing_intervals` now detects the gap **before** the first saved row when the requested `start` predates `file_min`; previously only the trailing gap (after `file_max`) was returned, causing `dccd backfill --start <early-date>` to silently skip all historical data before the first existing candle (#46)
14
+ - `dccd/histo_dl/coinbase.py` — raise `RuntimeError` when Coinbase returns HTTP 200 with a JSON dict (e.g. `{"message": "..."}` for near-future windows) instead of silently iterating dict keys and crashing with `ValueError` (#45)
15
+ - `dccd/histo_dl/coinbase.py` — additional guard: raise `RuntimeError` when Coinbase returns a JSON list whose first element is not itself a list/tuple (e.g. `["message"]`); previously caused `float("m")` `ValueError` (#45)
16
+ - `dccd/histo_dl/exchange.py` — `_sort_data` no longer raises `KeyError: 'TS'` when the API returns empty data; returns early with an empty `self.df` so the backfill skips the window cleanly (#45)
17
+ - `dccd/histo_dl/exchange.py` — `_sort_data` strips any candle at or beyond `self.end` before merging; exchanges with inclusive endpoint semantics (Coinbase) no longer cause `_advance` to overshoot by one span per window, preventing drift that accumulated into near-future requests (#45)
18
+ - `dccd/histo_dl/okx.py` — raise `RuntimeError` when OKX response code is not `"0"`, letting the backfill retry/skip logic handle API-level errors (#45)
19
+ - `dccd/histo_dl/okx.py` — switch `_import_data` from `/market/candles` to `/market/history-candles`; the former only serves the last ~24 h of 1-minute bars and silently returns empty data for older windows (#45)
20
+
9
21
  ## [2.3.0] - 2026-05-22
10
22
 
11
23
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dccd
3
- Version: 2.3.0
3
+ Version: 2.3.1
4
4
  Summary: Download Crypto Currency Data from different exchanges.
5
5
  Author-email: Arthur Bernard <arthur.bernard.92@gmail.com>
6
6
  License: MIT
@@ -26,7 +26,6 @@ from typing import Any
26
26
  from dccd.histo_dl.exchange import ImportDataCryptoCurrencies
27
27
 
28
28
  # Import local packages
29
- from dccd.tools.date_time import TS_to_date
30
29
 
31
30
  __all__ = ['FromCoinbase']
32
31
 
@@ -116,8 +115,8 @@ class FromCoinbase(ImportDataCryptoCurrencies):
116
115
  def _import_data(self, start: int | str = 'last', end: int | str = 'now') -> list[dict[str, Any]]:
117
116
  self.start, self.end = self._set_time(start, end)
118
117
  param = {
119
- 'start': TS_to_date(self.start - self.span),
120
- 'end': TS_to_date(self.end),
118
+ 'start': datetime.fromtimestamp(self.start, tz=timezone.utc).isoformat(),
119
+ 'end': datetime.fromtimestamp(self.end, tz=timezone.utc).isoformat(),
121
120
  'granularity': self.span,
122
121
  }
123
122
  r = self._fetch(
@@ -127,6 +126,10 @@ class FromCoinbase(ImportDataCryptoCurrencies):
127
126
  param,
128
127
  )
129
128
  text = r.json()
129
+ if not isinstance(text, list):
130
+ raise RuntimeError(f"Coinbase API error: {text!r:.200}")
131
+ if text and not isinstance(text[0], (list, tuple)):
132
+ raise RuntimeError(f"Coinbase unexpected response: {text[:3]!r:.200}")
130
133
  data = [{
131
134
  'date': float(e[0]),
132
135
  'open': float(e[3]),
@@ -128,10 +128,9 @@ class ImportDataCryptoCurrencies(ABC):
128
128
  wait=wait_exponential(multiplier=1, min=1, max=60),
129
129
  stop=stop_after_attempt(5))
130
130
  def _fetch(self, url: str, params: dict[str, Any]) -> requests.Response:
131
- """ Fetch URL with automatic retry on HTTP 429. """
131
+ """ Fetch URL and raise on any HTTP error. """
132
132
  r = requests.get(url, params)
133
- if r.status_code == 429:
134
- r.raise_for_status()
133
+ r.raise_for_status()
135
134
  return r
136
135
 
137
136
  def _get_last_date(self) -> int:
@@ -228,6 +227,17 @@ class ImportDataCryptoCurrencies(ABC):
228
227
  """
229
228
  data = [OHLCBar(**d).model_dump(exclude_none=False) for d in data]
230
229
  df = pd.DataFrame(data).rename(columns={'date': 'TS'})
230
+ if df.empty or 'TS' not in df.columns:
231
+ self.df = df
232
+ return self
233
+ # Discard any candle at or beyond self.end: those belong to the next
234
+ # window. Without this filter, exchanges that return the endpoint
235
+ # candle (inclusive API boundary) would cause _advance to overshoot
236
+ # by one span, and the drift compounds over many windows.
237
+ # Guard: only filter when self.end is set (> 0); direct callers that
238
+ # bypass _set_time leave self.end = 0, so we skip the filter.
239
+ if self.end > 0:
240
+ df = df[df['TS'] < self.end]
231
241
  # Use self.end as the exclusive grid boundary so the full window is
232
242
  # covered even when the last trade arrives >span seconds before the
233
243
  # window end. Callers must set self.end to the correct window
@@ -236,6 +246,9 @@ class ImportDataCryptoCurrencies(ABC):
236
246
  list(range(self.start, self.end, self.span)),
237
247
  columns=['TS']
238
248
  )
249
+ if df.empty or 'TS' not in df.columns:
250
+ self.df = df
251
+ return self
239
252
  df = (df.merge(TS, on='TS', how='outer', sort=False)
240
253
  .sort_values('TS')
241
254
  .reset_index(drop=True)
@@ -145,8 +145,11 @@ class FromOKX(ImportDataCryptoCurrencies):
145
145
  'limit': 300,
146
146
  }
147
147
 
148
- r = self._fetch('https://www.okx.com/api/v5/market/candles', param)
149
- text = r.json()['data']
148
+ r = self._fetch('https://www.okx.com/api/v5/market/history-candles', param)
149
+ body = r.json()
150
+ if body.get('code', '0') != '0':
151
+ raise RuntimeError(f"OKX API error {body['code']}: {body.get('msg', body)}")
152
+ text = body['data']
150
153
  text.reverse()
151
154
 
152
155
  data = [{
@@ -319,18 +319,23 @@ class DataStore:
319
319
 
320
320
  file_path = self.directory / f'{year}.parquet'
321
321
 
322
- if year < current_year and file_path.exists():
323
- if self.is_period_complete(year):
322
+ if file_path.exists():
323
+ if year < current_year and self.is_period_complete(year):
324
324
  continue # full year already on disk — skip
325
- # incomplete past year: resume from last saved row
326
- df = pd.read_parquet(file_path, columns=['TS'])
327
- if not df.empty:
328
- ivl_start = int(df['TS'].max()) + self.span
329
- elif file_path.exists():
330
- # current year: always extend from the last saved row
331
325
  df = pd.read_parquet(file_path, columns=['TS'])
332
326
  if not df.empty:
333
- ivl_start = int(df['TS'].max()) + self.span
327
+ file_min = int(df['TS'].min())
328
+ file_max = int(df['TS'].max())
329
+ # Gap before the first saved row (e.g. backfill requested
330
+ # from an earlier date than the file's first candle)
331
+ if ivl_start < file_min:
332
+ intervals.append((ivl_start, file_min))
333
+ # Trailing gap after the last saved row
334
+ trailing = file_max + self.span
335
+ if trailing < ivl_end:
336
+ intervals.append((trailing, ivl_end))
337
+ continue
338
+ # file exists but is empty: fall through to full-interval append
334
339
 
335
340
  if ivl_start < ivl_end:
336
341
  intervals.append((ivl_start, ivl_end))
@@ -47,7 +47,7 @@ def test_malformed_response_raises(loader, monkeypatch):
47
47
  m.status_code = 200
48
48
  m.json.return_value = {"error": "bad"}
49
49
  monkeypatch.setattr("requests.get", lambda *a, **kw: m)
50
- with pytest.raises((TypeError, ValueError)):
50
+ with pytest.raises((TypeError, ValueError, RuntimeError)):
51
51
  loader._import_data(start=0)
52
52
 
53
53
 
@@ -336,6 +336,52 @@ def test_missing_intervals_already_up_to_date(tmp_path):
336
336
  assert intervals == []
337
337
 
338
338
 
339
+ def test_missing_intervals_start_before_file_min_current_year(tmp_path):
340
+ """File starts mid-year; requested start is before file_min → beginning gap + trailing gap."""
341
+ from datetime import datetime, timezone
342
+ span = 3600
343
+ store = DataStore(str(tmp_path), 'binance', 'BTC/USDT', span, 'ohlc')
344
+ current_year = datetime.now(tz=timezone.utc).year
345
+ year_start = int(datetime(current_year, 1, 1, tzinfo=timezone.utc).timestamp())
346
+ # File covers mid-May onwards (simulates the user's real situation)
347
+ file_start = year_start + 100 * span
348
+ file_end = year_start + 110 * span
349
+ store.save(_ohlc_df(list(range(file_start, file_end + span, span))))
350
+ end_ts = file_end + 50 * span
351
+
352
+ intervals = store.missing_intervals(year_start, end_ts)
353
+
354
+ # Expect two intervals: [year_start, file_start) and [file_end+span, end_ts)
355
+ assert len(intervals) == 2
356
+ assert intervals[0] == (year_start, file_start)
357
+ assert intervals[1] == (file_end + span, end_ts)
358
+
359
+
360
+ def test_missing_intervals_start_before_file_min_past_year(tmp_path):
361
+ """Past incomplete year where file starts after requested start → beginning gap returned."""
362
+ span = 3600
363
+ store = DataStore(str(tmp_path), 'binance', 'BTC/USDT', span, 'ohlc')
364
+ file_start = _Y2023 + 100 * span
365
+ file_end = _Y2023 + 200 * span
366
+ store.save(_ohlc_df(list(range(file_start, file_end + span, span))))
367
+
368
+ intervals = store.missing_intervals(_Y2023, _Y2024)
369
+
370
+ assert len(intervals) == 2
371
+ assert intervals[0] == (_Y2023, file_start)
372
+ assert intervals[1] == (file_end + span, _Y2024)
373
+
374
+
375
+ def test_missing_intervals_file_already_covers_full_request(tmp_path):
376
+ """File already covers [start, end] exactly → no intervals."""
377
+ span = 3600
378
+ store = DataStore(str(tmp_path), 'binance', 'BTC/USDT', span, 'ohlc')
379
+ store.save(_ohlc_df(list(range(_Y2023, _Y2023 + 10 * span + span, span))))
380
+ end_ts = _Y2023 + 5 * span
381
+ intervals = store.missing_intervals(_Y2023, end_ts)
382
+ assert intervals == []
383
+
384
+
339
385
  def test_missing_intervals_non_ohlc_simple_resume(tmp_path):
340
386
  store = DataStore(str(tmp_path), 'binance', 'BTC/USDT', None, 'trades')
341
387
  store.save(_trades_df([1672531200]))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dccd
3
- Version: 2.3.0
3
+ Version: 2.3.1
4
4
  Summary: Download Crypto Currency Data from different exchanges.
5
5
  Author-email: Arthur Bernard <arthur.bernard.92@gmail.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dccd"
7
- version = "2.3.0"
7
+ version = "2.3.1"
8
8
  description = "Download Crypto Currency Data from different exchanges."
9
9
  readme = "README.rst"
10
10
  license = { text = "MIT" }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes