添加k线动能理论

This commit is contained in:
jackyu66git
2025-08-14 02:29:27 +08:00
parent 285f62f1ab
commit 15a3df55db
16 changed files with 1400 additions and 170 deletions
+497 -4
View File
@@ -1057,6 +1057,9 @@
<li class="nav-item" role="presentation">
<button class="nav-link" id="trade-points-tab" data-bs-toggle="tab" data-bs-target="#trade-points" type="button" role="tab">买卖点</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="trend-filter-tab" data-bs-toggle="tab" data-bs-target="#trend-filter" type="button" role="tab">趋势筛选(币对)</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="stock-filter-tab" data-bs-toggle="tab" data-bs-target="#stock-filter" type="button" role="tab">股票筛选</button>
</li>
@@ -1177,6 +1180,125 @@
</table>
</div>
</div>
<div class="tab-pane fade" id="trend-filter" role="tabpanel">
<div class="container-fluid">
<div class="row g-3 mb-2">
<div class="col-md-2">
<label class="form-label">K线周期</label>
<select id="trendTimeframe" class="form-select">
<option value="1m">1m</option>
<option value="5m">5m</option>
<option value="15m" selected>15m</option>
<option value="30m">30m</option>
<option value="1h">1h</option>
<option value="4h">4h</option>
<option value="1d">1d</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">方向</label>
<select id="trendDirection" class="form-select">
<option value="">全部</option>
<option value="bull">多头</option>
<option value="bear">空头</option>
<option value="sideways">盘整</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">阶段</label>
<select id="trendStage" class="form-select">
<option value="">全部</option>
<option value="early">初期</option>
<option value="mid">中期</option>
<option value="late">末期</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">强度 (0-100)</label>
<div class="d-flex align-items-center">
<input type="range" id="trendMinStrength" class="form-range me-2" min="0" max="100" value="30">
<input type="number" id="trendMinStrengthNum" class="form-control" style="width:90px" min="0" max="100" value="30">
</div>
</div>
<div class="col-md-3">
<label class="form-label">自选币对(逗号分隔)</label>
<input id="trendSymbols" class="form-control" placeholder="例如: BTC/USDT:USDT,ETH/USDT:USDT,可留空">
</div>
<div class="col-md-3">
<label class="form-label">开始时间</label>
<input type="datetime-local" id="trendStart" class="form-control">
</div>
<div class="col-md-3">
<label class="form-label">结束时间</label>
<input type="datetime-local" id="trendEnd" class="form-control">
</div>
<div class="col-md-2 d-flex align-items-end">
<button class="btn btn-primary w-100" id="btnTrendFilter">开始筛选</button>
</div>
</div>
<div class="table-container mb-3">
<div id="trendFilterStatus" class="alert alert-info py-2" style="display:none;">
<span class="loading-spinner"></span>
<span class="ms-2">正在筛选,请稍候...</span>
</div>
<table id="trendFilterTable" class="display compact" style="width:100%">
<thead>
<tr>
<th>交易对</th>
<th>时间</th>
<th>方向</th>
<th>阶段</th>
<th>强度</th>
<th>收盘</th>
<th>EMA5</th>
<th>EMA10</th>
<th>EMA24</th>
<th>EMA52</th>
<th>操作</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-header">
趋势详情
<span id="trendDetailStatus" class="text-muted ms-3" style="display:none;">
<span class="loading-spinner"></span>
<span class="ms-2">正在加载详情...</span>
</span>
</div>
<div class="card-body">
<div id="trendChartContainer" style="height:420px;"></div>
<div class="table-container mt-3">
<table id="trendDetailTable" class="display compact" style="width:100%">
<thead>
<tr>
<th>时间</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th>成交量</th>
<th>EMA5</th>
<th>EMA10</th>
<th>EMA24</th>
<th>EMA52</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="stock-filter" role="tabpanel">
<div class="container-fluid">
<div class="row mb-3">
@@ -1259,6 +1381,377 @@
<script>
let currentData = null;
const tables = {};
// ======= 趋势筛选(币对) =======
let trendTable = null;
let trendDetailTable = null;
let trendChart = null;
function initTrendTables() {
if (!trendTable) {
trendTable = $('#trendFilterTable').DataTable({
paging: true,
searching: false,
info: true,
order: [[4, 'desc']],
});
}
if (!trendDetailTable) {
trendDetailTable = $('#trendDetailTable').DataTable({
paging: true,
searching: false,
info: true,
order: [[0, 'desc']],
});
}
}
function bindTrendControls() {
// 双向绑定强度滑块与数字框
$('#trendMinStrength').on('input change', function(){
$('#trendMinStrengthNum').val($(this).val());
});
$('#trendMinStrengthNum').on('input change', function(){
let v = Math.max(0, Math.min(100, parseFloat($(this).val()||0)));
$(this).val(v);
$('#trendMinStrength').val(v);
});
// 周期变化时,自动填充当前时间回溯300根K线的时间范围
$('#trendTimeframe').on('change', function(){
const tf = $(this).val();
const tfToMs = {
'1m': 60*1000, '5m': 5*60*1000, '15m': 15*60*1000, '30m': 30*60*1000,
'1h': 60*60*1000, '4h': 4*60*60*1000, '1d': 24*60*60*1000
};
const step = tfToMs[tf] || (60*60*1000);
const now = new Date();
const endMs = now.getTime();
const startMs = endMs - 300 * step;
const toLocal = (ms) => new Date(ms - new Date(ms).getTimezoneOffset()*60000).toISOString().slice(0,16);
$('#trendEnd').val(toLocal(endMs));
$('#trendStart').val(toLocal(startMs));
});
$('#btnTrendFilter').on('click', async function(){
await runTrendFilter();
});
}
async function runTrendFilter() {
initTrendTables();
trendTable.clear().draw();
const timeframe = $('#trendTimeframe').val();
const direction = $('#trendDirection').val();
const stage = $('#trendStage').val();
const minStrength = $('#trendMinStrength').val();
const symbols = $('#trendSymbols').val();
let start = $('#trendStart').val();
let end = $('#trendEnd').val();
// 前端必须提供时间范围:若为空,自动以当前时间回溯300根
if (!start || !end) {
const tfToMs = {
'1m': 60*1000, '5m': 5*60*1000, '15m': 15*60*1000, '30m': 30*60*1000,
'1h': 60*60*1000, '4h': 4*60*60*1000, '1d': 24*60*60*1000
};
const step = tfToMs[timeframe] || (60*60*1000);
const now = Date.now();
const startMsAuto = now - 300 * step;
const toLocal = (ms) => new Date(ms - new Date(ms).getTimezoneOffset()*60000).toISOString().slice(0,16);
if (!end) $('#trendEnd').val(toLocal(now));
if (!start) $('#trendStart').val(toLocal(startMsAuto));
start = $('#trendStart').val();
end = $('#trendEnd').val();
}
let startMs = start ? new Date(start).getTime() : '';
let endMs = end ? new Date(end).getTime() : '';
const params = $.param({
timeframe: timeframe,
direction: direction || '',
stage: stage || '',
min_strength: minStrength,
symbols: symbols || '',
start_time: startMs || '',
end_time: endMs || ''
});
// 显示筛选状态
$('#trendFilterStatus').show();
try {
const res = await $.getJSON(`/api/trend_filter?${params}`);
// 初筛后端结果,再次用前端方向筛选(避免后端噪声)
const dirVal = $('#trendDirection').val();
const rows = (res.results || [])
.filter(r => {
if (!dirVal) return true;
return r.direction === dirVal;
})
.map(r => [
r.symbol,
new Date(r.time).toLocaleString('zh-CN', { timeZone: $('#timezone').val() || 'Asia/Shanghai' }),
r.direction === 'bull' ? '多头' : (r.direction === 'bear' ? '空头' : '盘整'),
r.stage === 'early' ? '初期' : (r.stage === 'mid' ? '中期' : '末期'),
r.strength,
r.close,
r.ema5,
r.ema10,
r.ema24,
r.ema52,
`<button class="btn btn-sm btn-outline-primary" data-symbol="${r.symbol}" data-timeframe="${timeframe}">查看</button>`
]);
trendTable.rows.add(rows).draw();
// 绑定查看按钮
$('#trendFilterTable').off('click', 'button').on('click', 'button', function(){
const sym = $(this).data('symbol');
const tf = $(this).data('timeframe');
loadTrendDetail(sym, tf, startMs, endMs);
});
// 精细化阶段判定(前端基于明细重算)
refineTrendStages(Array.from(new Set((res.results||[]).map(r => r.symbol))).slice(0, 20), timeframe, startMs, endMs);
} catch (e) {
alert('趋势筛选失败: ' + e);
} finally {
$('#trendFilterStatus').hide();
}
}
async function loadTrendDetail(symbol, timeframe, startMs, endMs) {
const params = $.param({
symbol: symbol,
timeframe: timeframe,
start_time: startMs || '',
end_time: endMs || '',
timezone: $('#timezone').val() || 'Asia/Shanghai'
});
// 显示详情加载状态
$('#trendDetailStatus').show();
try {
const data = await $.getJSON(`/api/trend_detail?${params}`);
// 填表
trendDetailTable.clear();
(data.kline_data || []).forEach(row => {
trendDetailTable.row.add([
new Date(row.timestamp).toLocaleString('zh-CN', { timeZone: data.timezone }),
row.open, row.high, row.low, row.close, row.volume,
row.ema5, row.ema10, row.ema24, row.ema52
]);
});
trendDetailTable.draw();
// 画图
drawTrendChart(data);
} catch (e) {
alert('加载趋势详情失败: ' + e);
} finally {
$('#trendDetailStatus').hide();
}
}
function drawTrendChart(data) {
const container = document.getElementById('trendChartContainer');
if (!container) return;
container.innerHTML = '';
const chart = LightweightCharts.createChart(container, {
layout: { background: { color: '#ffffff' }, textColor: '#333' },
rightPriceScale: { visible: true },
timeScale: { timeVisible: true, secondsVisible: false },
crosshair: { mode: LightweightCharts.CrosshairMode.Normal },
grid: { vertLines: { color: '#eee' }, horzLines: { color: '#eee' } },
autoSize: true
});
trendChart = chart;
const candle = chart.addCandlestickSeries();
// 关闭均线的价格线与最后值标签,仅保留K线的当前价格虚线
const ema5 = chart.addLineSeries({ color: '#ff0000', lineWidth: 2, lastValueVisible: false, priceLineVisible: false });
const ema10 = chart.addLineSeries({ color: '#2962FF', lineWidth: 2, lastValueVisible: false, priceLineVisible: false });
const ema24 = chart.addLineSeries({ color: '#008000', lineWidth: 2, lastValueVisible: false, priceLineVisible: false });
const ema52 = chart.addLineSeries({ color: '#800080', lineWidth: 2, lastValueVisible: false, priceLineVisible: false });
const k = (data.kline_data || []).map(r => ({
time: Math.floor(r.timestamp / 1000),
open: Number(r.open), high: Number(r.high), low: Number(r.low), close: Number(r.close)
}));
candle.setData(k);
// 前端过滤均线前导缺失/无效值,避免绘制为0
const sanitizeMA = (field) => {
const rows = data.kline_data || [];
const out = [];
let started = false;
for (let i = 0; i < rows.length; i++) {
const r = rows[i];
const raw = r[field];
const v = Number(raw);
const valid = Number.isFinite(v) && v > 0;
if (!started) {
if (!valid) continue;
started = true;
}
if (!valid) continue;
out.push({ time: Math.floor(r.timestamp / 1000), value: v });
}
return out;
};
ema5.setData(sanitizeMA('ema5'));
ema10.setData(sanitizeMA('ema10'));
ema24.setData(sanitizeMA('ema24'));
ema52.setData(sanitizeMA('ema52'));
// 趋势线(使用返回的拟合参数)
const trend = data.trend_line || null;
if (trend && k.length > 1) {
const L = Math.min(trend.length, k.length);
const startIdx = k.length - L;
const lineData = [];
for (let i = 0; i < L; i++) {
const y = trend.slope * i + trend.intercept;
const point = { time: k[startIdx + i].time, value: y };
lineData.push(point);
}
const trendSeries = chart.addLineSeries({ color: '#ffa500', lineWidth: 2, lineStyle: 0, lastValueVisible: false, priceLineVisible: false });
trendSeries.setData(lineData);
}
}
// ===== 前端精细化阶段判定 =====
function computeEMA(arr, period) {
const k = 2 / (period + 1);
const out = [];
let emaPrev = null;
for (let i = 0; i < arr.length; i++) {
const price = arr[i];
if (price == null || !isFinite(price)) { out.push(null); continue; }
if (emaPrev == null) {
// 取首个可用SMA种子
const start = Math.max(0, i - period + 1);
const window = arr.slice(start, i + 1).filter(v => v != null && isFinite(v));
const sma = window.length ? window.reduce((a,b)=>a+b,0)/window.length : price;
emaPrev = sma;
}
const ema = price * k + emaPrev * (1 - k);
out.push(ema);
emaPrev = ema;
}
return out;
}
function computeMACDSeries(close) {
const ema12 = computeEMA(close, 12);
const ema26 = computeEMA(close, 26);
const macd = close.map((_, i) => (ema12[i] != null && ema26[i] != null) ? (ema12[i] - ema26[i]) : null);
const signal = computeEMA(macd.map(v => v ?? null), 9);
const hist = macd.map((v, i) => (v != null && signal[i] != null) ? (v - signal[i]) : null);
return { macd, signal, hist };
}
function slope(series, win) {
const n = series.length;
const k = Math.min(win, n);
if (k < 3) return 0;
const y = series.slice(n - k).filter(v => v != null && isFinite(v));
if (y.length < 3) return 0;
const x = [...Array(y.length).keys()];
const xm = x.reduce((a,b)=>a+b,0)/x.length;
const ym = y.reduce((a,b)=>a+b,0)/y.length;
let num = 0, den = 0;
for (let i=0;i<x.length;i++){ num += (x[i]-xm)*(y[i]-ym); den += (x[i]-xm)*(x[i]-xm); }
return den ? num/den : 0;
}
function classifyStageFrontend(kline, directionHint) {
const close = kline.map(r => Number(r.close));
const ema24 = computeEMA(close, 24);
const ema52 = computeEMA(close, 52);
const last = close[close.length-1];
const e24 = ema24[ema24.length-1];
const e52 = ema52[ema52.length-1];
const s24 = slope(ema24, 20);
const s52 = slope(ema52, 30);
const dist52 = (e52 && isFinite(e52)) ? (last - e52)/e52 : 0;
const { hist } = computeMACDSeries(close);
const recent = hist.slice(-9).filter(v => v != null);
const earlier = hist.slice(-18, -9).filter(v => v != null);
const growth = (recent.length && earlier.length) ? (avgAbs(recent) - avgAbs(earlier)) : 0;
function avgAbs(arr){ return arr.reduce((a,b)=>a+Math.abs(b),0)/arr.length; }
let direction = directionHint;
if (!direction) {
if (e24 > e52 && s24 > 0 && s52 > 0) direction = 'bull';
else if (e24 < e52 && s24 < 0 && s52 < 0) direction = 'bear';
else direction = 'sideways';
}
let stage = 'early';
const ad = Math.abs(dist52);
if (direction === 'bull') {
if (ad < 0.03 && growth > 0) stage = 'early';
else if (ad < 0.10 && (growth >= 0 || s24 > 0)) stage = 'mid';
else stage = 'late';
} else if (direction === 'bear') {
if (ad < 0.03 && growth > 0) stage = 'early';
else if (ad < 0.10 && (growth >= 0 || s24 < 0)) stage = 'mid';
else stage = 'late';
} else {
stage = 'early';
}
return { direction, stage };
}
async function refineTrendStages(symbols, timeframe, startMs, endMs) {
if (!symbols || symbols.length === 0) return;
// 在表头上方提示
const info = $('<div class="text-muted mb-2" id="refineInfo">正在优化阶段判定...</div>');
$('#trendFilterTable').before(info);
const tz = $('#timezone').val() || 'Asia/Shanghai';
const selectedDir = $('#trendDirection').val(); // bull/bear/sideways/''
for (const sym of symbols) {
try {
const params = $.param({ symbol: sym, timeframe, start_time: startMs, end_time: endMs, timezone: tz });
const data = await $.getJSON(`/api/trend_detail?${params}`);
const { direction, stage } = classifyStageFrontend(data.kline_data || [], null);
// 若与选择的方向不一致,则在前端移除该行,避免“选择多头仍出现空头/盘整”
if (selectedDir && direction !== selectedDir) {
if (trendTable) {
trendTable.rows().every(function(){
const rowData = this.data();
if (rowData && rowData[0] === sym) {
this.remove();
}
});
trendTable.draw(false);
}
continue;
}
// 否则更新该行方向与阶段展示
$('#trendFilterTable tbody tr').each(function(){
const tds = $(this).find('td');
if (tds.eq(0).text() === sym) {
tds.eq(2).text(direction === 'bull' ? '多头' : (direction === 'bear' ? '空头' : '盘整'));
tds.eq(3).text(stage === 'early' ? '初期' : stage === 'mid' ? '中期' : '末期');
}
});
} catch(e) {
// 忽略单个失败
}
}
info.remove();
}
// 页面初始化时绑定控件
$(function(){
initTrendTables();
bindTrendControls();
});
let tvWidget = {
mainChart: null,
volumeChart: null,
@@ -4314,7 +4807,7 @@
if (fx.fx_strength < 1.0) { // 降低阈值让更多分型显示
displayText = fx.fx_strength >= 0.8 ? '' : '' // 0.8以上显示点,0.8以下不显示文本
}
displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("11", "").replace("21", "").replace("31", "");
displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("1", "").replace("2", "").replace("3", "");
// 添加标记配置
const markerConfig = {
time: timestamp,
@@ -4378,7 +4871,7 @@
if (fx.fx_strength < 1.0) { // 降低阈值让更多分型显示
displayText = fx.fx_strength >= 1.5 ? '' : '' // 0.8以上显示点,0.8以下不显示文本
}
displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("11", "").replace("12", "").replace("13", "");
displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("1", "").replace("2", "").replace("3", "");
// 添加标记配置
const markerConfig = {
time: timestamp,
@@ -4462,7 +4955,7 @@
if (fx.fx_strength < 1.0){ // 调整小周期阈值
displayText = fx.fx_strength >= 0.6 ? '' : '' // 0.6以上显示点
}
displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("11", "").replace("21", "").replace("31", "");
displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("1", "").replace("2", "").replace("3", "");
// 小周期分型标记配置
const markerConfig = {
time: timestamp,
@@ -4518,7 +5011,7 @@
if (fx.fx_strength < 2.0){ // 调整小周期阈值
displayText = fx.fx_strength >= 1.5 ? '' : '' // 0.6以上显示点
}
displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("11", "").replace("21", "").replace("13", "");
displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("1", "").replace("2", "").replace("3", "");
// 小周期KLU分型标记配置
const markerConfig = {
time: timestamp,