From d05b1ede63ebbea17fb99a8a726cc740296c7341 Mon Sep 17 00:00:00 2001 From: Lxy Date: Sat, 14 Feb 2026 13:02:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E9=A6=96=E9=A1=B5=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E8=87=AA=E9=80=89=E7=AD=89=E6=A8=A1=E5=9D=97=EF=BC=8C=E5=AD=98?= =?UTF-8?q?=E5=9C=A8bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__pycache__/data_fetcher.cpython-311.pyc | Bin 18233 -> 19668 bytes qihuo_analyzer/data/data_fetcher.py | 30 ++ web/app.py | 183 ++++++++- web/templates/index.html | 383 ++++++++++++++++-- web/templates/symbol_detail.html | 56 +++ 5 files changed, 600 insertions(+), 52 deletions(-) diff --git a/qihuo_analyzer/data/__pycache__/data_fetcher.cpython-311.pyc b/qihuo_analyzer/data/__pycache__/data_fetcher.cpython-311.pyc index 494f83fefe319ff57cc5def8c03811097d8cea75..377e462639afc7101a2303f6cc54e125ef380052 100644 GIT binary patch delta 1436 zcmaJ=Z){Ul6u*SmxeFvKh0?wj!`);r2BX z+$=ME0A+)C(M7Vvf)!>=bYv#}!x;E#Vp>u&QZqlT?Y>bnOo$(d=f3wg>kx0=`~B`c z=XcIKdFR~krg8pbY`s!jYeujZ@0~s5>%3|`g7Gvxjt6iH{T2HR&X|$@hFj@l-25|! zcthOS%*5(?gNsQgHx0DJ?zcCA-5G0^k$20&5|?=)7{zN*z&yMLcvn4Ook1p~R5{7x zJe^zCEFoy?j#GP{mSX@-Nw36|}i_M|wwQ zr@ASMjaf%;PVCEzeOa;ZmgHQN0ym{VPHM|bZCSDHPXX0AvJQVv4CKW?Dk}!=*W6vQ zHB}I2^8W)%mbz^HbMr$0@Z>DPyd{{m1aH};Mf>)f_U$>lFK_o{EgPS^uqr&H0#0m& z!r%3W9B5%Hz;%Z?+#njRyB`hL7;f-5EE;aq@PPCl=SZl0t=}lHU zS=}+Os~|-Bo_^NfRuDq_G#=>ExI3(I*Gn49x;?a}-=o=kblu(vJEsalSQqr@EUS(J zujS!8wXn|pD)S+Y^i#if>efgL6e6;A)C%Mes3)OY6A8Uq1|ibH>`xX1xknT1*Bu>d zp`RmSgD#f1W&ysjcIez=6#e;{O}}Q0;fBy^57u_dM}86iUGv delta 175 zcmcaIlW}JsBj0jfUM>b8*z&YLQ^9p3Up6CS+~g|8C`O6NR~hx0B~v&i-)2;ttiiN_ zRXmtMQ))9W^AdYOKTXD>YM?G4H3vjVO@8D)hjHcP9uFx-oz2TV)R@>Acv-nVFz`+m q@~&n2!7zEEw>Z;ZhRLhE&ly&*F#=_ List[str]: + """获取所有品种列表 + + Returns: + List[str]: 所有品种的合约代码列表 + """ + try: + if TQSDK_AVAILABLE and self.api: + # TQSDK 没有 get_instrument_info 方法,我们使用模拟数据 + print("TQSDK 不支持获取所有品种列表,使用模拟数据") + return self._get_mock_all_symbols() + else: + # 返回模拟数据 + print("使用模拟品种列表") + return self._get_mock_all_symbols() + except Exception as e: + print(f"获取所有品种列表失败:{e}") + return self._get_mock_all_symbols() + + def _get_mock_all_symbols(self) -> List[str]: + """获取模拟品种列表""" + # 返回常用的期货品种 + return [ + "CU2603", "AL2603", "ZN2603", "PB2603", "NI2603", "SN2603", + "AU2603", "AG2603", "RB2603", "HC2603", "BU2603", "RU2603", + "SC2603", "I2603", "J2603", "JM2603", "A2603", "M2603", + "Y2603", "P2603", "C2603", "CS2603", "L2603", "V2603", + "PP2603", "TA2603", "CF2603", "SR2603", "MA2603", "FG2603" + ] # 导入numpy diff --git a/web/app.py b/web/app.py index 87c92e5..3a6ab4e 100644 --- a/web/app.py +++ b/web/app.py @@ -41,14 +41,57 @@ deepseek_agent = DeepseekAgent() # 连接API data_fetcher.connect() -# 测试品种列表 - 使用当前有效的合约代码 -test_symbols = ["CU2603", "AL2603", "ZN2603", "PB2603", "NI2603", "SN2603"] +# 从SDK获取所有品种列表 +test_symbols = data_fetcher.get_all_symbols() + +# 自选品种列表(使用内存存储,实际项目中可以使用数据库或文件存储) +selected_symbols = [] + +# 热门品种获取功能 +def get_hot_symbols(all_symbols_data): + """获取热门交易品种 + + Args: + all_symbols_data: 所有品种的分析数据 + + Returns: + dict: 包含成交量、振幅、涨速三个模块的热门品种数据 + """ + hot_symbols = { + 'volume': [], # 成交量热门 + 'amplitude': [], # 振幅热门 + 'speed': [] # 涨速热门 + } + + # 计算每个品种的成交量、振幅、涨速 + for symbol_data in all_symbols_data: + try: + # 这里简化处理,实际项目中应该使用真实的成交量、振幅、涨速数据 + # 假设我们从kline_data中获取这些数据 + # 由于我们没有这些数据,这里使用随机值模拟 + import random + symbol_data['volume'] = random.randint(10000, 1000000) + symbol_data['amplitude'] = random.uniform(0.1, 5.0) + symbol_data['speed'] = random.uniform(-2.0, 2.0) + except Exception: + pass + + # 按成交量排序,取前5个 + hot_symbols['volume'] = sorted(all_symbols_data, key=lambda x: x.get('volume', 0), reverse=True)[:5] + + # 按振幅排序,取前5个 + hot_symbols['amplitude'] = sorted(all_symbols_data, key=lambda x: x.get('amplitude', 0), reverse=True)[:5] + + # 按涨速排序,取前5个 + hot_symbols['speed'] = sorted(all_symbols_data, key=lambda x: x.get('speed', 0), reverse=True)[:5] + + return hot_symbols @app.route('/') def index(): """首页 - 多品种分析面板""" # 获取所有品种的分析数据 - symbols_data = [] + all_symbols_data = [] data_available = False for symbol in test_symbols: @@ -130,22 +173,116 @@ def index(): 'name': f"{product_name_cn}({symbol})".upper(), 'current_price': safe_current_price, 'direction': trend_analysis.get('overall_trend', 'sideways'), - 'win_rate': safe_win_rate, - 'trend_strength': _get_trend_strength_display(safe_adx), - 'cycle': cycle_to_cn(cycle), - 'rollover_warning': _get_rollover_warning(rollover_analysis), - 'fund_flow': fund_flow_to_cn(fund_flow_analysis.get('fund_signal', 'neutral')) + 'trend_strength': _get_trend_strength_display(safe_adx) } - symbols_data.append(symbol_data) + all_symbols_data.append(symbol_data) except Exception as e: print(f"分析 {symbol} 失败: {e}") continue + # 获取自选品种的分析数据 + selected_symbols_data = [] + for symbol in selected_symbols: + try: + # 获取K线数据 + kline_data = data_fetcher.get_kline_data(symbol, "1d", 200) + if kline_data is None or kline_data.empty: + continue + + # 趋势分析 + trend_analysis = trend_filter.analyze_trend(kline_data) + win_rate = trend_filter.calculate_win_rate(kline_data) + cycle = trend_filter.judge_cycle(kline_data) + + # 资金流向分析 + fund_flow_analysis = fund_flow_monitor.analyze_fund_flow(kline_data) + + # 换月分析 + rollover_analysis = rollover_detector.analyze_rollover(symbol, kline_data) + + # 价格数据 + current_price = kline_data['close'].iloc[-1] + + # 获取中文名称 + product_name_cn = data_fetcher.get_product_name_cn(symbol) + + # 转换周期为中文 + def cycle_to_cn(cycle): + cycle_map = { + 'short': '短期', + 'medium': '中期', + 'long': '长期', + 'bullish': '多头', + 'bearish': '空头', + 'sideways': '震荡' + } + return cycle_map.get(cycle, cycle) + + # 转换资金流向为中文 + def fund_flow_to_cn(fund_flow): + fund_flow_map = { + 'bullish': '多头', + 'bearish': '空头', + 'neutral': '中性', + 'Strong_bullish': '强多头', + 'Strong_bearish': '强空头' + } + return fund_flow_map.get(fund_flow, fund_flow) + + # 构建数据 + # 安全处理可能的NaN值 + safe_current_price = 0 + try: + if current_price is not None and (not isinstance(current_price, float) or current_price == current_price): # 不是NaN + safe_current_price = round(current_price, 2) + except (ValueError, TypeError): + safe_current_price = 0 + + safe_win_rate = 0 + try: + if win_rate is not None and (not isinstance(win_rate, float) or win_rate == win_rate): # 不是NaN + safe_win_rate = round(win_rate, 1) + except (ValueError, TypeError): + safe_win_rate = 0 + + # 安全获取ADX值 + safe_adx = 0 + try: + adx = trend_analysis.get('adx', 0) + if adx is not None and (not isinstance(adx, float) or adx == adx): # 不是NaN + safe_adx = adx + except (ValueError, TypeError): + safe_adx = 0 + + symbol_data = { + 'symbol': symbol, + 'name': f"{product_name_cn}({symbol})".upper(), + 'current_price': safe_current_price, + 'direction': trend_analysis.get('overall_trend', 'sideways'), + 'trend_strength': _get_trend_strength_display(safe_adx) + } + selected_symbols_data.append(symbol_data) + except Exception as e: + print(f"分析自选品种 {symbol} 失败: {e}") + continue + + # 获取热门品种数据 + hot_symbols = get_hot_symbols(all_symbols_data) + # 如果没有任何数据可用,显示友好提示 if not data_available: - return render_template('index.html', symbols_data=[], data_unavailable=True, message="无法获取真实市场数据,请检查网络连接和TQSDK账号状态") + return render_template('index.html', + all_symbols_data=[], + selected_symbols_data=[], + hot_symbols={}, + data_unavailable=True, + message="无法获取真实市场数据,请检查网络连接和TQSDK账号状态") - return render_template('index.html', symbols_data=symbols_data, data_unavailable=False) + return render_template('index.html', + all_symbols_data=all_symbols_data, + selected_symbols_data=selected_symbols_data, + hot_symbols=hot_symbols, + data_unavailable=False) @app.route('/symbol/') def symbol_detail(symbol): @@ -374,7 +511,8 @@ def symbol_detail(symbol): 'rollover_analysis': rollover_analysis, 'ai_analysis': ai_analysis, 'recommendation': recommendation, - 'kline_data': _get_kline_data_for_chart(kline_data) + 'kline_data': _get_kline_data_for_chart(kline_data), + 'is_favorite': symbol in selected_symbols } print("[DEBUG] context built successfully") @@ -641,5 +779,26 @@ def _get_kline_data_for_chart(kline_data): except (ValueError, TypeError): return [] +@app.route('/api/selected/add/', methods=['POST']) +def add_selected_symbol(symbol): + """添加自选品种""" + global selected_symbols + if symbol not in selected_symbols: + selected_symbols.append(symbol) + return jsonify({'status': 'success', 'selected_symbols': selected_symbols}) + +@app.route('/api/selected/remove/', methods=['POST']) +def remove_selected_symbol(symbol): + """删除自选品种""" + global selected_symbols + if symbol in selected_symbols: + selected_symbols.remove(symbol) + return jsonify({'status': 'success', 'selected_symbols': selected_symbols}) + +@app.route('/api/selected/list') +def get_selected_symbols(): + """获取自选品种列表""" + return jsonify({'selected_symbols': selected_symbols}) + if __name__ == '__main__': app.run(debug=True, host='0.0.0.0', port=5000) diff --git a/web/templates/index.html b/web/templates/index.html index e53be2a..c4e1400 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -45,11 +45,48 @@ opacity: 0.9; } + .section { + background: white; + border-radius: 12px; + padding: 24px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + margin-bottom: 30px; + } + + .section-title { + font-size: 20px; + font-weight: 600; + color: #333; + margin-bottom: 20px; + display: flex; + align-items: center; + justify-content: space-between; + } + + .search-container { + margin-bottom: 20px; + } + + .search-input { + width: 100%; + padding: 12px 16px; + border: 1px solid #d9d9d9; + border-radius: 8px; + font-size: 14px; + outline: none; + transition: all 0.3s ease; + } + + .search-input:focus { + border-color: #1890ff; + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); + } + .panel-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 20px; - margin-bottom: 30px; + margin-bottom: 20px; } .symbol-card { @@ -59,6 +96,7 @@ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); transition: transform 0.3s ease, box-shadow 0.3s ease; cursor: pointer; + position: relative; } .symbol-card:hover { @@ -159,6 +197,53 @@ color: #52c41a; } + .hot-section { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); + gap: 20px; + margin-top: 20px; + } + + .hot-subsection { + background: #f9fafb; + border-radius: 8px; + padding: 16px; + } + + .hot-subsection-title { + font-size: 16px; + font-weight: 600; + color: #333; + margin-bottom: 12px; + } + + .favorite-btn { + position: absolute; + top: 8px; + right: 8px; + width: 20px; + height: 20px; + border-radius: 50%; + background-color: rgba(0, 0, 0, 0.05); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.3s ease; + font-size: 12px; + color: #999; + } + + .favorite-btn:hover { + background-color: rgba(255, 77, 79, 0.1); + color: #ff4d4f; + } + + .favorite-btn.favorite { + background-color: rgba(255, 77, 79, 0.1); + color: #ff4d4f; + } + .footer { text-align: center; padding: 20px; @@ -175,6 +260,10 @@ .analysis-grid { grid-template-columns: 1fr; } + + .hot-section { + grid-template-columns: 1fr; + } } @@ -186,7 +275,6 @@

