消灭更多bug,修改K线颜色,macd颜色
This commit is contained in:
+70
-30
@@ -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
@@ -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
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user