添加断线重连

This commit is contained in:
Porter
2025-11-16 11:59:52 +08:00
parent af255ee70d
commit 03c5148c29
3 changed files with 138 additions and 24 deletions
Vendored
BIN
View File
Binary file not shown.
+1 -13
View File
@@ -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
View File
@@ -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()