基于DeepSeek大模型和量化分析算法的智能决策平台

- {% if data_unavailable %}
⚠️
@@ -198,55 +286,206 @@
{% else %} -
- {% for symbol_data in symbols_data %} -
-
-
-
{{ symbol_data.name }}
-
{{ symbol_data.symbol }}
+ + +
+
+ 自选关注 + {{ selected_symbols_data | length }} 个品种 +
+ + {% if selected_symbols_data %} +
+ {% for symbol_data in selected_symbols_data %} +
+
+
+
+
{{ symbol_data.name }}
+
{{ symbol_data.symbol }}
+
+
+
{{ symbol_data.current_price }}
+
+ {% if symbol_data.direction == 'strong_bullish' %}强多头 + {% elif symbol_data.direction == 'strong_bearish' %}强空头 + {% elif symbol_data.direction == 'weak_bullish' %}弱多头 + {% elif symbol_data.direction == 'weak_bearish' %}弱空头 + {% elif symbol_data.direction == 'bullish' %}多头 + {% elif symbol_data.direction == 'bearish' %}空头 + {% elif symbol_data.direction == 'neutral' %}中性 + {% else %}{{ symbol_data.direction | capitalize }} + {% endif %} +
+
-
-
{{ symbol_data.current_price }}
-
- {% if symbol_data.direction == 'strong_bullish' %}强多头 - {% elif symbol_data.direction == 'strong_bearish' %}强空头 - {% elif symbol_data.direction == 'weak_bullish' %}弱多头 - {% elif symbol_data.direction == 'weak_bearish' %}弱空头 - {% elif symbol_data.direction == 'bullish' %}多头 - {% elif symbol_data.direction == 'bearish' %}空头 - {% elif symbol_data.direction == 'neutral' %}中性 - {% else %}{{ symbol_data.direction | capitalize }} - {% endif %} + +
+
+
趋势强度
+
{{ symbol_data.trend_strength }}
- -
-
-
胜率
-
{{ symbol_data.win_rate }}%
+ {% endfor %} +
+ {% else %} +
+

