opencode-pyneruntime 6.6.4__py3-none-any.whl
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.
- opencode_pyneruntime-6.6.4.dist-info/METADATA +281 -0
- opencode_pyneruntime-6.6.4.dist-info/RECORD +261 -0
- opencode_pyneruntime-6.6.4.dist-info/WHEEL +5 -0
- opencode_pyneruntime-6.6.4.dist-info/entry_points.txt +6 -0
- opencode_pyneruntime-6.6.4.dist-info/licenses/LICENSE +201 -0
- opencode_pyneruntime-6.6.4.dist-info/licenses/NOTICE +21 -0
- opencode_pyneruntime-6.6.4.dist-info/top_level.txt +1 -0
- pynecore/__init__.py +6 -0
- pynecore/cli/__init__.py +2 -0
- pynecore/cli/app.py +238 -0
- pynecore/cli/commands/__init__.py +343 -0
- pynecore/cli/commands/benchmark.py +186 -0
- pynecore/cli/commands/compile.py +198 -0
- pynecore/cli/commands/data.py +857 -0
- pynecore/cli/commands/debug.py +63 -0
- pynecore/cli/commands/optimize.py +956 -0
- pynecore/cli/commands/plugin.py +242 -0
- pynecore/cli/commands/run.py +2006 -0
- pynecore/cli/pluggable.py +132 -0
- pynecore/cli/utils/__init__.py +0 -0
- pynecore/cli/utils/api_error_handler.py +168 -0
- pynecore/cli/utils/broker_picker.py +330 -0
- pynecore/cli/utils/error_hook.py +28 -0
- pynecore/cli/utils/keyreader.py +178 -0
- pynecore/cli/utils/provider_picker.py +19 -0
- pynecore/cli/utils/symbol_browser.py +1149 -0
- pynecore/core/__init__.py +0 -0
- pynecore/core/aggregator.py +257 -0
- pynecore/core/bar_magnifier.py +168 -0
- pynecore/core/broker/__init__.py +64 -0
- pynecore/core/broker/defaults.py +113 -0
- pynecore/core/broker/disappearance.py +927 -0
- pynecore/core/broker/emulator.py +345 -0
- pynecore/core/broker/exceptions.py +346 -0
- pynecore/core/broker/idempotency.py +401 -0
- pynecore/core/broker/intent_builder.py +334 -0
- pynecore/core/broker/journal.py +1785 -0
- pynecore/core/broker/models.py +1600 -0
- pynecore/core/broker/native_failsafe_manager.py +1436 -0
- pynecore/core/broker/one_way_emulator.py +1128 -0
- pynecore/core/broker/position.py +787 -0
- pynecore/core/broker/run_identity.py +126 -0
- pynecore/core/broker/software_entry_stop_engine.py +351 -0
- pynecore/core/broker/software_partial_bracket_engine.py +1379 -0
- pynecore/core/broker/spot_inventory.py +1327 -0
- pynecore/core/broker/storage.py +2655 -0
- pynecore/core/broker/store_helpers.py +2161 -0
- pynecore/core/broker/sync_engine.py +16070 -0
- pynecore/core/broker/validation.py +382 -0
- pynecore/core/class_property.py +7 -0
- pynecore/core/config.py +392 -0
- pynecore/core/csv_file.py +547 -0
- pynecore/core/currency.py +262 -0
- pynecore/core/data_converter.py +1002 -0
- pynecore/core/datetime.py +296 -0
- pynecore/core/download_info.py +71 -0
- pynecore/core/download_runner.py +274 -0
- pynecore/core/htf_aggregator.py +181 -0
- pynecore/core/import_hook.py +358 -0
- pynecore/core/instance_state.py +494 -0
- pynecore/core/live_ltf_collector.py +442 -0
- pynecore/core/live_ltf_window.py +189 -0
- pynecore/core/live_runner.py +1347 -0
- pynecore/core/module_property.py +26 -0
- pynecore/core/ohlcv_file.py +1888 -0
- pynecore/core/overload.py +371 -0
- pynecore/core/pine_cast.py +113 -0
- pynecore/core/pine_export.py +95 -0
- pynecore/core/pine_method.py +244 -0
- pynecore/core/pine_range.py +86 -0
- pynecore/core/pine_udt.py +69 -0
- pynecore/core/plugin/__init__.py +394 -0
- pynecore/core/plugin/broker.py +781 -0
- pynecore/core/plugin/cli.py +96 -0
- pynecore/core/plugin/live_provider.py +208 -0
- pynecore/core/plugin/provider.py +331 -0
- pynecore/core/provider_string.py +148 -0
- pynecore/core/random.py +40 -0
- pynecore/core/resampler.py +686 -0
- pynecore/core/safe_convert.py +64 -0
- pynecore/core/script.py +1011 -0
- pynecore/core/script_runner.py +3202 -0
- pynecore/core/security.py +1749 -0
- pynecore/core/security_process.py +1253 -0
- pynecore/core/security_shm.py +456 -0
- pynecore/core/series.py +417 -0
- pynecore/core/strategy_stats.py +669 -0
- pynecore/core/symbol_map.py +134 -0
- pynecore/core/syminfo.py +505 -0
- pynecore/core/viz.py +591 -0
- pynecore/lib/__init__.py +1771 -0
- pynecore/lib/_fixnan.py +32 -0
- pynecore/lib/_math_stateful.py +202 -0
- pynecore/lib/_timeframe_change.py +101 -0
- pynecore/lib/adjustment.py +6 -0
- pynecore/lib/alert.py +39 -0
- pynecore/lib/alert.pyi +14 -0
- pynecore/lib/array.py +1051 -0
- pynecore/lib/barmerge.py +60 -0
- pynecore/lib/barstate.py +30 -0
- pynecore/lib/box.py +415 -0
- pynecore/lib/chart.py +128 -0
- pynecore/lib/color.py +152 -0
- pynecore/lib/color.pyi +50 -0
- pynecore/lib/currency.py +62 -0
- pynecore/lib/dayofweek.py +36 -0
- pynecore/lib/dayofweek.pyi +18 -0
- pynecore/lib/display.py +8 -0
- pynecore/lib/dividends.py +9 -0
- pynecore/lib/earnings.py +11 -0
- pynecore/lib/extend.py +6 -0
- pynecore/lib/font.py +5 -0
- pynecore/lib/footprint.py +79 -0
- pynecore/lib/format.py +11 -0
- pynecore/lib/hline.py +67 -0
- pynecore/lib/hline.pyi +24 -0
- pynecore/lib/label.py +409 -0
- pynecore/lib/line.py +433 -0
- pynecore/lib/linefill.py +93 -0
- pynecore/lib/location.py +11 -0
- pynecore/lib/log.py +362 -0
- pynecore/lib/map.py +150 -0
- pynecore/lib/math.py +385 -0
- pynecore/lib/matrix.py +708 -0
- pynecore/lib/order.py +8 -0
- pynecore/lib/pivotpointtype.py +8 -0
- pynecore/lib/plot.py +95 -0
- pynecore/lib/plot.pyi +33 -0
- pynecore/lib/polyline.py +91 -0
- pynecore/lib/position.py +15 -0
- pynecore/lib/request.py +281 -0
- pynecore/lib/runtime.py +5 -0
- pynecore/lib/scale.py +9 -0
- pynecore/lib/session.py +267 -0
- pynecore/lib/session.pyi +12 -0
- pynecore/lib/shape.py +18 -0
- pynecore/lib/size.py +12 -0
- pynecore/lib/splits.py +4 -0
- pynecore/lib/strategy/__init__.py +4778 -0
- pynecore/lib/strategy/closedtrades.py +347 -0
- pynecore/lib/strategy/closedtrades.pyi +53 -0
- pynecore/lib/strategy/commission.py +9 -0
- pynecore/lib/strategy/direction.py +9 -0
- pynecore/lib/strategy/oca.py +13 -0
- pynecore/lib/strategy/opentrades.py +281 -0
- pynecore/lib/strategy/opentrades.pyi +49 -0
- pynecore/lib/strategy/risk.py +109 -0
- pynecore/lib/string.py +649 -0
- pynecore/lib/syminfo.py +84 -0
- pynecore/lib/ta.py +2230 -0
- pynecore/lib/table.py +290 -0
- pynecore/lib/text.py +17 -0
- pynecore/lib/ticker.py +207 -0
- pynecore/lib/timeframe.py +293 -0
- pynecore/lib/volume_row.py +67 -0
- pynecore/lib/xloc.py +4 -0
- pynecore/lib/yloc.py +5 -0
- pynecore/providers/__init__.py +0 -0
- pynecore/providers/ccxt.py +664 -0
- pynecore/providers/replay.py +187 -0
- pynecore/pynesys/__init__.py +0 -0
- pynecore/pynesys/api.py +498 -0
- pynecore/pynesys/compiler.py +112 -0
- pynecore/standalone.py +99 -0
- pynecore/testing/__init__.py +1 -0
- pynecore/testing/broker_lab/__init__.py +41 -0
- pynecore/testing/broker_lab/__main__.py +5 -0
- pynecore/testing/broker_lab/cli.py +87 -0
- pynecore/testing/broker_lab/generate.py +47 -0
- pynecore/testing/broker_lab/model.py +84 -0
- pynecore/testing/broker_lab/reference.py +645 -0
- pynecore/testing/broker_lab/runner.py +372 -0
- pynecore/testing/broker_lab/scheduler.py +50 -0
- pynecore/testing/broker_lab/subprocess.py +73 -0
- pynecore/transformers/__init__.py +0 -0
- pynecore/transformers/builtin_shadow.py +136 -0
- pynecore/transformers/closure_arguments_transformer.py +428 -0
- pynecore/transformers/display_rewrite.py +140 -0
- pynecore/transformers/dynamic_default.py +147 -0
- pynecore/transformers/function_isolation.py +757 -0
- pynecore/transformers/import_lifter.py +61 -0
- pynecore/transformers/import_normalizer.py +328 -0
- pynecore/transformers/inline_series_hoist.py +178 -0
- pynecore/transformers/input_transformer.py +175 -0
- pynecore/transformers/lib_series.py +201 -0
- pynecore/transformers/locations.py +70 -0
- pynecore/transformers/module_properties.json +3387 -0
- pynecore/transformers/module_property.py +221 -0
- pynecore/transformers/ne_guard.py +70 -0
- pynecore/transformers/persistent.py +320 -0
- pynecore/transformers/persistent_series.py +76 -0
- pynecore/transformers/safe_convert_transformer.py +97 -0
- pynecore/transformers/safe_division_transformer.py +95 -0
- pynecore/transformers/script_requirements.py +308 -0
- pynecore/transformers/security.py +752 -0
- pynecore/transformers/security_instantiation.py +274 -0
- pynecore/transformers/series.py +275 -0
- pynecore/transformers/slot_layout.py +381 -0
- pynecore/transformers/type_checking_stripper.py +25 -0
- pynecore/transformers/unused_series_detector.py +267 -0
- pynecore/types/__init__.py +21 -0
- pynecore/types/alert.py +5 -0
- pynecore/types/barmerge.py +5 -0
- pynecore/types/base.py +39 -0
- pynecore/types/box.py +37 -0
- pynecore/types/chart.py +17 -0
- pynecore/types/color.py +107 -0
- pynecore/types/currency.py +5 -0
- pynecore/types/datetime.py +6 -0
- pynecore/types/display.py +5 -0
- pynecore/types/dividends.py +5 -0
- pynecore/types/earnings.py +5 -0
- pynecore/types/extend.py +5 -0
- pynecore/types/font.py +5 -0
- pynecore/types/footprint.py +41 -0
- pynecore/types/format.py +5 -0
- pynecore/types/hline.py +24 -0
- pynecore/types/ib_persistent.py +8 -0
- pynecore/types/ib_persistent.pyi +10 -0
- pynecore/types/label.py +35 -0
- pynecore/types/line.py +32 -0
- pynecore/types/linefill.py +13 -0
- pynecore/types/location.py +5 -0
- pynecore/types/matrix.py +999 -0
- pynecore/types/na.py +237 -0
- pynecore/types/na.pyi +83 -0
- pynecore/types/ohlcv.py +12 -0
- pynecore/types/order.py +5 -0
- pynecore/types/persistent.py +8 -0
- pynecore/types/persistent.pyi +13 -0
- pynecore/types/pine_types.py +11 -0
- pynecore/types/pine_types.pyi +15 -0
- pynecore/types/pivotpointtype.py +5 -0
- pynecore/types/plot.py +12 -0
- pynecore/types/plot_meta.py +60 -0
- pynecore/types/polyline.py +40 -0
- pynecore/types/position.py +5 -0
- pynecore/types/scale.py +5 -0
- pynecore/types/script_type.py +15 -0
- pynecore/types/series.py +23 -0
- pynecore/types/series.pyi +19 -0
- pynecore/types/session.py +35 -0
- pynecore/types/shape.py +5 -0
- pynecore/types/size.py +5 -0
- pynecore/types/source.py +33 -0
- pynecore/types/splits.py +5 -0
- pynecore/types/strategy.py +45 -0
- pynecore/types/table.py +87 -0
- pynecore/types/text.py +13 -0
- pynecore/types/type_checker.py +7 -0
- pynecore/types/type_checker.pyi +48 -0
- pynecore/types/volume_row.py +36 -0
- pynecore/types/weekdays.py +11 -0
- pynecore/types/xloc.py +5 -0
- pynecore/types/yloc.py +5 -0
- pynecore/utils/__init__.py +0 -0
- pynecore/utils/file_utils.py +50 -0
- pynecore/utils/rich/__init__.py +0 -0
- pynecore/utils/rich/date_column.py +25 -0
- pynecore/utils/sequence_view.py +92 -0
- pynecore/utils/stdlib_checker.py +17 -0
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
from typing import Any, Callable
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
import re
|
|
4
|
+
from datetime import datetime, UTC, timedelta, time
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import tomllib
|
|
7
|
+
|
|
8
|
+
from pynecore.core.plugin import override, ProviderError, Broker
|
|
9
|
+
from pynecore.core.plugin.live_provider import LiveProviderConfig, LiveProviderPlugin
|
|
10
|
+
from pynecore.core.syminfo import SymInfo, SymInfoInterval, SymInfoSession
|
|
11
|
+
from ..types.ohlcv import OHLCV
|
|
12
|
+
|
|
13
|
+
__all__ = ['CCXTProvider', 'CCXTError']
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CCXTError(ProviderError):
|
|
17
|
+
"""Raised when a CCXT library call fails in a user-actionable way.
|
|
18
|
+
|
|
19
|
+
Wraps the underlying :class:`ccxt.BaseError` (and the unknown-exchange
|
|
20
|
+
``AttributeError``) raised during client creation, market loading and
|
|
21
|
+
candle downloads so the ``pyne data`` CLI reports an exchange / network /
|
|
22
|
+
auth failure as a clean one-line error instead of a traceback. The live
|
|
23
|
+
streaming path is intentionally left unwrapped so its errors still reach
|
|
24
|
+
the :class:`LiveProviderPlugin` reconnect logic.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
_KNOWN_LIMITS = {
|
|
28
|
+
'binance': 1000,
|
|
29
|
+
'bitget': {
|
|
30
|
+
'1w': 12,
|
|
31
|
+
'1d': 300,
|
|
32
|
+
'4h': 1000,
|
|
33
|
+
'default': 200
|
|
34
|
+
},
|
|
35
|
+
'bitmex': 500,
|
|
36
|
+
'bybit': 200,
|
|
37
|
+
'coinbase': 300,
|
|
38
|
+
'kraken': 720,
|
|
39
|
+
'kucoin': 1500,
|
|
40
|
+
'okex': 200,
|
|
41
|
+
'huobi': 2000,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
_PYNECORE_ONLY_CONFIG_KEYS: frozenset[str] = frozenset({
|
|
45
|
+
'sandbox',
|
|
46
|
+
'default_type',
|
|
47
|
+
'symbol_map',
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
# Fallback tick size when an exchange exposes no usable price precision at all.
|
|
51
|
+
# Eight decimals is the common crypto default; an over-fine tick only adds
|
|
52
|
+
# display precision, while a too-coarse one would silently round away real
|
|
53
|
+
# price moves, so erring fine is the safe direction.
|
|
54
|
+
_DEFAULT_MINTICK: float = 1e-8
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def add_space_before_uppercase(s):
|
|
58
|
+
return re.sub(r'(?<!^)([A-Z])', r' \1', s)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class CCXTConfig(LiveProviderConfig):
|
|
63
|
+
"""CCXT provider configuration.
|
|
64
|
+
|
|
65
|
+
Fields map to CCXT constructor keyword arguments, so adding a new
|
|
66
|
+
exchange-specific setting is usually as simple as declaring it here
|
|
67
|
+
with the same name CCXT uses — ``vars(config)`` is filtered for
|
|
68
|
+
truthy values and spread into the CCXT client constructor. Inherits
|
|
69
|
+
``symbol_map`` from :class:`LiveProviderConfig` so ``ccxt.toml`` can
|
|
70
|
+
declare TradingView→native symbol translations for
|
|
71
|
+
:meth:`ProviderPlugin.resolve_symbol`.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
apiKey: str = ""
|
|
75
|
+
"""Default API key for all exchanges"""
|
|
76
|
+
|
|
77
|
+
secret: str = ""
|
|
78
|
+
"""Default API secret for all exchanges"""
|
|
79
|
+
|
|
80
|
+
password: str = ""
|
|
81
|
+
"""Default API password (required by some exchanges like KuCoin)"""
|
|
82
|
+
|
|
83
|
+
sandbox: bool = False
|
|
84
|
+
"""Enable the exchange's testnet / demo endpoint via ``set_sandbox_mode``."""
|
|
85
|
+
|
|
86
|
+
default_type: str = "swap"
|
|
87
|
+
"""Default market type — ``"spot"`` / ``"swap"`` / ``"future"`` / ``"margin"``."""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class CCXTProvider(LiveProviderPlugin[CCXTConfig]):
|
|
91
|
+
"""CCXT-based market-data provider for ~100 crypto exchanges.
|
|
92
|
+
|
|
93
|
+
Wraps the CCXT library for symbol discovery and historical candle
|
|
94
|
+
downloads, and CCXT Pro for real-time WebSocket OHLCV streaming.
|
|
95
|
+
One plugin instance serves every CCXT-registered exchange — the
|
|
96
|
+
exchange is selected from the symbol prefix (`BYBIT:BTCUSDT` etc.).
|
|
97
|
+
|
|
98
|
+
**Supported**
|
|
99
|
+
|
|
100
|
+
- Symbol discovery and market metadata across all CCXT exchanges
|
|
101
|
+
- Historical candle downloads with per-exchange bar-limit hints
|
|
102
|
+
- Real-time OHLCV WebSocket streaming (CCXT Pro)
|
|
103
|
+
- Sandbox / testnet endpoints via the `sandbox` config flag
|
|
104
|
+
- Forwards any extra field on `CCXTConfig` as a CCXT constructor
|
|
105
|
+
kwarg — add exchange-specific settings without touching the plugin
|
|
106
|
+
|
|
107
|
+
**Limitations**
|
|
108
|
+
|
|
109
|
+
- **No order execution** — this plugin is market-data only. For live
|
|
110
|
+
trading use a dedicated exchange `BrokerPlugin` (Capital.com,
|
|
111
|
+
Interactive Brokers, Bybit, Binance).
|
|
112
|
+
- API credentials are per-exchange; the defaults on `CCXTConfig`
|
|
113
|
+
apply to every exchange unless overridden via CCXT's own routing.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
plugin_name = "CCXT"
|
|
117
|
+
Config = CCXTConfig
|
|
118
|
+
multi_broker = True
|
|
119
|
+
|
|
120
|
+
# ``watch_ohlcv`` is trade-driven on many exchanges (ccxt.pro builds
|
|
121
|
+
# candles from trades where no native kline stream pushes idle
|
|
122
|
+
# updates), so an illiquid 24/7 pair can legitimately stay silent for
|
|
123
|
+
# several bars. The framework default (3 bars) would reconnect-churn
|
|
124
|
+
# on every quiet stretch; 30 bars keeps the dead-feed safety net while
|
|
125
|
+
# making false positives rare.
|
|
126
|
+
feed_timeout_bars = 30
|
|
127
|
+
|
|
128
|
+
@classmethod
|
|
129
|
+
@override
|
|
130
|
+
def get_list_of_brokers(cls) -> list[Broker]:
|
|
131
|
+
"""Return the exchanges CCXT can serve, with their display names.
|
|
132
|
+
|
|
133
|
+
The id is the CCXT exchange id (e.g. ``"binance"``); the name is the
|
|
134
|
+
exchange's human-readable title (e.g. ``"Binance"``), read from a
|
|
135
|
+
throwaway instance — construction is offline and cheap (~100 exchanges
|
|
136
|
+
in a fraction of a second).
|
|
137
|
+
"""
|
|
138
|
+
try:
|
|
139
|
+
import ccxt
|
|
140
|
+
except ImportError:
|
|
141
|
+
raise ImportError("CCXT is not installed. Please install it using `pip install ccxt`.")
|
|
142
|
+
brokers: list[Broker] = []
|
|
143
|
+
for exchange_id in ccxt.exchanges:
|
|
144
|
+
try:
|
|
145
|
+
name = getattr(ccxt, exchange_id)().name or ""
|
|
146
|
+
except Exception: # noqa: BLE001 - a single bad exchange must not drop the list
|
|
147
|
+
name = ""
|
|
148
|
+
brokers.append(Broker(id=exchange_id, name=name))
|
|
149
|
+
return brokers
|
|
150
|
+
|
|
151
|
+
@classmethod
|
|
152
|
+
@override
|
|
153
|
+
def to_tradingview_timeframe(cls, timeframe: str) -> str:
|
|
154
|
+
"""
|
|
155
|
+
Convert CCXT timeframe fmt to TradingView fmt.
|
|
156
|
+
|
|
157
|
+
:param timeframe: Timeframe in CCXT fmt (e.g. "1m", "5m", "1h", "1d", "1w", "1M")
|
|
158
|
+
:return: Timeframe in TradingView fmt (e.g. "1", "5", "60", "1D", "1W", "1M")
|
|
159
|
+
:raises ValueError: If timeframe fmt is invalid
|
|
160
|
+
"""
|
|
161
|
+
if len(timeframe) < 2:
|
|
162
|
+
raise ValueError(f"Invalid timeframe fmt: {timeframe}")
|
|
163
|
+
|
|
164
|
+
unit = timeframe[-1]
|
|
165
|
+
value = timeframe[:-1]
|
|
166
|
+
|
|
167
|
+
if not value.isdigit() or int(value) <= 0:
|
|
168
|
+
raise ValueError(f"Invalid timeframe value: {value}")
|
|
169
|
+
|
|
170
|
+
if unit == 'm':
|
|
171
|
+
return value
|
|
172
|
+
elif unit == 'h':
|
|
173
|
+
return str(int(value) * 60)
|
|
174
|
+
elif unit == 'd':
|
|
175
|
+
return f"{value}D"
|
|
176
|
+
elif unit == 'w':
|
|
177
|
+
return f"{value}W"
|
|
178
|
+
elif unit == 'M':
|
|
179
|
+
return f"{value}M"
|
|
180
|
+
else:
|
|
181
|
+
raise ValueError(f"Invalid timeframe fmt: {timeframe}")
|
|
182
|
+
|
|
183
|
+
@classmethod
|
|
184
|
+
@override
|
|
185
|
+
def to_exchange_timeframe(cls, timeframe: str) -> str:
|
|
186
|
+
"""
|
|
187
|
+
Convert TradingView timeframe fmt to CCXT fmt.
|
|
188
|
+
|
|
189
|
+
:param timeframe: Timeframe in TradingView fmt (e.g. "1", "5", "60", "1D", "1W", "1M")
|
|
190
|
+
:return: Timeframe in CCXT fmt (e.g. "1m", "5m", "1h", "1d", "1w", "1M")
|
|
191
|
+
:raises ValueError: If timeframe fmt is invalid
|
|
192
|
+
"""
|
|
193
|
+
if timeframe.isdigit():
|
|
194
|
+
mins = int(timeframe)
|
|
195
|
+
if mins <= 0:
|
|
196
|
+
raise ValueError(f"Invalid timeframe value: {timeframe}")
|
|
197
|
+
if mins >= 60 and mins % 60 == 0:
|
|
198
|
+
return f"{mins // 60}h"
|
|
199
|
+
return f"{mins}m"
|
|
200
|
+
|
|
201
|
+
if len(timeframe) < 2:
|
|
202
|
+
raise ValueError(f"Invalid timeframe fmt: {timeframe}")
|
|
203
|
+
|
|
204
|
+
unit = timeframe[-1].upper()
|
|
205
|
+
value = timeframe[:-1]
|
|
206
|
+
|
|
207
|
+
if not value.isdigit() or int(value) <= 0:
|
|
208
|
+
raise ValueError(f"Invalid timeframe value: {value}")
|
|
209
|
+
|
|
210
|
+
if unit == 'D':
|
|
211
|
+
return f"{value}d"
|
|
212
|
+
elif unit == 'W':
|
|
213
|
+
return f"{value}w"
|
|
214
|
+
elif unit == 'M':
|
|
215
|
+
return f"{value}M"
|
|
216
|
+
else:
|
|
217
|
+
raise ValueError(f"Invalid timeframe fmt: {timeframe}")
|
|
218
|
+
|
|
219
|
+
@override
|
|
220
|
+
def __init__(self, *, symbol: str | None = None, timeframe: str | None = None,
|
|
221
|
+
ohlcv_dir: Path | None = None, config: object | None = None):
|
|
222
|
+
"""
|
|
223
|
+
:param symbol: The symbol to get data for (e.g. "binance:BTC/USDT")
|
|
224
|
+
:param timeframe: The timeframe to get data for in TradingView fmt
|
|
225
|
+
:param ohlcv_dir: The directory to save OHLCV data
|
|
226
|
+
:param config: Pre-loaded CCXTConfig instance
|
|
227
|
+
"""
|
|
228
|
+
try:
|
|
229
|
+
import ccxt
|
|
230
|
+
except ImportError:
|
|
231
|
+
raise ImportError("CCXT is not installed. Please install it using `pip install ccxt`.")
|
|
232
|
+
|
|
233
|
+
super().__init__(symbol=symbol, timeframe=timeframe, ohlcv_dir=ohlcv_dir, config=config)
|
|
234
|
+
|
|
235
|
+
# Check symbol fmt
|
|
236
|
+
try:
|
|
237
|
+
if symbol is None:
|
|
238
|
+
raise ValueError("Error: Symbol not provided!")
|
|
239
|
+
xchg, symbol = symbol.split(':', 1)
|
|
240
|
+
except (ValueError, AttributeError):
|
|
241
|
+
xchg = symbol
|
|
242
|
+
symbol = None
|
|
243
|
+
|
|
244
|
+
if not xchg:
|
|
245
|
+
raise ValueError("Error: Exchange name not provided! Use 'exchange:symbol' fmt! "
|
|
246
|
+
"(or simple exchange, if you want to list symbols)")
|
|
247
|
+
|
|
248
|
+
self.symbol = symbol
|
|
249
|
+
self._exchange_name = xchg.lower()
|
|
250
|
+
exchange_name = self._exchange_name
|
|
251
|
+
|
|
252
|
+
# Build exchange config from the Config dataclass + optional exchange-specific TOML sections
|
|
253
|
+
exchange_config = {}
|
|
254
|
+
if self.config:
|
|
255
|
+
exchange_config = {
|
|
256
|
+
k: v for k, v in vars(self.config).items()
|
|
257
|
+
if v and k not in _PYNECORE_ONLY_CONFIG_KEYS
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
# Check for exchange-specific override in the raw TOML
|
|
261
|
+
if self.ohlcv_path:
|
|
262
|
+
config_dir = self.ohlcv_path.parent.parent / 'config'
|
|
263
|
+
else:
|
|
264
|
+
config_dir = None
|
|
265
|
+
|
|
266
|
+
if config_dir:
|
|
267
|
+
toml_path = config_dir / 'plugins' / 'ccxt.toml'
|
|
268
|
+
if toml_path.exists():
|
|
269
|
+
with open(toml_path, 'rb') as f:
|
|
270
|
+
raw_toml = tomllib.load(f)
|
|
271
|
+
if exchange_name in raw_toml and isinstance(raw_toml[exchange_name], dict):
|
|
272
|
+
exchange_config = raw_toml[exchange_name]
|
|
273
|
+
|
|
274
|
+
self._async_client = None
|
|
275
|
+
self._last_bar_timestamp: int | None = None
|
|
276
|
+
self._last_bar_ohlcv: OHLCV | None = None
|
|
277
|
+
self._exchange_config: dict[str, Any] = dict(exchange_config)
|
|
278
|
+
|
|
279
|
+
# Create the CCXT client
|
|
280
|
+
try:
|
|
281
|
+
self._client: ccxt.Exchange = getattr(ccxt, exchange_name)({
|
|
282
|
+
'enableRateLimit': True,
|
|
283
|
+
'adjustForTimeDifference': True,
|
|
284
|
+
**exchange_config
|
|
285
|
+
})
|
|
286
|
+
except AttributeError:
|
|
287
|
+
raise CCXTError(
|
|
288
|
+
f"Unknown exchange: {exchange_name!r}. Run "
|
|
289
|
+
f"`pyne data download ccxt --list-brokers` to see available exchanges."
|
|
290
|
+
) from None
|
|
291
|
+
if self.config and getattr(self.config, 'sandbox', False):
|
|
292
|
+
try:
|
|
293
|
+
self._client.set_sandbox_mode(True)
|
|
294
|
+
except ccxt.NotSupported:
|
|
295
|
+
pass
|
|
296
|
+
|
|
297
|
+
@override
|
|
298
|
+
def normalize_symbol(self, symbol: str) -> str:
|
|
299
|
+
"""Strip exchange prefix: ``"binance:BTC/USDT"`` → ``"BTC/USDT"``."""
|
|
300
|
+
return self.symbol or symbol
|
|
301
|
+
|
|
302
|
+
@override
|
|
303
|
+
def get_list_of_symbols(self, *args, **kwargs) -> list[str]:
|
|
304
|
+
"""Get list of symbols."""
|
|
305
|
+
import ccxt
|
|
306
|
+
try:
|
|
307
|
+
self._client.load_markets()
|
|
308
|
+
except ccxt.BaseError as exc:
|
|
309
|
+
raise CCXTError(f"{self._client.id}: {exc}") from exc
|
|
310
|
+
return self._client.symbols or []
|
|
311
|
+
|
|
312
|
+
@staticmethod
|
|
313
|
+
def _create_24_7_sessions() -> tuple[
|
|
314
|
+
list[SymInfoInterval], list[SymInfoSession], list[SymInfoSession]
|
|
315
|
+
]:
|
|
316
|
+
"""
|
|
317
|
+
Create 24/7 opening hours and sessions for crypto markets.
|
|
318
|
+
|
|
319
|
+
:return: Tuple of (opening_hours, session_starts, session_ends).
|
|
320
|
+
"""
|
|
321
|
+
return cls._create_24_7_sessions()
|
|
322
|
+
|
|
323
|
+
@staticmethod
|
|
324
|
+
def _create_24_7_sessions() -> tuple[
|
|
325
|
+
list[SymInfoInterval], list[SymInfoSession], list[SymInfoSession]
|
|
326
|
+
]:
|
|
327
|
+
"""
|
|
328
|
+
Create 24/7 opening hours and sessions for crypto markets.
|
|
329
|
+
|
|
330
|
+
:return: Tuple of (opening_hours, session_starts, session_ends).
|
|
331
|
+
"""
|
|
332
|
+
opening_hours = []
|
|
333
|
+
session_starts = []
|
|
334
|
+
session_ends = []
|
|
335
|
+
for i in range(7):
|
|
336
|
+
opening_hours.append(
|
|
337
|
+
SymInfoInterval(day=i, start=time(hour=0, minute=0),
|
|
338
|
+
end=time(hour=23, minute=59, second=59)))
|
|
339
|
+
session_starts.append(SymInfoSession(day=i, time=time(hour=0, minute=0)))
|
|
340
|
+
session_ends.append(SymInfoSession(day=i, time=time(hour=23, minute=59, second=59)))
|
|
341
|
+
return opening_hours, session_starts, session_ends
|
|
342
|
+
|
|
343
|
+
def _derive_mintick(self, market_details: dict) -> float:
|
|
344
|
+
"""Derive the minimum price increment (tick size) for a market.
|
|
345
|
+
|
|
346
|
+
CCXT exposes price precision in three different ``precisionMode``
|
|
347
|
+
flavours, and not every exchange populates ``precision.price`` at all:
|
|
348
|
+
|
|
349
|
+
* ``TICK_SIZE`` -- ``precision.price`` already *is* the tick size.
|
|
350
|
+
* ``DECIMAL_PLACES`` -- ``precision.price`` is a decimal-place count.
|
|
351
|
+
* ``SIGNIFICANT_DIGITS`` -- ``precision.price`` cannot map to a fixed
|
|
352
|
+
tick (it depends on the price magnitude) and is often ``None`` (e.g.
|
|
353
|
+
bitvavo).
|
|
354
|
+
|
|
355
|
+
For the latter two we fall back to the raw ``info['tickSize']`` the
|
|
356
|
+
exchange ships, then to ``limits.price.min``, and finally to a
|
|
357
|
+
conservative default so symbol info never crashes on a missing or
|
|
358
|
+
ambiguous precision.
|
|
359
|
+
|
|
360
|
+
:param market_details: A CCXT market dict.
|
|
361
|
+
:return: The tick size as a positive float.
|
|
362
|
+
"""
|
|
363
|
+
import ccxt
|
|
364
|
+
|
|
365
|
+
precision_price = market_details.get('precision', {}).get('price')
|
|
366
|
+
mode = self._client.precisionMode
|
|
367
|
+
|
|
368
|
+
if precision_price is not None:
|
|
369
|
+
if mode == ccxt.TICK_SIZE:
|
|
370
|
+
return float(precision_price)
|
|
371
|
+
if mode == ccxt.DECIMAL_PLACES:
|
|
372
|
+
return 10.0 ** -int(precision_price)
|
|
373
|
+
# SIGNIFICANT_DIGITS: not a fixed tick -- fall through to raw values.
|
|
374
|
+
|
|
375
|
+
for raw in (market_details.get('info', {}).get('tickSize'),
|
|
376
|
+
market_details.get('limits', {}).get('price', {}).get('min')):
|
|
377
|
+
if raw is None:
|
|
378
|
+
continue
|
|
379
|
+
try:
|
|
380
|
+
tick = float(raw)
|
|
381
|
+
except (TypeError, ValueError):
|
|
382
|
+
continue
|
|
383
|
+
if tick > 0.0:
|
|
384
|
+
return tick
|
|
385
|
+
|
|
386
|
+
import logging
|
|
387
|
+
logging.getLogger(__name__).warning(
|
|
388
|
+
"%s: no usable price precision for %r; defaulting tick size to %g",
|
|
389
|
+
self._client.id, self.symbol, _DEFAULT_MINTICK,
|
|
390
|
+
)
|
|
391
|
+
return _DEFAULT_MINTICK
|
|
392
|
+
|
|
393
|
+
def _derive_qty_step(self, market_details: dict) -> float:
|
|
394
|
+
"""Derive the minimum order quantity step (``mincontract``) for a market.
|
|
395
|
+
|
|
396
|
+
Mirrors :meth:`_derive_mintick` on the amount axis: with ``TICK_SIZE``
|
|
397
|
+
precision ``precision.amount`` already *is* the step, with
|
|
398
|
+
``DECIMAL_PLACES`` it is a decimal-place count, and
|
|
399
|
+
``SIGNIFICANT_DIGITS`` cannot map to a fixed step. The raw fallback is
|
|
400
|
+
``limits.amount.min`` — TV defines ``mincontract`` as the smallest
|
|
401
|
+
tradable amount, which on most exchanges equals the amount step (e.g.
|
|
402
|
+
Binance ``LOT_SIZE`` ``stepSize`` / ``minQty``).
|
|
403
|
+
|
|
404
|
+
:param market_details: A CCXT market dict.
|
|
405
|
+
:return: The quantity step, or ``0.0`` when the exchange does not
|
|
406
|
+
expose one (the symbol info chain then falls back to volume
|
|
407
|
+
analysis / heuristics).
|
|
408
|
+
"""
|
|
409
|
+
import ccxt
|
|
410
|
+
|
|
411
|
+
precision_amount = market_details.get('precision', {}).get('amount')
|
|
412
|
+
mode = self._client.precisionMode
|
|
413
|
+
|
|
414
|
+
if precision_amount is not None:
|
|
415
|
+
if mode == ccxt.TICK_SIZE:
|
|
416
|
+
return float(precision_amount)
|
|
417
|
+
if mode == ccxt.DECIMAL_PLACES:
|
|
418
|
+
return 10.0 ** -int(precision_amount)
|
|
419
|
+
# SIGNIFICANT_DIGITS: not a fixed step -- fall through to raw values.
|
|
420
|
+
|
|
421
|
+
raw = market_details.get('limits', {}).get('amount', {}).get('min')
|
|
422
|
+
try:
|
|
423
|
+
step = float(raw)
|
|
424
|
+
except (TypeError, ValueError):
|
|
425
|
+
return 0.0
|
|
426
|
+
return step if step > 0.0 else 0.0
|
|
427
|
+
|
|
428
|
+
@override
|
|
429
|
+
def update_symbol_info(self) -> SymInfo:
|
|
430
|
+
"""Update symbol info from the exchange."""
|
|
431
|
+
import ccxt
|
|
432
|
+
try:
|
|
433
|
+
self._client.load_markets()
|
|
434
|
+
except ccxt.BaseError as exc:
|
|
435
|
+
raise CCXTError(f"{self._client.id}: {exc}") from exc
|
|
436
|
+
assert self._client.markets
|
|
437
|
+
try:
|
|
438
|
+
market_details = self._client.markets[self.symbol]
|
|
439
|
+
except KeyError:
|
|
440
|
+
raise CCXTError(
|
|
441
|
+
f"Unknown symbol {self.symbol!r} on {self._client.id}."
|
|
442
|
+
) from None
|
|
443
|
+
|
|
444
|
+
opening_hours, session_starts, session_ends = self._create_24_7_sessions()
|
|
445
|
+
|
|
446
|
+
# Derive pricescale from the tick size. ``round_to_mintick`` divides by
|
|
447
|
+
# pricescale directly and ignores minmove, so the tick must be an
|
|
448
|
+
# integer reciprocal and minmove must stay 1.
|
|
449
|
+
mintick = self._derive_mintick(market_details)
|
|
450
|
+
pricescale = max(1, int(round(1.0 / mintick)))
|
|
451
|
+
if abs(mintick * pricescale - 1.0) > 1e-9:
|
|
452
|
+
raise CCXTError(
|
|
453
|
+
f"{self._client.id}: price tick {mintick} for {self.symbol!r} "
|
|
454
|
+
f"is not an integer reciprocal; cannot derive pricescale."
|
|
455
|
+
)
|
|
456
|
+
minmove = 1
|
|
457
|
+
|
|
458
|
+
try:
|
|
459
|
+
ticker = market_details['info']['symbol']
|
|
460
|
+
except KeyError:
|
|
461
|
+
try:
|
|
462
|
+
ticker = market_details['symbol']
|
|
463
|
+
except KeyError:
|
|
464
|
+
ticker = market_details['id']
|
|
465
|
+
|
|
466
|
+
assert self._client.id
|
|
467
|
+
return SymInfo(
|
|
468
|
+
prefix=self._client.id.upper(),
|
|
469
|
+
description=f"{market_details['base']} / {market_details['quote']} "
|
|
470
|
+
f"{add_space_before_uppercase(market_details['info'].get('contractType', 'Spot'))}",
|
|
471
|
+
ticker=ticker,
|
|
472
|
+
currency=market_details['quote'],
|
|
473
|
+
basecurrency=market_details['base'],
|
|
474
|
+
period=self.timeframe or "1D",
|
|
475
|
+
type="crypto",
|
|
476
|
+
mintick=mintick,
|
|
477
|
+
pricescale=pricescale,
|
|
478
|
+
minmove=minmove,
|
|
479
|
+
pointvalue=market_details.get('contractSize') or 1.0,
|
|
480
|
+
mincontract=self._derive_qty_step(market_details),
|
|
481
|
+
timezone=self.timezone,
|
|
482
|
+
opening_hours=opening_hours,
|
|
483
|
+
session_starts=session_starts,
|
|
484
|
+
session_ends=session_ends,
|
|
485
|
+
taker_fee=market_details.get('taker'),
|
|
486
|
+
maker_fee=market_details.get('maker'),
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
@override
|
|
490
|
+
def download_ohlcv(self, time_from: datetime, time_to: datetime,
|
|
491
|
+
on_progress: Callable[[datetime], None] | None = None,
|
|
492
|
+
limit: int | None = None, with_extra: bool = False):
|
|
493
|
+
"""
|
|
494
|
+
Download OHLCV data.
|
|
495
|
+
|
|
496
|
+
:param time_from: The start time.
|
|
497
|
+
:param time_to: The end time.
|
|
498
|
+
:param on_progress: Optional callback to call on progress.
|
|
499
|
+
:param limit: Override the automatic chunk size.
|
|
500
|
+
:param with_extra: Ignored; CCXT OHLCV has no extra per-bar fields.
|
|
501
|
+
"""
|
|
502
|
+
import ccxt
|
|
503
|
+
|
|
504
|
+
assert self.symbol is not None
|
|
505
|
+
assert self.xchg_timeframe is not None
|
|
506
|
+
|
|
507
|
+
tf: datetime = time_from.replace(tzinfo=None)
|
|
508
|
+
tt: datetime = time_to.replace(tzinfo=None)
|
|
509
|
+
|
|
510
|
+
if limit is None:
|
|
511
|
+
assert self._client.id
|
|
512
|
+
limit_config = _KNOWN_LIMITS.get(self._client.id, 100)
|
|
513
|
+
|
|
514
|
+
if isinstance(limit_config, dict):
|
|
515
|
+
limit = limit_config.get(self.xchg_timeframe, limit_config.get('default', 100))
|
|
516
|
+
else:
|
|
517
|
+
limit = limit_config
|
|
518
|
+
|
|
519
|
+
try:
|
|
520
|
+
while tf < tt:
|
|
521
|
+
if on_progress:
|
|
522
|
+
on_progress(tf)
|
|
523
|
+
|
|
524
|
+
try:
|
|
525
|
+
res: list = self._client.fetch_ohlcv(
|
|
526
|
+
symbol=self.symbol,
|
|
527
|
+
limit=limit,
|
|
528
|
+
timeframe=self.xchg_timeframe,
|
|
529
|
+
since=self._client.parse8601(tf.isoformat())
|
|
530
|
+
)
|
|
531
|
+
except ccxt.BaseError as exc:
|
|
532
|
+
raise CCXTError(f"{self._client.id}: {exc}") from exc
|
|
533
|
+
|
|
534
|
+
if not res:
|
|
535
|
+
tf += timedelta(days=1)
|
|
536
|
+
|
|
537
|
+
for r in res:
|
|
538
|
+
t = int(r[0] / 1000)
|
|
539
|
+
dt = datetime.fromtimestamp(t, UTC).replace(tzinfo=None)
|
|
540
|
+
if dt > tt:
|
|
541
|
+
raise StopIteration
|
|
542
|
+
|
|
543
|
+
ohlcv = OHLCV(
|
|
544
|
+
timestamp=t,
|
|
545
|
+
open=float(r[1]),
|
|
546
|
+
high=float(r[2]),
|
|
547
|
+
low=float(r[3]),
|
|
548
|
+
close=float(r[4]),
|
|
549
|
+
volume=float(r[5]),
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
self.save_ohlcv_data(ohlcv)
|
|
553
|
+
tf = dt + timedelta(minutes=1)
|
|
554
|
+
|
|
555
|
+
except StopIteration:
|
|
556
|
+
pass
|
|
557
|
+
|
|
558
|
+
if on_progress:
|
|
559
|
+
on_progress(tt)
|
|
560
|
+
|
|
561
|
+
# --- LiveProviderPlugin methods ---
|
|
562
|
+
|
|
563
|
+
@override
|
|
564
|
+
async def connect(self) -> None:
|
|
565
|
+
"""Establish async CCXT Pro connection for live OHLCV streaming."""
|
|
566
|
+
try:
|
|
567
|
+
import ccxt.pro as ccxtpro
|
|
568
|
+
except ImportError:
|
|
569
|
+
raise ImportError(
|
|
570
|
+
"CCXT Pro is required for live data. Install it with: pip install ccxt"
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
# ``connect()`` runs on every reconnect too, so the candle-close
|
|
574
|
+
# tracker must be wiped: a leftover ``_last_bar_*`` from the dead
|
|
575
|
+
# websocket would make the first ``watch_ohlcv`` after a forced
|
|
576
|
+
# reconnect (e.g. the stale-feed watchdog) emit that pre-outage
|
|
577
|
+
# partial bar as a confirmed close once a new-timestamp candle
|
|
578
|
+
# arrives.
|
|
579
|
+
self._last_bar_timestamp = None
|
|
580
|
+
self._last_bar_ohlcv = None
|
|
581
|
+
|
|
582
|
+
exchange_name = self._client.id
|
|
583
|
+
|
|
584
|
+
exchange_config: dict[str, Any] = {'enableRateLimit': True}
|
|
585
|
+
exchange_config.update(self._exchange_config)
|
|
586
|
+
|
|
587
|
+
if self.config:
|
|
588
|
+
exchange_config.update({
|
|
589
|
+
k: v for k, v in vars(self.config).items()
|
|
590
|
+
if v and k not in _PYNECORE_ONLY_CONFIG_KEYS
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
self._async_client = getattr(ccxtpro, exchange_name)(exchange_config)
|
|
594
|
+
|
|
595
|
+
if self.config and self.config.sandbox:
|
|
596
|
+
try:
|
|
597
|
+
self._async_client.set_sandbox_mode(True)
|
|
598
|
+
except Exception as exc: # noqa: BLE001
|
|
599
|
+
import logging
|
|
600
|
+
logging.getLogger(__name__).warning(
|
|
601
|
+
"Exchange %r does not support sandbox mode: %s",
|
|
602
|
+
exchange_name, exc,
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
if self.config and self.config.default_type:
|
|
606
|
+
self._async_client.options = {
|
|
607
|
+
**getattr(self._async_client, 'options', {}),
|
|
608
|
+
'defaultType': self.config.default_type,
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
@override
|
|
612
|
+
async def disconnect(self) -> None:
|
|
613
|
+
"""Close the async CCXT connection."""
|
|
614
|
+
if hasattr(self, '_async_client') and self._async_client:
|
|
615
|
+
await self._async_client.close()
|
|
616
|
+
self._async_client = None
|
|
617
|
+
|
|
618
|
+
@property
|
|
619
|
+
@override
|
|
620
|
+
def is_connected(self) -> bool:
|
|
621
|
+
"""Whether the async CCXT connection is active."""
|
|
622
|
+
return hasattr(self, '_async_client') and self._async_client is not None
|
|
623
|
+
|
|
624
|
+
@override
|
|
625
|
+
async def watch_ohlcv(self, symbol: str, timeframe: str) -> OHLCV:
|
|
626
|
+
"""
|
|
627
|
+
Wait for the next OHLCV update from the exchange websocket.
|
|
628
|
+
|
|
629
|
+
Detects bar closure by tracking timestamp changes: when a new bar
|
|
630
|
+
timestamp appears, the previous bar is returned as closed. Intra-bar
|
|
631
|
+
updates (same timestamp) are returned with ``is_closed=False``.
|
|
632
|
+
|
|
633
|
+
:param symbol: Symbol in CCXT format (e.g. "BTC/USDT:USDT").
|
|
634
|
+
:param timeframe: Timeframe in TradingView format (e.g. "1D", "1", "4H").
|
|
635
|
+
:return: OHLCV with ``is_closed=True`` for a final bar, ``False`` for intra-bar updates.
|
|
636
|
+
"""
|
|
637
|
+
xchg_tf = self.to_exchange_timeframe(timeframe)
|
|
638
|
+
|
|
639
|
+
while True:
|
|
640
|
+
candles = await self._async_client.watch_ohlcv(symbol, xchg_tf)
|
|
641
|
+
last = candles[-1]
|
|
642
|
+
timestamp = int(last[0] / 1000)
|
|
643
|
+
|
|
644
|
+
current_ohlcv = OHLCV(
|
|
645
|
+
timestamp=timestamp,
|
|
646
|
+
open=float(last[1]),
|
|
647
|
+
high=float(last[2]),
|
|
648
|
+
low=float(last[3]),
|
|
649
|
+
close=float(last[4]),
|
|
650
|
+
volume=float(last[5]),
|
|
651
|
+
is_closed=False,
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
if (self._last_bar_timestamp is not None
|
|
655
|
+
and timestamp != self._last_bar_timestamp):
|
|
656
|
+
assert self._last_bar_ohlcv is not None
|
|
657
|
+
closed_bar = self._last_bar_ohlcv._replace(is_closed=True)
|
|
658
|
+
self._last_bar_timestamp = timestamp
|
|
659
|
+
self._last_bar_ohlcv = current_ohlcv
|
|
660
|
+
return closed_bar
|
|
661
|
+
|
|
662
|
+
self._last_bar_timestamp = timestamp
|
|
663
|
+
self._last_bar_ohlcv = current_ohlcv
|
|
664
|
+
return current_ohlcv
|