消灭更多bug,修改K线颜色,macd颜色

This commit is contained in:
jackyu66git
2025-11-13 20:34:36 +08:00
parent 5ef10c2694
commit 3401cb56bf
3 changed files with 180 additions and 62 deletions
+70 -30
View File
@@ -942,16 +942,31 @@ async def rest_poll_loop(
"mode": "rest_poll", "mode": "rest_poll",
}, },
) )
await process_candles( try:
symbol, await process_candles(
timeframe, symbol,
candles, timeframe,
derived_timeframes, candles,
tf_ms, derived_timeframes,
finalized=True, tf_ms,
allow_verification=True, finalized=True,
closed_flags=closed_flags, allow_verification=True,
) closed_flags=closed_flags,
)
except asyncio.CancelledError:
raise
except Exception as exc:
logger.exception(
"处理基础周期 K 线失败 [%s %s]",
symbol,
timeframe,
)
state = fetch_states.get(state_key)
if state:
state.last_error = str(exc)
state.consecutive_errors += 1
await asyncio.sleep(interval)
continue
await asyncio.sleep(interval) await asyncio.sleep(interval)
except asyncio.CancelledError: except asyncio.CancelledError:
raise raise
@@ -1028,7 +1043,7 @@ def build_exchange():
async def fetch_loop(symbol: str, timeframe: str): async def fetch_loop(symbol: str, timeframe: str):
"""初次通过 REST 补齐历史,随后转入 Binance WebSocket 拉取增量。""" """初次通过 REST 补齐历史,随后持续轮询/流式拉取增量。"""
derived_timeframes = AGGREGATION_TARGETS.get(timeframe, []) derived_timeframes = AGGREGATION_TARGETS.get(timeframe, [])
global RESAMPLE_WARNING_EMITTED global RESAMPLE_WARNING_EMITTED
if derived_timeframes and not RESAMPLE_AVAILABLE and not RESAMPLE_WARNING_EMITTED: if derived_timeframes and not RESAMPLE_AVAILABLE and not RESAMPLE_WARNING_EMITTED:
@@ -1056,27 +1071,52 @@ async def fetch_loop(symbol: str, timeframe: str):
extra={"symbol": symbol, "timeframe": timeframe, "timestamp": last_ts, "count": initial_count}, extra={"symbol": symbol, "timeframe": timeframe, "timestamp": last_ts, "count": initial_count},
) )
start_since = parse_start_from_ms(START_FROM) start_from = parse_start_from_ms(START_FROM)
if last_ts is not None: backoff = 1.0
rewind_since = max(0, last_ts - tf_ms)
initial_since = max(start_since, rewind_since)
else:
initial_since = start_since
logger.info("启动拉取任务", extra={"symbol": symbol, "timeframe": timeframe})
try: try:
await rest_catchup(symbol, timeframe, derived_timeframes, tf_ms, initial_since) while True:
if WS_ENABLED: state = fetch_states[state_key]
await stream_loop(symbol, timeframe, derived_timeframes, tf_ms) state.started_at = datetime.utcnow()
else: if state.last_candle_ts is not None:
await rest_poll_loop(symbol, timeframe, derived_timeframes, tf_ms) rewind_since = max(0, state.last_candle_ts - tf_ms)
except asyncio.CancelledError: initial_since = max(start_from, rewind_since)
logger.info("取消拉取任务", extra={"symbol": symbol, "timeframe": timeframe}) else:
state = fetch_states.get(state_key) initial_since = start_from
if state:
state.last_error = "cancelled" logger.info(
raise "启动拉取任务 [%s %s] since=%s",
symbol,
timeframe,
initial_since,
)
try:
await rest_catchup(symbol, timeframe, derived_timeframes, tf_ms, initial_since)
if WS_ENABLED:
await stream_loop(symbol, timeframe, derived_timeframes, tf_ms)
else:
await rest_poll_loop(symbol, timeframe, derived_timeframes, tf_ms)
except asyncio.CancelledError:
logger.info("取消拉取任务 [%s %s]", symbol, timeframe)
state.last_error = "cancelled"
raise
except Exception as exc:
state.last_error = str(exc)
state.consecutive_errors += 1
logger.exception(
"拉取任务异常 [%s %s]%.1f 秒后重启",
symbol,
timeframe,
backoff,
)
await asyncio.sleep(backoff)
backoff = min(backoff * BACKOFF_BASE, BACKOFF_MAX)
continue
else:
backoff = 1.0
logger.warning("拉取循环提前结束 [%s %s]1 秒后重启", symbol, timeframe)
await asyncio.sleep(1.0)
finally: finally:
logger.info("拉取任务退出", extra={"symbol": symbol, "timeframe": timeframe}) logger.info("拉取任务退出", extra={"symbol": symbol, "timeframe": timeframe})
+84 -6
View File
@@ -1,10 +1,15 @@
import logging
import os import os
import shutil
import threading import threading
from datetime import datetime
from typing import List, Optional from typing import List, Optional
import pandas as pd import pandas as pd
import pyarrow.dataset as ds import pyarrow.dataset as ds
logger = logging.getLogger("datasvc")
_lock = threading.Lock() _lock = threading.Lock()
@@ -24,7 +29,20 @@ def read_candles(base_dir: str, symbol: str, timeframe: str, start: Optional[int
p = _path(base_dir, symbol, timeframe) p = _path(base_dir, symbol, timeframe)
if not os.path.exists(p): if not os.path.exists(p):
return pd.DataFrame(columns=["timestamp", "open", "high", "low", "close", "volume"]) # empty return pd.DataFrame(columns=["timestamp", "open", "high", "low", "close", "volume"]) # empty
df = pd.read_parquet(p) try:
df = pd.read_parquet(p)
except Exception as exc:
with _lock:
backup = _backup_corrupted_file(p)
extra = f",已备份至 {backup}" if backup else ""
logger.warning(
"读取缓存失败,将视为空数据 [%s %s]%s%s",
symbol,
timeframe,
extra,
exc,
)
return pd.DataFrame(columns=["timestamp", "open", "high", "low", "close", "volume"])
if start is not None: if start is not None:
df = df[df["timestamp"] >= int(start)] df = df[df["timestamp"] >= int(start)]
if end is not None: if end is not None:
@@ -38,19 +56,53 @@ def upsert_candles(base_dir: str, symbol: str, timeframe: str, candles: List[Lis
new_df = pd.DataFrame(candles, columns=["timestamp", "open", "high", "low", "close", "volume"]) new_df = pd.DataFrame(candles, columns=["timestamp", "open", "high", "low", "close", "volume"])
with _lock: with _lock:
if os.path.exists(p): if os.path.exists(p):
old = pd.read_parquet(p) try:
old = pd.read_parquet(p)
except Exception as exc:
backup = _backup_corrupted_file(p)
extra = f",已备份至 {backup}" if backup else ""
logger.warning(
"读取缓存失败,准备重建文件 [%s %s]%s%s",
symbol,
timeframe,
extra,
exc,
)
old = pd.DataFrame(columns=["timestamp", "open", "high", "low", "close", "volume"])
merged = pd.concat([old, new_df], ignore_index=True) merged = pd.concat([old, new_df], ignore_index=True)
merged = merged.drop_duplicates(subset=["timestamp"], keep="last").sort_values("timestamp") merged = merged.drop_duplicates(subset=["timestamp"], keep="last").sort_values("timestamp")
else: else:
merged = new_df.sort_values("timestamp") merged = new_df.sort_values("timestamp")
merged.to_parquet(p, index=False) temp_path = f"{p}.tmp"
try:
merged.to_parquet(temp_path, index=False)
os.replace(temp_path, p)
finally:
if os.path.exists(temp_path):
try:
os.remove(temp_path)
except OSError:
pass
def get_last_timestamp(base_dir: str, symbol: str, timeframe: str) -> Optional[int]: def get_last_timestamp(base_dir: str, symbol: str, timeframe: str) -> Optional[int]:
p = _path(base_dir, symbol, timeframe) p = _path(base_dir, symbol, timeframe)
if not os.path.exists(p): if not os.path.exists(p):
return None return None
df = pd.read_parquet(p) try:
df = pd.read_parquet(p)
except Exception as exc:
with _lock:
backup = _backup_corrupted_file(p)
extra = f",已备份至 {backup}" if backup else ""
logger.warning(
"获取最后时间戳失败 [%s %s]%s%s",
symbol,
timeframe,
extra,
exc,
)
return None
if df.empty: if df.empty:
return None return None
return int(df["timestamp"].iloc[-1]) return int(df["timestamp"].iloc[-1])
@@ -60,10 +112,36 @@ def read_candle_exact(base_dir: str, symbol: str, timeframe: str, timestamp: int
p = _path(base_dir, symbol, timeframe) p = _path(base_dir, symbol, timeframe)
if not os.path.exists(p): if not os.path.exists(p):
return pd.DataFrame(columns=["timestamp", "open", "high", "low", "close", "volume"]) return pd.DataFrame(columns=["timestamp", "open", "high", "low", "close", "volume"])
dataset = ds.dataset(p, format="parquet") try:
table = dataset.to_table(filter=ds.field("timestamp") == int(timestamp)) dataset = ds.dataset(p, format="parquet")
table = dataset.to_table(filter=ds.field("timestamp") == int(timestamp))
except Exception as exc:
with _lock:
backup = _backup_corrupted_file(p)
extra = f",已备份至 {backup}" if backup else ""
logger.warning(
"读取指定时间 K 线失败 [%s %s]%s%s",
symbol,
timeframe,
extra,
exc,
)
return pd.DataFrame(columns=["timestamp", "open", "high", "low", "close", "volume"])
if table.num_rows == 0: if table.num_rows == 0:
return pd.DataFrame(columns=["timestamp", "open", "high", "low", "close", "volume"]) return pd.DataFrame(columns=["timestamp", "open", "high", "low", "close", "volume"])
return table.to_pandas() return table.to_pandas()
def _backup_corrupted_file(path: str) -> Optional[str]:
try:
if not os.path.exists(path):
return None
timestamp = datetime.utcnow().strftime("%Y%m%d%H%M%S")
backup_path = f"{path}.corrupted.{timestamp}"
shutil.move(path, backup_path)
return backup_path
except Exception as exc:
logger.warning("备份损坏文件失败 (%s)%s", path, exc)
return None
+26 -26
View File
@@ -17,7 +17,7 @@
<script defer src="{{ url_for('static', filename='js/charts.js') }}"></script> <script defer src="{{ url_for('static', filename='js/charts.js') }}"></script>
<!-- TradingView Widget END --> <!-- TradingView Widget END -->
<script> <script>
window.AVAILABLE_TIMEFRAMES = {{ timeframe_keys_json | safe }}; window.AVAILABLE_TIMEFRAMES = JSON.parse('{{ timeframe_keys_json | safe }}');
window.DEFAULT_MAIN_TIMEFRAME = "{{ default_main_timeframe }}"; window.DEFAULT_MAIN_TIMEFRAME = "{{ default_main_timeframe }}";
window.DEFAULT_ELEMENT_TIMEFRAME = "{{ default_element_timeframe }}"; window.DEFAULT_ELEMENT_TIMEFRAME = "{{ default_element_timeframe }}";
window.timeframeToMs = function(tf) { window.timeframeToMs = function(tf) {
@@ -2474,40 +2474,40 @@
if (klineType === 'candlestick') { if (klineType === 'candlestick') {
const series = mainChart.addCandlestickSeries({ const series = mainChart.addCandlestickSeries({
upColor: '#dc3545', upColor: '#28a745',
downColor: '#28a745', downColor: '#dc3545',
borderVisible: false, borderVisible: false,
wickUpColor: '#dc3545', wickUpColor: '#28a745',
wickDownColor: '#28a745', wickDownColor: '#dc3545',
}); });
series.setData(candles); series.setData(candles);
tvWidget.series.candleSeries = series; tvWidget.series.candleSeries = series;
} else if (klineType === 'renko') { } else if (klineType === 'renko') {
const series = mainChart.addCandlestickSeries({ const series = mainChart.addCandlestickSeries({
upColor: '#dc3545', upColor: '#28a745',
downColor: '#28a745', downColor: '#dc3545',
borderVisible: false, borderVisible: false,
wickUpColor: '#dc3545', wickUpColor: '#28a745',
wickDownColor: '#28a745', wickDownColor: '#dc3545',
}); });
const bricks = buildRenkoFromCandles(candles); const bricks = buildRenkoFromCandles(candles);
series.setData(bricks); series.setData(bricks);
tvWidget.series.renkoSeries = series; tvWidget.series.renkoSeries = series;
} else if (klineType === 'heikin') { } else if (klineType === 'heikin') {
const series = mainChart.addCandlestickSeries({ const series = mainChart.addCandlestickSeries({
upColor: '#dc3545', upColor: '#28a745',
downColor: '#28a745', downColor: '#dc3545',
borderVisible: false, borderVisible: false,
wickUpColor: '#dc3545', wickUpColor: '#28a745',
wickDownColor: '#28a745', wickDownColor: '#dc3545',
}); });
const hk = buildHeikinFromCandles(candles); const hk = buildHeikinFromCandles(candles);
series.setData(hk); series.setData(hk);
tvWidget.series.heikinSeries = series; tvWidget.series.heikinSeries = series;
} else if (klineType === 'bar') { } else if (klineType === 'bar') {
const series = mainChart.addBarSeries({ const series = mainChart.addBarSeries({
upColor: '#dc3545', upColor: '#28a745',
downColor: '#28a745', downColor: '#dc3545',
thinBars: false thinBars: false
}); });
series.setData(candles); series.setData(candles);
@@ -2549,11 +2549,11 @@
} else if (klineType === 'klc') { } else if (klineType === 'klc') {
// KLC显示模式 - 使用蜡烛线显示KLC数据 // KLC显示模式 - 使用蜡烛线显示KLC数据
const series = mainChart.addCandlestickSeries({ const series = mainChart.addCandlestickSeries({
upColor: '#dc3545', upColor: '#28a745',
downColor: '#28a745', downColor: '#dc3545',
borderVisible: false, borderVisible: false,
wickUpColor: '#dc3545', wickUpColor: '#28a745',
wickDownColor: '#28a745', wickDownColor: '#dc3545',
}); });
// 使用KLC数据创建蜡烛图 // 使用KLC数据创建蜡烛图
const klcCandles = buildKLCFromAnalysis(currentData); const klcCandles = buildKLCFromAnalysis(currentData);
@@ -2577,7 +2577,7 @@
return { return {
time: timestamp, time: timestamp,
value: parseFloat(kline.volume), value: parseFloat(kline.volume),
color: parseFloat(kline.close) >= parseFloat(kline.open) ? 'rgba(220, 53, 69, 0.5)' : 'rgba(40, 167, 69, 0.5)', color: parseFloat(kline.close) >= parseFloat(kline.open) ? 'rgba(40, 167, 69, 0.5)' : 'rgba(220, 53, 69, 0.5)',
}; };
}); });
@@ -2713,7 +2713,7 @@
histogramData.push({ histogramData.push({
time: timestamp, time: timestamp,
value: histValue, value: histValue,
color: histValue >= 0 ? 'rgba(220, 53, 69, 0.5)' : 'rgba(40, 167, 69, 0.5)' color: histValue >= 0 ? 'rgba(40, 167, 69, 0.5)' : 'rgba(220, 53, 69, 0.5)'
}); });
} }
} }
@@ -2829,7 +2829,7 @@
chanHistData.push({ chanHistData.push({
time: timestamp, time: timestamp,
value: macdDataSource.histogram[i], value: macdDataSource.histogram[i],
color: macdDataSource.histogram[i] >= 0 ? 'rgba(220, 53, 69, 0.5)' : 'rgba(40, 167, 69, 0.5)' color: macdDataSource.histogram[i] >= 0 ? 'rgba(40, 167, 69, 0.5)' : 'rgba(220, 53, 69, 0.5)'
}); });
} }
} }
@@ -6053,7 +6053,7 @@
return { return {
time: timestamp, time: timestamp,
value: parseFloat(kline.volume), value: parseFloat(kline.volume),
color: parseFloat(kline.close) >= parseFloat(kline.open) ? 'rgba(220, 53, 69, 0.5)' : 'rgba(40, 167, 69, 0.5)', color: parseFloat(kline.close) >= parseFloat(kline.open) ? 'rgba(40, 167, 69, 0.5)' : 'rgba(220, 53, 69, 0.5)',
}; };
}); });
} else if (currentData.kline_data && Array.isArray(currentData.kline_data)) { } else if (currentData.kline_data && Array.isArray(currentData.kline_data)) {
@@ -6062,7 +6062,7 @@
return { return {
time: timestamp, time: timestamp,
value: parseFloat(kline.volume), value: parseFloat(kline.volume),
color: parseFloat(kline.close) >= parseFloat(kline.open) ? 'rgba(220, 53, 69, 0.5)' : 'rgba(40, 167, 69, 0.5)', color: parseFloat(kline.close) >= parseFloat(kline.open) ? 'rgba(40, 167, 69, 0.5)' : 'rgba(220, 53, 69, 0.5)',
}; };
}); });
} }
@@ -6136,7 +6136,7 @@
histogramData.push({ histogramData.push({
time: timestamp, time: timestamp,
value: histValue, value: histValue,
color: histValue >= 0 ? 'rgba(220, 53, 69, 0.5)' : 'rgba(40, 167, 69, 0.5)' color: histValue >= 0 ? 'rgba(40, 167, 69, 0.5)' : 'rgba(220, 53, 69, 0.5)'
}); });
} }
} }
@@ -6160,7 +6160,7 @@
const timestamp = Math.floor(new Date(kline.date).getTime() / 1000); const timestamp = Math.floor(new Date(kline.date).getTime() / 1000);
chanMacdData.push({ time: timestamp, value: macdDataSource.macd[i] }); chanMacdData.push({ time: timestamp, value: macdDataSource.macd[i] });
chanSignalData.push({ time: timestamp, value: macdDataSource.signal[i] }); chanSignalData.push({ time: timestamp, value: macdDataSource.signal[i] });
chanHistData.push({ time: timestamp, value: macdDataSource.histogram[i], color: macdDataSource.histogram[i] >= 0 ? 'rgba(220, 53, 69, 0.5)' : 'rgba(40, 167, 69, 0.5)' }); chanHistData.push({ time: timestamp, value: macdDataSource.histogram[i], color: macdDataSource.histogram[i] >= 0 ? 'rgba(40, 167, 69, 0.5)' : 'rgba(220, 53, 69, 0.5)' });
} }
} }
if (chanMacdData.length > 0) { if (chanMacdData.length > 0) {