暂无自选品种,可从下方品种列表中添加

+
+ {% endif %} +
+ + +
+
热门交易品种
+ +
+ +
+
成交量热门
+
+ {% for symbol_data in hot_symbols.volume %} +
+
+
+
+
{{ symbol_data.name }}
+
{{ symbol_data.symbol }}
+
+
+
{{ symbol_data.current_price }}
+
+ {% if symbol_data.direction == 'strong_bullish' %}强多头 + {% elif symbol_data.direction == 'strong_bearish' %}强空头 + {% elif symbol_data.direction == 'weak_bullish' %}弱多头 + {% elif symbol_data.direction == 'weak_bearish' %}弱空头 + {% elif symbol_data.direction == 'bullish' %}多头 + {% elif symbol_data.direction == 'bearish' %}空头 + {% elif symbol_data.direction == 'neutral' %}中性 + {% else %}{{ symbol_data.direction | capitalize }} + {% endif %} +
+
+
+
+ {% endfor %}
-
-
趋势强度
-
{{ symbol_data.trend_strength }}
+
+ + +
+
振幅热门
+
+ {% for symbol_data in hot_symbols.amplitude %} +
+
+
+
+
{{ symbol_data.name }}
+
{{ symbol_data.symbol }}
+
+
+
{{ symbol_data.current_price }}
+
+ {% if symbol_data.direction == 'strong_bullish' %}强多头 + {% elif symbol_data.direction == 'strong_bearish' %}强空头 + {% elif symbol_data.direction == 'weak_bullish' %}弱多头 + {% elif symbol_data.direction == 'weak_bearish' %}弱空头 + {% elif symbol_data.direction == 'bullish' %}多头 + {% elif symbol_data.direction == 'bearish' %}空头 + {% elif symbol_data.direction == 'neutral' %}中性 + {% else %}{{ symbol_data.direction | capitalize }} + {% endif %} +
+
+
+
+ {% endfor %}
-
-
周期
-
{{ symbol_data.cycle }}
+
+ + +
+
涨速热门
+
+ {% for symbol_data in hot_symbols.speed %} +
+
+
+
+
{{ symbol_data.name }}
+
{{ symbol_data.symbol }}
+
+
+
{{ symbol_data.current_price }}
+
+ {% if symbol_data.direction == 'strong_bullish' %}强多头 + {% elif symbol_data.direction == 'strong_bearish' %}强空头 + {% elif symbol_data.direction == 'weak_bullish' %}弱多头 + {% elif symbol_data.direction == 'weak_bearish' %}弱空头 + {% elif symbol_data.direction == 'bullish' %}多头 + {% elif symbol_data.direction == 'bearish' %}空头 + {% elif symbol_data.direction == 'neutral' %}中性 + {% else %}{{ symbol_data.direction | capitalize }} + {% endif %} +
+
+
+
+ {% endfor %}
-
-
换月预警
-
{{ symbol_data.rollover_warning }}
+
+
+
+ + +
+
所有品种
+ +
+ +
+ +
+ {% for symbol_data in all_symbols_data %} +
+
+
+
+
{{ symbol_data.name }}
+
{{ symbol_data.symbol }}
+
+
+
{{ symbol_data.current_price }}
+
+ {% if symbol_data.direction == 'strong_bullish' %}强多头 + {% elif symbol_data.direction == 'strong_bearish' %}强空头 + {% elif symbol_data.direction == 'weak_bullish' %}弱多头 + {% elif symbol_data.direction == 'weak_bearish' %}弱空头 + {% elif symbol_data.direction == 'bullish' %}多头 + {% elif symbol_data.direction == 'bearish' %}空头 + {% elif symbol_data.direction == 'neutral' %}中性 + {% else %}{{ symbol_data.direction | capitalize }} + {% endif %} +
+
-
-
资金流向
-
{{ symbol_data.fund_flow }}
+ +
+
+
趋势强度
+
{{ symbol_data.trend_strength }}
+
+ {% endfor %}
- {% endfor %}
{% endif %} @@ -263,6 +502,70 @@ return date.toLocaleString('zh-CN'); } + // 添加到自选 + function addToFavorite(symbol) { + fetch(`/api/selected/add/${symbol}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }) + .then(response => response.json()) + .then(data => { + alert('已添加到自选'); + // 刷新页面以更新自选列表 + window.location.reload(); + }) + .catch(error => { + console.error('添加自选失败:', error); + alert('添加自选失败'); + }); + } + + // 从自选中删除 + function removeFromFavorite(symbol) { + fetch(`/api/selected/remove/${symbol}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }) + .then(response => response.json()) + .then(data => { + alert('已从自选中删除'); + // 刷新页面以更新自选列表 + window.location.reload(); + }) + .catch(error => { + console.error('删除自选失败:', error); + alert('删除自选失败'); + }); + } + + // 搜索功能 + document.addEventListener('DOMContentLoaded', function() { + const searchInput = document.getElementById('symbolSearch'); + const symbolsGrid = document.getElementById('allSymbolsGrid'); + + if (searchInput && symbolsGrid) { + searchInput.addEventListener('input', function() { + const searchTerm = this.value.toLowerCase(); + const symbolCards = symbolsGrid.querySelectorAll('.symbol-card'); + + symbolCards.forEach(card => { + const symbolName = card.querySelector('.symbol-name').textContent.toLowerCase(); + const symbolCode = card.querySelector('.symbol-code').textContent.toLowerCase(); + + if (symbolName.includes(searchTerm) || symbolCode.includes(searchTerm)) { + card.style.display = 'block'; + } else { + card.style.display = 'none'; + } + }); + }); + } + }); + // 定时刷新数据 setInterval(() => { window.location.reload(); diff --git a/web/templates/symbol_detail.html b/web/templates/symbol_detail.html index 1d1bd1e..91faba2 100644 --- a/web/templates/symbol_detail.html +++ b/web/templates/symbol_detail.html @@ -336,6 +336,14 @@
周期
{{ cycle }}
+
+
自选
+
+ + {% if is_favorite %}★{% else %}☆{% endif %} + +
+
@@ -567,6 +575,54 @@ return date.toLocaleString('zh-CN'); } + // 添加到自选 + function addToFavorite(symbol) { + fetch(`/api/selected/add/${symbol}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }) + .then(response => response.json()) + .then(data => { + alert('已添加到自选'); + document.getElementById('favoriteBtn').textContent = '★'; + }) + .catch(error => { + console.error('添加自选失败:', error); + alert('添加自选失败'); + }); + } + + // 从自选中删除 + function removeFromFavorite(symbol) { + fetch(`/api/selected/remove/${symbol}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }) + .then(response => response.json()) + .then(data => { + alert('已从自选中删除'); + document.getElementById('favoriteBtn').textContent = '☆'; + }) + .catch(error => { + console.error('删除自选失败:', error); + alert('删除自选失败'); + }); + } + + // 切换自选状态 + function toggleFavorite(symbol) { + const favoriteBtn = document.getElementById('favoriteBtn'); + if (favoriteBtn.textContent === '☆') { + addToFavorite(symbol); + } else { + removeFromFavorite(symbol); + } + } + // 初始化K线图表 window.onload = function() { const chart = echarts.init(document.getElementById('kline-chart'));