添加断线重连
This commit is contained in:
@@ -3,19 +3,7 @@
|
|||||||
"symbols": [
|
"symbols": [
|
||||||
"BTC/USDT:USDT",
|
"BTC/USDT:USDT",
|
||||||
"ETH/USDT:USDT",
|
"ETH/USDT:USDT",
|
||||||
"SOL/USDT:USDT",
|
"SOL/USDT:USDT"
|
||||||
"WIF/USDT:USDT",
|
|
||||||
"LINK/USDT:USDT",
|
|
||||||
"ORDI/USDT:USDT",
|
|
||||||
"AAVE/USDT:USDT",
|
|
||||||
"REEF/USDT:USDT",
|
|
||||||
"1INCH/USDT:USDT",
|
|
||||||
"DOGE/USDT:USDT",
|
|
||||||
"TON/USDT:USDT",
|
|
||||||
"UNI/USDT:USDT",
|
|
||||||
"XRP/USDT:USDT",
|
|
||||||
"SUN/USDT:USDT",
|
|
||||||
"SUI/USDT:USDT"
|
|
||||||
],
|
],
|
||||||
"start_time": "2025-01-01T00:00:00Z",
|
"start_time": "2025-01-01T00:00:00Z",
|
||||||
"timeframes": ["1m", "1h", "1d", "1w"],
|
"timeframes": ["1m", "1h", "1d", "1w"],
|
||||||
|
|||||||
+130
-4
@@ -143,6 +143,12 @@ class DataProvider:
|
|||||||
self._stop_event = threading.Event()
|
self._stop_event = threading.Event()
|
||||||
self._fetch_thread: Optional[threading.Thread] = None
|
self._fetch_thread: Optional[threading.Thread] = None
|
||||||
self._persist_thread: Optional[threading.Thread] = None
|
self._persist_thread: Optional[threading.Thread] = None
|
||||||
|
# 记录断线后需要从哪个 since 重新拉取(symbol -> timeframe -> since_ms)
|
||||||
|
self._resume_since: Dict[str, Dict[str, int]] = {}
|
||||||
|
# 恢复点持久化文件
|
||||||
|
self._resume_file: Path = self.data_dir / "resume_since.json"
|
||||||
|
# 尝试加载历史恢复点
|
||||||
|
self._load_resume_since()
|
||||||
|
|
||||||
def _load_config(self) -> Dict[str, object]:
|
def _load_config(self) -> Dict[str, object]:
|
||||||
if not self.config_path.exists():
|
if not self.config_path.exists():
|
||||||
@@ -330,6 +336,95 @@ class DataProvider:
|
|||||||
def resample_df(self, df: pd.DataFrame, interval: int) -> pd.DataFrame:
|
def resample_df(self, df: pd.DataFrame, interval: int) -> pd.DataFrame:
|
||||||
return resample_to_interval(df, interval)
|
return resample_to_interval(df, interval)
|
||||||
|
|
||||||
|
def _save_resume_since(self) -> None:
|
||||||
|
path = self._resume_file
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
tmp_path = path.with_suffix(path.suffix + ".tmp")
|
||||||
|
with self._lock:
|
||||||
|
snapshot = {
|
||||||
|
symbol: {tf: int(since) for tf, since in tf_map.items()}
|
||||||
|
for symbol, tf_map in self._resume_since.items()
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
with tmp_path.open("w", encoding="utf-8") as fp:
|
||||||
|
json.dump(snapshot, fp, ensure_ascii=False, separators=(",", ":"))
|
||||||
|
os.replace(tmp_path, path)
|
||||||
|
finally:
|
||||||
|
if tmp_path.exists():
|
||||||
|
try:
|
||||||
|
tmp_path.unlink()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
logger.debug("恢复点已保存到磁盘: %s", path)
|
||||||
|
|
||||||
|
def _load_resume_since(self) -> None:
|
||||||
|
path = self._resume_file
|
||||||
|
if not path.exists():
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
with path.open("r", encoding="utf-8") as fp:
|
||||||
|
raw = json.load(fp)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("恢复点文件读取失败,忽略: %s (%s)", path, exc)
|
||||||
|
return
|
||||||
|
if not isinstance(raw, dict):
|
||||||
|
logger.warning("恢复点文件格式错误,忽略: %s", path)
|
||||||
|
return
|
||||||
|
loaded: Dict[str, Dict[str, int]] = {}
|
||||||
|
for symbol, tf_map in raw.items():
|
||||||
|
if not isinstance(tf_map, dict):
|
||||||
|
continue
|
||||||
|
per_symbol: Dict[str, int] = {}
|
||||||
|
for timeframe, since in tf_map.items():
|
||||||
|
try:
|
||||||
|
per_symbol[str(timeframe)] = int(since)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if per_symbol:
|
||||||
|
loaded[str(symbol)] = per_symbol
|
||||||
|
if not loaded:
|
||||||
|
return
|
||||||
|
with self._lock:
|
||||||
|
# 合并为更早的 since,避免遗漏
|
||||||
|
for symbol, tf_map in loaded.items():
|
||||||
|
cur = self._resume_since.setdefault(symbol, {})
|
||||||
|
for timeframe, since in tf_map.items():
|
||||||
|
prev = cur.get(timeframe)
|
||||||
|
if prev is None or since < prev:
|
||||||
|
cur[timeframe] = since
|
||||||
|
logger.info("已加载恢复点: %s", path)
|
||||||
|
|
||||||
|
def _get_resume_since(self, symbol: str, timeframe: str) -> Optional[int]:
|
||||||
|
with self._lock:
|
||||||
|
return self._resume_since.get(symbol, {}).get(timeframe)
|
||||||
|
|
||||||
|
def _set_resume_since(self, symbol: str, timeframe: str, since_ms: int) -> None:
|
||||||
|
with self._lock:
|
||||||
|
per_symbol = self._resume_since.setdefault(symbol, {})
|
||||||
|
prev = per_symbol.get(timeframe)
|
||||||
|
# 取更早的 since,避免跳过数据
|
||||||
|
if prev is None or since_ms < prev:
|
||||||
|
per_symbol[timeframe] = since_ms
|
||||||
|
logger.warning(
|
||||||
|
"记录断线恢复点: %s %s since=%s (%s)",
|
||||||
|
symbol,
|
||||||
|
timeframe,
|
||||||
|
since_ms,
|
||||||
|
to_utc_iso(since_ms),
|
||||||
|
)
|
||||||
|
# 同步写盘
|
||||||
|
self._save_resume_since()
|
||||||
|
|
||||||
|
def _clear_resume_since(self, symbol: str, timeframe: str) -> None:
|
||||||
|
with self._lock:
|
||||||
|
if symbol in self._resume_since and timeframe in self._resume_since[symbol]:
|
||||||
|
del self._resume_since[symbol][timeframe]
|
||||||
|
if not self._resume_since[symbol]:
|
||||||
|
del self._resume_since[symbol]
|
||||||
|
logger.info("清除断线恢复点: %s %s", symbol, timeframe)
|
||||||
|
# 同步写盘
|
||||||
|
self._save_resume_since()
|
||||||
|
|
||||||
def start_background_workers(self) -> None:
|
def start_background_workers(self) -> None:
|
||||||
if self._fetch_thread and self._fetch_thread.is_alive():
|
if self._fetch_thread and self._fetch_thread.is_alive():
|
||||||
return
|
return
|
||||||
@@ -353,21 +448,50 @@ class DataProvider:
|
|||||||
for symbol in self.symbols:
|
for symbol in self.symbols:
|
||||||
for timeframe in self.timeframes:
|
for timeframe in self.timeframes:
|
||||||
try:
|
try:
|
||||||
|
# 若存在断线恢复点,则优先从该 since 补齐历史数据
|
||||||
|
resume_since = self._get_resume_since(symbol, timeframe)
|
||||||
|
if resume_since is not None:
|
||||||
|
logger.info(
|
||||||
|
"开始断线后补数: %s %s since=%s (%s)",
|
||||||
|
symbol,
|
||||||
|
timeframe,
|
||||||
|
resume_since,
|
||||||
|
to_utc_iso(resume_since),
|
||||||
|
)
|
||||||
|
history = self._fetch_history(symbol, timeframe, resume_since)
|
||||||
|
with self._lock:
|
||||||
|
current = self.data.setdefault(symbol, {}).get(timeframe, [])
|
||||||
|
merged = self._merge_candles(timeframe, current, history)
|
||||||
|
self.data[symbol][timeframe] = merged
|
||||||
|
self._clear_resume_since(symbol, timeframe)
|
||||||
|
else:
|
||||||
|
# 正常增量获取最近若干根K线
|
||||||
candles = self.exchange.fetch_ohlcv(
|
candles = self.exchange.fetch_ohlcv(
|
||||||
symbol,
|
symbol,
|
||||||
timeframe=timeframe,
|
timeframe=timeframe,
|
||||||
limit=RECENT_CANDLE_LIMIT,
|
limit=RECENT_CANDLE_LIMIT,
|
||||||
)
|
)
|
||||||
except ccxt.BaseError as exc:
|
|
||||||
logger.error("更新最新K线失败 (%s %s): %s", symbol, timeframe, exc)
|
|
||||||
time.sleep(2)
|
|
||||||
continue
|
|
||||||
if not candles:
|
if not candles:
|
||||||
continue
|
continue
|
||||||
with self._lock:
|
with self._lock:
|
||||||
current = self.data.setdefault(symbol, {}).get(timeframe, [])
|
current = self.data.setdefault(symbol, {}).get(timeframe, [])
|
||||||
merged = self._merge_candles(timeframe, current, candles)
|
merged = self._merge_candles(timeframe, current, candles)
|
||||||
self.data[symbol][timeframe] = merged
|
self.data[symbol][timeframe] = merged
|
||||||
|
except ccxt.BaseError as exc:
|
||||||
|
logger.error("更新最新K线失败 (%s %s): %s", symbol, timeframe, exc)
|
||||||
|
# 记录应当从何时恢复拉取,避免重连后从当前时间开始导致丢K
|
||||||
|
with self._lock:
|
||||||
|
current = self.data.get(symbol, {}).get(timeframe, [])
|
||||||
|
if current:
|
||||||
|
last_ts = int(current[-1]["timestamp"])
|
||||||
|
else:
|
||||||
|
last_ts = self.start_time_ms
|
||||||
|
tf_ms = TIMEFRAME_TO_MS[timeframe]
|
||||||
|
# 回退一个周期,确保包含可能未完全收盘的K线,去重由 _merge_candles 处理
|
||||||
|
since_ms = max(self.start_time_ms, last_ts - tf_ms)
|
||||||
|
self._set_resume_since(symbol, timeframe, since_ms)
|
||||||
|
time.sleep(2)
|
||||||
|
continue
|
||||||
if self._stop_event.wait(RECENT_FETCH_INTERVAL):
|
if self._stop_event.wait(RECENT_FETCH_INTERVAL):
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -386,6 +510,8 @@ class DataProvider:
|
|||||||
for symbol, tf_map in snapshot.items():
|
for symbol, tf_map in snapshot.items():
|
||||||
for timeframe, data in tf_map.items():
|
for timeframe, data in tf_map.items():
|
||||||
self._write_to_disk(symbol, timeframe, data)
|
self._write_to_disk(symbol, timeframe, data)
|
||||||
|
# 周期性也保存一次恢复点,保证一致性
|
||||||
|
self._save_resume_since()
|
||||||
|
|
||||||
def is_ready(self) -> bool:
|
def is_ready(self) -> bool:
|
||||||
return self._ready.is_set()
|
return self._ready.is_set()
|
||||||
|
|||||||
Reference in New Issue
Block a user