#!/usr/bin/env python3 # Flask web 应用 from flask import Flask, render_template, jsonify, request import sys import os # 添加项目根目录到 Python 路径 sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from qihuo_analyzer.data.data_fetcher import DataFetcher from qihuo_analyzer.data.data_storage import DataStorage from qihuo_analyzer.modules.trend_filter import TrendFilter from qihuo_analyzer.modules.risk_manager import RiskManager from qihuo_analyzer.modules.fund_flow_monitor import FundFlowMonitor from qihuo_analyzer.modules.support_resistance import SupportResistance from qihuo_analyzer.modules.rollover_detector import RolloverDetector from qihuo_analyzer.modules.deepseek_agent import DeepseekAgent from qihuo_analyzer.core.models import AnalysisResult app = Flask(__name__) # 模板上下文处理器 @app.context_processor def inject_functions(): import datetime def now(): return datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') return {'now': now} # 初始化组件 data_fetcher = DataFetcher() data_storage = DataStorage() trend_filter = TrendFilter() risk_manager = RiskManager() fund_flow_monitor = FundFlowMonitor() support_resistance = SupportResistance() rollover_detector = RolloverDetector() deepseek_agent = DeepseekAgent() # 连接API data_fetcher.connect() # 测试品种列表 - 使用当前有效的合约代码 test_symbols = ["CU2603", "AL2603", "ZN2603", "PB2603", "NI2603", "SN2603"] @app.route('/') def index(): """首页 - 多品种分析面板""" # 获取所有品种的分析数据 symbols_data = [] data_available = False for symbol in test_symbols: try: # 获取K线数据 kline_data = data_fetcher.get_kline_data(symbol, "1d", 200) if kline_data is None or kline_data.empty: continue data_available = True # 趋势分析 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'), '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')) } symbols_data.append(symbol_data) except Exception as e: print(f"分析 {symbol} 失败: {e}") continue # 如果没有任何数据可用,显示友好提示 if not data_available: return render_template('index.html', symbols_data=[], data_unavailable=True, message="无法获取真实市场数据,请检查网络连接和TQSDK账号状态") return render_template('index.html', symbols_data=symbols_data, data_unavailable=False) @app.route('/symbol/') def symbol_detail(symbol): """品种详情页""" try: # 辅助函数:安全地四舍五入数字 def safe_round(value, decimals=2, context=''): try: if context: print(f"[DEBUG] safe_round called with value: {value}, type: {type(value)}, context: {context}") if value is None: print(f"[DEBUG] safe_round returning 0 for None value, context: {context}") return 0 # 检查是否为NaN if isinstance(value, float) and value != value: # NaN检查 print(f"[DEBUG] safe_round returning 0 for NaN value, context: {context}") return 0 # 检查是否为undefined if value == 'undefined' or value == 'nan' or value == 'None': print(f"[DEBUG] safe_round returning 0 for undefined-like value, context: {context}") return 0 result = round(float(value), decimals) if context: print(f"[DEBUG] safe_round returning {result} for value: {value}, context: {context}") return result except (ValueError, TypeError) as e: print(f"[DEBUG] safe_round exception: {e}, value: {value}, type: {type(value)}, context: {context}") return 0 # 获取K线数据 kline_data = data_fetcher.get_kline_data(symbol, "1d", 200) if kline_data is None or kline_data.empty: return render_template('error.html', message="获取数据失败") # 基础数据 try: current_price = kline_data['close'].iloc[-1] # 检查current_price是否为有效数字 if current_price is None or (isinstance(current_price, float) and current_price != current_price): # NaN检查 return render_template('error.html', message="获取数据失败:价格数据无效") except (ValueError, TypeError, IndexError): return render_template('error.html', message="获取数据失败:价格数据无效") try: price_change = kline_data['close'].iloc[-1] - kline_data['close'].iloc[-2] price_change_pct = (price_change / kline_data['close'].iloc[-2]) * 100 except (ValueError, TypeError, IndexError): price_change = 0 price_change_pct = 0 # 趋势分析 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) # 压力支撑分析 sr_analysis = support_resistance.analyze_support_resistance(kline_data) support_levels = sr_analysis['support_resistance_levels']['support_levels'] resistance_levels = sr_analysis['support_resistance_levels']['resistance_levels'] # 风险分析 atr = trend_analysis.get('atr', 20) stop_loss_long = risk_manager.calculate_stop_loss(kline_data, current_price, "long") stop_loss_short = risk_manager.calculate_stop_loss(kline_data, current_price, "short") # 仓位计算 account_balance = 1000000 position_info = risk_manager.calculate_position_size(account_balance, kline_data, "long", current_price) # 换月分析 rollover_analysis = rollover_detector.analyze_rollover(symbol, kline_data) # AI 分析 market_data = { 'symbol': symbol, 'latest_price': current_price, 'volume': kline_data['volume'].iloc[-1], 'open_interest': kline_data['open_interest'].iloc[-1], 'timeframe': '1d' } technical_indicators = { 'macd': {'signal': '金叉'}, 'rsi': 55, 'bollinger': {'position': '中轨附近'}, 'kdj': {'signal': '金叉'}, 'atr': atr } # 安全获取趋势分析数据,处理可能的NaN值 def safe_get_trend_value(key, default=0): value = trend_analysis.get(key, default) try: if value is None: return default if isinstance(value, float) and value != value: # NaN检查 return default return value except (ValueError, TypeError): return default # 打印趋势分析数据 print(f"[DEBUG] trend_analysis: {trend_analysis}") print(f"[DEBUG] win_rate: {win_rate}, type: {type(win_rate)}") print(f"[DEBUG] atr: {atr}, type: {type(atr)}") print(f"[DEBUG] stop_loss_long: {stop_loss_long}, type: {type(stop_loss_long)}") print(f"[DEBUG] stop_loss_short: {stop_loss_short}, type: {type(stop_loss_short)}") print(f"[DEBUG] position_info: {position_info}") trend_data = { 'adx': safe_get_trend_value('adx'), 'trend_strength': safe_get_trend_value('trend_strength', 'none'), 'trend_direction': safe_get_trend_value('trend_direction', 'neutral'), 'ma_relationship': safe_get_trend_value('ma_relationship', 'neutral'), 'overall_trend': safe_get_trend_value('overall_trend', 'neutral'), 'win_rate': safe_round(win_rate, 1, context='trend_data_win_rate') if win_rate is not None else 0 } # 安全获取止损和目标价格 safe_stop_loss = safe_round(stop_loss_long, 2, context='safe_stop_loss') if stop_loss_long is not None else 0 safe_target_price = 0 if resistance_levels: try: target = resistance_levels[0] print(f"[DEBUG] resistance_levels[0]: {target}, type: {type(target)}") if target is not None and (not isinstance(target, float) or target == target): # 不是NaN safe_target_price = safe_round(target, 2, context='safe_target_price_from_resistance') else: safe_target_price = safe_round(current_price * 1.05, 2, context='safe_target_price_from_current') except (ValueError, TypeError, IndexError) as e: print(f"[DEBUG] Error getting target price: {e}") safe_target_price = safe_round(current_price * 1.05, 2, context='safe_target_price_default') else: safe_target_price = safe_round(current_price * 1.05, 2, context='safe_target_price_no_resistance') risk_metrics = { 'stop_loss': safe_stop_loss, 'target_price': safe_target_price, 'profit_loss_ratio': 1.8, 'position_size': position_info.get('suggested_units', 0), 'risk_ratio': safe_round(position_info.get('actual_risk_percent', 0) * 100, 2, context='risk_metrics_risk_ratio') } # AI 分析 ai_analysis = deepseek_agent.analyze_market(market_data, technical_indicators, trend_data, risk_metrics) recommendation = deepseek_agent.generate_trade_recommendation(ai_analysis) # 构建模板数据 # 获取中文名称 product_name_cn = data_fetcher.get_product_name_cn(symbol) print(f"[DEBUG] product_name_cn: {product_name_cn}") # 转换周期为中文 def cycle_to_cn(cycle): cycle_map = { 'short': '短期', 'medium': '中期', 'long': '长期', 'bullish': '多头', 'bearish': '空头', 'sideways': '震荡' } return cycle_map.get(cycle, cycle) # 转换趋势方向为中文 def direction_to_cn(direction): direction_map = { 'bullish': '多头', 'bearish': '空头', 'sideways': '震荡' } return direction_map.get(direction, direction) # 转换资金流向为中文 def fund_flow_to_cn(fund_flow): fund_flow_map = { 'bullish': '多头', 'bearish': '空头', 'neutral': '中性', 'Strong_bullish': '强多头', 'Strong_bearish': '强空头', 'strong_increasing': '强劲增加', 'increasing': '增加', 'strong_decreasing': '强劲减少', 'decreasing': '减少', 'stable': '稳定', 'price_up_oi_up': '价涨量增', 'price_up_oi_down': '价涨量减', 'price_down_oi_up': '价跌量增', 'price_down_oi_down': '价跌量减', 'bullish_divergence': '看涨背离', 'bearish_divergence': '看跌背离', 'no_divergence': '无背离' } return fund_flow_map.get(fund_flow, fund_flow) print(f"[DEBUG] current_price: {current_price}, type: {type(current_price)}") print(f"[DEBUG] price_change: {price_change}, type: {type(price_change)}") print(f"[DEBUG] price_change_pct: {price_change_pct}, type: {type(price_change_pct)}") print(f"[DEBUG] cycle: {cycle}, type: {type(cycle)}") context = { 'symbol': symbol, 'name': f"{product_name_cn}({symbol})".upper(), 'current_price': safe_round(current_price, 2, context='context_current_price'), 'price_change': safe_round(price_change, 2, context='context_price_change'), 'price_change_pct': safe_round(price_change_pct, 2, context='context_price_change_pct'), 'trend_analysis': trend_analysis, 'win_rate': safe_round(win_rate, 1, context='context_win_rate'), 'cycle': cycle_to_cn(cycle), 'fund_flow_analysis': fund_flow_analysis, 'sr_analysis': sr_analysis, 'support_levels': support_levels, 'resistance_levels': resistance_levels, 'risk_analysis': { 'atr': safe_round(atr, 2, context='risk_analysis_atr'), 'stop_loss_long': safe_round(stop_loss_long, 2, context='risk_analysis_stop_loss_long'), 'stop_loss_short': safe_round(stop_loss_short, 2, context='risk_analysis_stop_loss_short'), 'position_size': position_info.get('suggested_units', 0), 'risk_ratio': safe_round(position_info.get('actual_risk_percent', 0) * 100, 2, context='risk_analysis_risk_ratio'), 'leverage': safe_round(position_info.get('leverage', 0), 2, context='risk_analysis_leverage') }, 'rollover_analysis': rollover_analysis, 'ai_analysis': ai_analysis, 'recommendation': recommendation, 'kline_data': _get_kline_data_for_chart(kline_data) } print("[DEBUG] context built successfully") return render_template('symbol_detail.html', **context) except Exception as e: print(f"分析 {symbol} 详情失败: {e}") return render_template('error.html', message=f"分析失败: {str(e)}") @app.route('/api/analysis/') def api_analysis(symbol): """API: 获取品种分析数据""" try: # 辅助函数:安全地四舍五入数字 def safe_round(value, decimals=2): try: if value is None: return 0 # 检查是否为NaN if isinstance(value, float) and value != value: # NaN检查 return 0 return round(float(value), decimals) except (ValueError, TypeError): return 0 # 获取K线数据 kline_data = data_fetcher.get_kline_data(symbol, "1d", 200) if kline_data.empty: return jsonify({"error": "获取数据失败"}), 400 # 趋势分析 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) # 压力支撑分析 sr_analysis = support_resistance.analyze_support_resistance(kline_data) # 换月分析 rollover_analysis = rollover_detector.analyze_rollover(symbol, kline_data) # 价格数据 current_price = kline_data['close'].iloc[-1] # 构建响应 response = { 'symbol': symbol, 'current_price': safe_round(current_price, 2), 'trend_analysis': trend_analysis, 'win_rate': safe_round(win_rate, 1), 'cycle': cycle, 'fund_flow_analysis': fund_flow_analysis, 'sr_analysis': sr_analysis, 'rollover_analysis': rollover_analysis } return jsonify(response) except Exception as e: return jsonify({"error": str(e)}), 500 @app.route('/api/card/') def api_card(symbol): """API: 获取分析卡片数据""" try: # 辅助函数:安全地四舍五入数字 def safe_round(value, decimals=2): try: if value is None: return 0 # 检查是否为NaN if isinstance(value, float) and value != value: # NaN检查 return 0 return round(float(value), decimals) except (ValueError, TypeError): return 0 # 获取K线数据 kline_data = data_fetcher.get_kline_data(symbol, "1d", 200) if kline_data.empty: return jsonify({"error": "获取数据失败"}), 400 # 分析数据 trend_analysis = trend_filter.analyze_trend(kline_data) win_rate = trend_filter.calculate_win_rate(kline_data) fund_flow_analysis = fund_flow_monitor.analyze_fund_flow(kline_data) sr_analysis = support_resistance.analyze_support_resistance(kline_data) rollover_analysis = rollover_detector.analyze_rollover(symbol, kline_data) # AI 分析 current_price = kline_data['close'].iloc[-1] market_data = { 'symbol': symbol, 'latest_price': current_price, 'volume': kline_data['volume'].iloc[-1], 'open_interest': kline_data['open_interest'].iloc[-1], 'timeframe': '1d' } technical_indicators = { 'macd': {'signal': '金叉'}, 'rsi': 55, 'bollinger': {'position': '中轨附近'}, 'kdj': {'signal': '金叉'}, 'atr': trend_analysis.get('atr', 20) } # 安全获取趋势分析数据,处理可能的NaN值 def safe_get_trend_value(key, default=0): value = trend_analysis.get(key, default) try: if value is None: return default if isinstance(value, float) and value != value: # NaN检查 return default return value except (ValueError, TypeError): return default trend_data = { 'adx': safe_get_trend_value('adx'), 'trend_strength': safe_get_trend_value('trend_strength', 'none'), 'trend_direction': safe_get_trend_value('trend_direction', 'neutral'), 'ma_relationship': safe_get_trend_value('ma_relationship', 'neutral'), 'overall_trend': safe_get_trend_value('overall_trend', 'neutral'), 'win_rate': safe_round(win_rate, 1) if win_rate is not None else 0 } # 安全获取止损和目标价格 safe_stop_loss = 0 try: stop_loss = risk_manager.calculate_stop_loss(kline_data, current_price, "long") safe_stop_loss = safe_round(stop_loss, 2) if stop_loss is not None else 0 except Exception: safe_stop_loss = 0 safe_target_price = 0 try: resistance_levels = sr_analysis.get('support_resistance_levels', {}).get('resistance_levels', []) if resistance_levels: target = resistance_levels[0] if target is not None and (not isinstance(target, float) or target == target): # 不是NaN safe_target_price = safe_round(target, 2) else: safe_target_price = safe_round(current_price * 1.05, 2) else: safe_target_price = safe_round(current_price * 1.05, 2) except Exception: safe_target_price = safe_round(current_price * 1.05, 2) risk_metrics = { 'stop_loss': safe_stop_loss, 'target_price': safe_target_price, 'profit_loss_ratio': 1.8, 'position_size': 2, 'risk_ratio': 2.5 } ai_analysis = deepseek_agent.analyze_market(market_data, technical_indicators, trend_data, risk_metrics) recommendation = deepseek_agent.generate_trade_recommendation(ai_analysis) # 构建卡片数据 # 获取中文名称 product_name_cn = data_fetcher.get_product_name_cn(symbol) # 转换趋势方向为中文 def direction_to_cn(direction): direction_map = { 'bullish': '多头', 'bearish': '空头', 'sideways': '震荡' } return direction_map.get(direction, direction) card_data = { 'symbol': symbol, 'name': f"{product_name_cn}({symbol})".upper(), 'current_price': safe_round(current_price, 2), 'direction': direction_to_cn(trend_analysis.get('overall_trend', 'sideways')), 'win_rate': safe_round(win_rate, 1), 'recommendation': recommendation, 'technical_indicators': technical_indicators, 'fund_flow': fund_flow_analysis, 'rollover_warning': rollover_analysis } return jsonify(card_data) except Exception as e: return jsonify({"error": str(e)}), 500 @app.route('/card/') def card(symbol): """分析卡片页面""" try: # 获取卡片数据 import requests response = requests.get(f"http://localhost:5000/api/card/{symbol}") card_data = response.json() if 'error' in card_data: return render_template('error.html', message=card_data['error']) return render_template('card.html', **card_data) except Exception as e: print(f"获取卡片数据失败: {e}") return render_template('error.html', message=f"获取卡片数据失败: {str(e)}") def _get_trend_strength_display(adx): """获取趋势强度显示文本""" try: adx_value = float(adx) if adx is not None else 0 if adx_value > 40: return f"强势多头({round(adx_value, 1)})" elif adx_value > 25: return f"多头({round(adx_value, 1)})" elif adx_value > 20: return f"弱势多头({round(adx_value, 1)})" else: return f"震荡({round(adx_value, 1)})" except (ValueError, TypeError): return "震荡(0.0)" def _get_rollover_warning(rollover_analysis): """获取换月预警信息""" try: days = rollover_analysis.get('days_to_delivery', 0) level = rollover_analysis.get('warning_level', 'low') try: days_value = int(days) if days is not None else 0 except (ValueError, TypeError): days_value = 0 if level == 'critical': return f"⚠️ {abs(days_value)} 天" elif level == 'high': return f"⚠️ {days_value} 天" elif level == 'medium': return f"⚠️ {days_value} 天" else: return f"✅ {days_value} 天" except (ValueError, TypeError): return "✅ 0 天" def _get_kline_data_for_chart(kline_data): """获取图表用的K线数据""" try: chart_data = [] if kline_data is not None and not kline_data.empty: for idx, row in kline_data.tail(30).iterrows(): try: chart_data.append({ 'date': idx.strftime('%Y-%m-%d') if hasattr(idx, 'strftime') else '2023-01-01', 'open': float(row.get('open', 0)), 'high': float(row.get('high', 0)), 'low': float(row.get('low', 0)), 'close': float(row.get('close', 0)), 'volume': float(row.get('volume', 0)) }) except (ValueError, TypeError): continue return chart_data except (ValueError, TypeError): return [] if __name__ == '__main__': app.run(debug=True, host='0.0.0.0', port=5000)