""" Kronos Enhanced Web UI - Backend 集成波浪理论、技术指标、量价分析的增强版预测系统 """ import os import sys import json import pandas as pd import numpy as np import plotly.graph_objects as go import plotly.utils from datetime import datetime import warnings warnings.filterwarnings('ignore') from flask import Flask, make_response, render_template, request, jsonify from flask_cors import CORS # import akshare as ak # 已弃用,改用腾讯财经 API from io import StringIO import threading import time as time_module # Classic pattern detection try: import sys, importlib, types as _types _cp_path = os.path.join(os.path.dirname(__file__), '..', 'model', 'classic_patterns.py') if os.path.exists(_cp_path) and 'classic_patterns' not in sys.modules: _spec = importlib.util.spec_from_file_location('classic_patterns', os.path.abspath(_cp_path)) _mod = importlib.util.module_from_spec(_spec) sys.modules['classic_patterns'] = _mod _spec.loader.exec_module(_mod) from classic_patterns import detect_classic_patterns CLASSIC_PATTERN_AVAILABLE = True print("[OK] classic_patterns module loaded") else: CLASSIC_PATTERN_AVAILABLE = False except Exception as _e: CLASSIC_PATTERN_AVAILABLE = False print(f"[Warning] classic_patterns not available: {_e}") # Load Wyckoff VPA module (独立于 enhanced modules) WYCKOFF_VPA_AVAILABLE = False try: _wv_path = os.path.join(os.path.dirname(__file__), '..', 'model', 'wyckoff_vpa.py') if os.path.exists(_wv_path) and 'wyckoff_vpa' not in sys.modules: _wv_spec = importlib.util.spec_from_file_location('wyckoff_vpa', os.path.abspath(_wv_path)) _wv_mod = importlib.util.module_from_spec(_wv_spec) sys.modules['wyckoff_vpa'] = _wv_mod _wv_spec.loader.exec_module(_wv_mod) if 'wyckoff_vpa' in sys.modules: from wyckoff_vpa import detect_wyckoff_vpa WYCKOFF_VPA_AVAILABLE = True except Exception as _wve: WYCKOFF_VPA_AVAILABLE = False print(f"[Warning] wyckoff_vpa not available: {_wve}") # Wave Analysis Module (dynamic load, same pattern as classic_patterns/wyckoff_vpa) WAVE_ANALYSIS_AVAILABLE = False try: _wa_path = os.path.join(os.path.dirname(__file__), '..', 'model', 'wave_analysis.py') if os.path.exists(_wa_path) and 'wave_analysis' not in sys.modules: _wa_spec = importlib.util.spec_from_file_location('wave_analysis', os.path.abspath(_wa_path)) _wa_mod = importlib.util.module_from_spec(_wa_spec) sys.modules['wave_analysis'] = _wa_mod _wa_spec.loader.exec_module(_wa_mod) if 'wave_analysis' in sys.modules: from wave_analysis import analyze_wave_patterns, WaveAnalyzer WAVE_ANALYSIS_AVAILABLE = True print('[OK] wave_analysis module loaded') else: print('[Warning] wave_analysis not in sys.modules after load attempt') except Exception as _we: WAVE_ANALYSIS_AVAILABLE = False print(f'[Warning] wave_analysis module not available: {_we}') # Fundamental Analysis Module (dynamic load) FUNDAMENTAL_AVAILABLE = False try: _fa_path = os.path.join(os.path.dirname(__file__), '..', 'model', 'fundamental_analysis.py') if os.path.exists(_fa_path) and 'fundamental_analysis' not in sys.modules: _fa_spec = importlib.util.spec_from_file_location('fundamental_analysis', os.path.abspath(_fa_path)) _fa_mod = importlib.util.module_from_spec(_fa_spec) sys.modules['fundamental_analysis'] = _fa_mod _fa_spec.loader.exec_module(_fa_mod) if 'fundamental_analysis' in sys.modules: from fundamental_analysis import run_fundamental_analysis FUNDAMENTAL_AVAILABLE = True print('[OK] fundamental_analysis module loaded') else: print('[Warning] fundamental_analysis not in sys.modules after load attempt') except Exception as _fe: FUNDAMENTAL_AVAILABLE = False print(f'[Warning] fundamental_analysis module not available: {_fe}') # ====== 踏浪之道 · 主升浪识别模块 (v4.1 评分制) ====== TALANG_AVAILABLE = False try: _tl_path = os.path.join(os.path.dirname(__file__), '..', 'model', 'talang_integration.py') if os.path.exists(_tl_path) and 'talang_integration' not in sys.modules: _tl_spec = importlib.util.spec_from_file_location('talang_integration', os.path.abspath(_tl_path)) _tl_mod = importlib.util.module_from_spec(_tl_spec) sys.modules['talang_integration'] = _tl_mod _tl_spec.loader.exec_module(_tl_mod) if 'talang_integration' in sys.modules: from talang_integration import talang_score_analysis, talang_backtest_full, RULE_NAMES TALANG_AVAILABLE = True print('[OK] 踏浪之道(talang_integration) module loaded') else: print('[Warning] talang_integration not in sys.modules') except Exception as _tle: TALANG_AVAILABLE = False print(f'[Warning] 踏浪之道 not available: {_tle}') # Add project root directory to path PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.insert(0, PROJECT_ROOT) # ---- Mock talib with pure-pandas compat layer ---- import sys, importlib, types as _types _talib_compat = os.path.join(os.path.dirname(__file__), '..', 'model', 'talib.py') if os.path.exists(_talib_compat) and 'talib' not in sys.modules: _spec = importlib.util.spec_from_file_location('talib', os.path.abspath(_talib_compat)) _mod = importlib.util.module_from_spec(_spec) sys.modules['talib'] = _mod _spec.loader.exec_module(_mod) print("[OK] talib compat layer loaded") app = Flask(__name__) CORS(app) # Patch Flask JSON provider to handle numpy types import flask.json.provider _orig_default = flask.json.provider.DefaultJSONProvider.default def _patched_default(self, o): if isinstance(o, (np.bool_,)): return bool(o) if isinstance(o, (np.integer,)): return int(o) if isinstance(o, (np.floating,)): return float(o) if isinstance(o, np.ndarray): return o.tolist() return _orig_default(self, o) flask.json.provider.DefaultJSONProvider.default = _patched_default # ---- Global state ---- MODEL_AVAILABLE = False try: from model import Kronos, KronosTokenizer, KronosPredictor MODEL_AVAILABLE = True except ImportError: print("[Warning] Kronos model cannot be imported") # Enhanced modules (direct import to avoid model/__init__.py torch dependency) ENHANCED_AVAILABLE = False TechnicalIndicators = VolumePriceAnalyzer = WaveTheoryAnalyzer = MultiDimensionSignalAnalyzer = None EnhancedKronosPredictor = create_enhanced_predictor = EnhancedKronosConfig = None try: import importlib.util, sys, os as _os _ti_path = _os.path.join(_os.path.dirname(__file__), '..', 'model', 'technical_indicators.py') _spec = importlib.util.spec_from_file_location('model_technical_indicators', _ti_path) _ti_mod = importlib.util.module_from_spec(_spec) sys.modules['model_technical_indicators'] = _ti_mod _spec.loader.exec_module(_ti_mod) TechnicalIndicators = _ti_mod.TechnicalIndicators VolumePriceAnalyzer = _ti_mod.VolumePriceAnalyzer WaveTheoryAnalyzer = _ti_mod.WaveTheoryAnalyzer MultiDimensionSignalAnalyzer = _ti_mod.MultiDimensionSignalAnalyzer print("[OK] technical_indicators loaded (direct)") except Exception as e: print(f"[Warning] technical_indicators not available: {e}") try: from model.enhanced_kronos import EnhancedKronosPredictor, create_enhanced_predictor from model.enhanced_config import EnhancedKronosConfig ENHANCED_AVAILABLE = True print("[OK] enhanced_kronos loaded") except ImportError as e: print(f"[Warning] enhanced_kronos not available: {e}") # Enable ENHANCED_AVAILABLE if at least technical indicators loaded if TechnicalIndicators is not None: ENHANCED_AVAILABLE = True # Analyzers (stateless, create once) technical_analyzer = TechnicalIndicators() if TechnicalIndicators else None volume_price_analyzer = VolumePriceAnalyzer() if VolumePriceAnalyzer else None wave_analyzer = WaveTheoryAnalyzer() if WaveTheoryAnalyzer else None multidim_analyzer = MultiDimensionSignalAnalyzer() if MultiDimensionSignalAnalyzer else None # Model instances tokenizer = None model = None predictor = None enhanced_predictor = None # ---- Stock data cache (stock_code -> DataFrame) ---- stock_data_cache = {} def _download_stock_data(stock_code, start_date='20200101', end_date=None, max_retries=3): """Download stock data via Tencent Finance API. Supports A-shares (6-digit) and HK stocks (5-digit).""" if not end_date: end_date = datetime.now().strftime('%Y-%m-%d') # Normalize date format start_fmt = start_date[:4] + '-' + start_date[4:6] + '-' + start_date[6:8] if len(start_date) == 8 else start_date # Detect market type # Strip existing sh/sz prefix first _clean = stock_code if stock_code[:2].lower() in ('sh', 'sz') and len(stock_code) > 2: _clean = stock_code[2:] is_hk = len(_clean) == 5 and _clean.isdigit() cache_key = f"{stock_code}_{start_fmt}_{end_date}" if cache_key in stock_data_cache: return stock_data_cache[cache_key], None import urllib.request last_err = None for attempt in range(max_retries): try: if is_hk: # HK stock: hkfqkline endpoint, 5-digit code, fields: date/open/close/high/low/volume param_code = f"hk{_clean}" url = f'https://web.ifzq.gtimg.cn/appstock/app/hkfqkline/get?param={param_code},day,,,800,qfq' req = urllib.request.Request(url, headers={ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' }) resp = urllib.request.urlopen(req, timeout=15) raw = resp.read().decode() # Strip JSONP wrapper if present if raw.startswith('kline_dayqfq'): raw = raw[raw.index('(') + 1:raw.rindex(')')] data_json = json.loads(raw) stock_data = data_json.get('data', {}).get(param_code, {}) klines = stock_data.get('qfqday') or stock_data.get('day', []) code_label = _clean else: # A-share: fqkline endpoint, 6-digit code with market prefix if _clean.startswith('6'): market = 'sh' elif _clean.startswith('0') or _clean.startswith('3'): market = 'sz' else: market = 'sh' param_code = f"{market}{_clean}" url = f'https://web.ifzq.gtimg.cn/appstock/app/fqkline/get?param={param_code},day,{start_fmt},{end_date},800,qfq' req = urllib.request.Request(url, headers={ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' }) resp = urllib.request.urlopen(req, timeout=15) data_json = json.loads(resp.read().decode()) stock_data = data_json.get('data', {}).get(param_code, {}) klines = stock_data.get('qfqday') or stock_data.get('day', []) code_label = f"{market.upper()}{_clean}" if not klines or not isinstance(klines, list): return None, f"No data found for stock {stock_code}" # Parse klines: A-share=[date, open, close, high, low, vol]; HK=[date, open, close, high, low, vol, ...] records = [] for line in klines: if isinstance(line, list): parts = line else: parts = str(line).split(',') if len(parts) >= 6: records.append({ 'timestamps': parts[0], 'open': float(parts[1]), 'close': float(parts[2]), 'high': float(parts[3]), 'low': float(parts[4]), 'volume': float(parts[5]) }) if not records: return None, f"No data found for stock {stock_code}" df = pd.DataFrame(records) df['timestamps'] = pd.to_datetime(df['timestamps']) df = df.sort_values('timestamps').reset_index(drop=True) df['amount'] = df['volume'] * df['close'] stock_data_cache[cache_key] = df return df, None except Exception as e: last_err = e if attempt < max_retries - 1: time_module.sleep(1 * (attempt + 1)) return None, f"数据下载失败(已重试{max_retries}次): {str(last_err)[:100]}" def _download_period_data(stock_code, period='m60', count=800): """Download K-line data for various periods via Tencent Finance API (replacing EastMoney). period: 'day'(day), 'week'(week), 'm60'(60min), 'm30'(30min) Returns DataFrame same format as _download_stock_data. """ import urllib.request if not stock_code.isdigit(): return None, "Invalid stock code" # Normalize code with market prefix if stock_code.startswith('6'): market = 'sh' elif stock_code.startswith('0') or stock_code.startswith('3'): market = 'sz' elif stock_code.startswith('8') or stock_code.startswith('4'): market = 'sz' # NEEQ else: market = 'sh' param_code = f"{market}{stock_code}" # Tencent API kline types: day, week, month, 60min, 30min, 15min, 5min, 1min klt_map = {'m60': '60min', 'm30': '30min', 'week': 'week', 'day': 'day'} klt = klt_map.get(period, '60min') url = f"https://web.ifzq.gtimg.cn/appstock/app/fqkline/get?param={param_code},{klt},,,,{count},qfq" try: req = urllib.request.Request(url, headers={ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' }) resp = urllib.request.urlopen(req, timeout=15) data_json = json.loads(resp.read().decode()) klines = data_json.get('data', {}).get(param_code, {}).get('qfqday', []) if not klines or not isinstance(klines, list): return None, f"No data found for {stock_code} (period={klt})" records = [] for k in klines: if not isinstance(k, list) or len(k) < 6: continue try: records.append({ 'timestamps': k[0], 'open': float(k[1]), 'close': float(k[2]), 'high': float(k[3]), 'low': float(k[4]), 'volume': float(k[5]), 'amount': float(k[6]) if len(k) > 6 else float(k[5]) * float(k[2]), }) except (ValueError, IndexError): continue if not records: return None, f"No valid records for {stock_code}" df = pd.DataFrame(records) df['timestamps'] = pd.to_datetime(df['timestamps']) df = df.sort_values('timestamps').reset_index(drop=True) if 'amount' not in df.columns: df['amount'] = df['volume'] * df['close'] return df, None except Exception as e: return None, f"Period data download failed: {str(e)[:100]}" def _load_csv_file(file_path): """Load CSV, ensure OHLCV + timestamps columns, return DataFrame.""" df = pd.read_csv(file_path) required_cols = ['open', 'high', 'low', 'close'] if not all(c in df.columns for c in required_cols): return None, f"Missing required columns: {required_cols}" for col in required_cols: df[col] = pd.to_numeric(df[col], errors='coerce') if 'volume' in df.columns: df['volume'] = pd.to_numeric(df['volume'], errors='coerce') if 'amount' in df.columns: df['amount'] = pd.to_numeric(df['amount'], errors='coerce') ts_col = None for candidate in ('timestamps', 'timestamp', 'date'): if candidate in df.columns: ts_col = candidate break if ts_col: df['timestamps'] = pd.to_datetime(ts_col) else: df['timestamps'] = pd.date_range(start='2024-01-01', periods=len(df), freq='1H') df = df.dropna(subset=['open', 'high', 'low', 'close']) return df, None def _load_csv_file_for_analysis(file_path): """Load CSV and return a clean df with DatetimeIndex for analysis.""" df, err = _load_csv_file(file_path) if err: return None, err df = df.set_index('timestamps').sort_index() if 'volume' not in df.columns: df['volume'] = 0 if 'amount' not in df.columns: df['amount'] = 0 return df, None def _load_data_files(): """Scan data directory for CSV files.""" data_dir = os.path.join(PROJECT_ROOT, 'data') files = [] if os.path.exists(data_dir): for f in os.listdir(data_dir): if f.endswith(('.csv', '.feather')): fp = os.path.join(data_dir, f) sz = os.path.getsize(fp) files.append({ 'name': f, 'path': fp, 'size': f"{sz/1024:.1f} KB" if sz < 1048576 else f"{sz/1048576:.1f} MB" }) return files # ============================================================ # Helper: chart builders # ============================================================ def _build_kline_chart(df, pred_df, lookback, pred_len, actual_df=None, start_idx=0, wave_analysis=None): """Build a candlestick chart with wave overlays, MA lines on historical data.""" if start_idx + lookback + pred_len <= len(df): hist_df = df.iloc[start_idx:start_idx + lookback] else: avail = min(lookback, len(df) - start_idx) hist_df = df.iloc[start_idx:start_idx + avail] fig = go.Figure() # Historical K-line (convert all to native float) h_x = [pd.Timestamp(x).isoformat() if not isinstance(x, str) else x for x in hist_df['timestamps'].tolist()] h_close = [float(v) for v in hist_df['close'].tolist()] fig.add_trace(go.Candlestick( x=h_x, open=[float(v) for v in hist_df['open'].tolist()], high=[float(v) for v in hist_df['high'].tolist()], low=[float(v) for v in hist_df['low'].tolist()], close=h_close, name='Historical', increasing_line_color='#EF5350', decreasing_line_color='#26A69A' )) # MA moving averages on historical close (SMA 5/10/20/60) ma_cfg = [ (5, 'MA5', '#FF6B6B', 1.3), (10, 'MA10', '#4ECDC4', 1.3), (20, 'MA20', '#FFD93D', 1.3), (60, 'MA60', '#6C5CE7', 1.3), ] for period, label, color, width in ma_cfg: if len(h_close) >= period: ma_series = pd.Series(h_close).rolling(window=period, min_periods=period).mean() ma_vals = ma_series.tolist() ma_x = h_x[:len(ma_vals)] # Trim leading NaN portion while len(ma_vals) > 0 and (ma_vals[0] is None or (isinstance(ma_vals[0], float) and np.isnan(ma_vals[0]))): ma_vals.pop(0) ma_x.pop(0) if ma_vals: fig.add_trace(go.Scatter( x=ma_x, y=[float(v) for v in ma_vals], mode='lines', name=label, line=dict(color=color, width=width), opacity=0.85 )) # Prediction K-line — purple for up, yellow for down if pred_df is not None and len(pred_df) > 0: if len(hist_df) > 0: last_ts = pd.Timestamp(hist_df['timestamps'].iloc[-1]) prev_ts = pd.Timestamp(hist_df['timestamps'].iloc[-2]) if len(hist_df) > 1 else last_ts - pd.Timedelta(days=1) td = last_ts - prev_ts if td.total_seconds() <= 0: td = pd.Timedelta(days=1) pred_ts = [pd.Timestamp(last_ts + td * (i+1)).isoformat() for i in range(len(pred_df))] else: pred_ts = [str(i) for i in range(len(pred_df))] fig.add_trace(go.Candlestick( x=pred_ts, open=[float(v) for v in pred_df['open'].tolist()], high=[float(v) for v in pred_df['high'].tolist()], low=[float(v) for v in pred_df['low'].tolist()], close=[float(v) for v in pred_df['close'].tolist()], name='Prediction', increasing_line_color='#9C27B0', increasing_fillcolor='#CE93D8', decreasing_line_color='#F9A825', decreasing_fillcolor='#FFF176' )) # Wave overlays if wave_analysis: fib = wave_analysis.get('fibonacci_levels', {}) if fib: for label, value in fib.items(): if isinstance(value, (int, float)): fig.add_hline(y=float(value), line_dash='dash', line_color='#E040FB', opacity=0.6, annotation_text=f'{label}: {float(value):.2f}') sup = wave_analysis.get('support_level') res = wave_analysis.get('resistance_level') if isinstance(sup, (int, float)): fig.add_hline(y=float(sup), line_dash='dot', line_color='#2196F3', opacity=0.7, annotation_text=f'Support: {float(sup):.2f}') if isinstance(res, (int, float)): fig.add_hline(y=float(res), line_dash='dot', line_color='#F44336', opacity=0.7, annotation_text=f'Resistance: {float(res):.2f}') fig.update_layout( title='Kronos Enhanced Prediction (Wave Theory)', xaxis_title='Time', yaxis_title='Price', template='plotly_white', height=600, showlegend=True, xaxis_rangeslider_visible=False ) return json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder) def _build_main_chart(df, stock_code, wave_chart_data=None, buy_markers=None, sell_markers=None): """Build main candlestick chart with volume bars, wave overlays, and buy/sell markers.""" fig = go.Figure() # Format x-axis dates dates = [pd.Timestamp(x).isoformat() if not isinstance(x, str) else x for x in df['timestamps']] n = len(df) low_arr = df['low'].values.astype(float) high_arr = df['high'].values.astype(float) # Candlestick fig.add_trace(go.Candlestick( x=dates, open=df['open'].tolist(), high=df['high'].tolist(), low=df['low'].tolist(), close=df['close'].tolist(), name='K线', increasing_line_color='#EF5350', decreasing_line_color='#26A69A' )) # Volume bars colors = ['#EF5350' if c >= o else '#26A69A' for c, o in zip(df['close'], df['open'])] fig.add_trace(go.Bar( x=dates, y=df['volume'].tolist(), name='成交量', marker_color=colors, opacity=0.4, yaxis='y2' )) # 20-day and 60-day moving averages if len(df) >= 20: ma20 = df['close'].rolling(20).mean().dropna() fig.add_trace(go.Scatter( x=[dates[i] for i in range(len(df)) if i in ma20.index], y=ma20.tolist(), mode='lines', name='MA20', line=dict(color='#FF9800', width=1) )) if len(df) >= 60: ma60 = df['close'].rolling(60).mean().dropna() fig.add_trace(go.Scatter( x=[dates[i] for i in range(len(df)) if i in ma60.index], y=ma60.tolist(), mode='lines', name='MA60', line=dict(color='#2196F3', width=1) )) # Wave theory overlays: annotations & shapes all_annotations = [] all_shapes = [] if wave_chart_data: all_annotations.extend(wave_chart_data.get('annotations', [])) all_shapes.extend(wave_chart_data.get('shapes', [])) # Buy markers: three visual styles by signal type if buy_markers: for m in buy_markers: idx = m.get('index', -1) if 0 <= idx < n: y_pos = low_arr[max(0, idx-1):min(n, idx+2)].min() * 0.985 label = m.get('label', '') if '倍量关注' in label: # 倍量关注: 粉红色圆点, 无文字 all_annotations.append({ 'x': dates[idx], 'y': y_pos, 'text': '', 'showarrow': True, 'arrowhead': 0, 'arrowcolor': '#FF80AB', 'arrowsize': 1.2, 'arrowwidth': 0.1, 'font': {'size': 1, 'color': 'rgba(0,0,0,0)'}, 'ax': 0, 'ay': 25, }) elif '倍量' in label: # 倍量买入/倍量强势: 红色三角形 + 文字 all_annotations.append({ 'x': dates[idx], 'y': y_pos, 'text': '倍量买入', 'showarrow': True, 'arrowhead': 2, 'arrowcolor': '#FF1744', 'arrowsize': 1.5, 'arrowwidth': 2, 'font': {'size': 11, 'color': '#FF1744', 'family': 'Microsoft YaHei, sans-serif'}, 'ax': 0, 'ay': 40, 'bordercolor': '#FF1744', 'borderwidth': 1, 'borderpad': 2, }) else: # E+买入/踏浪之道等: 深红色箭头 + 文字 (过滤弱信号) score_val = m.get('score', 0) if '待确认' in label and score_val < 15: continue # 过滤踏浪之道低分信号 if 'E+买入' in label: continue # E+买入信号过于频繁且参考意义不大,完全删除 display_text = label if label else '买入' all_annotations.append({ 'x': dates[idx], 'y': y_pos, 'text': display_text, 'showarrow': True, 'arrowhead': 2, 'arrowcolor': '#B71C1C', 'arrowsize': 1.5, 'arrowwidth': 2, 'font': {'size': 11, 'color': '#B71C1C', 'family': 'Microsoft YaHei, sans-serif'}, 'ax': 0, 'ay': 40, 'bordercolor': '#B71C1C', 'borderwidth': 1, 'borderpad': 2, }) # Sell markers: green triangle arrows above K-line (波段高位等, 过滤弱信号) if sell_markers: for m in sell_markers: idx = m.get('index', -1) if 0 <= idx < n: score_val = m.get('score', 0) label = m.get('label', '') if 'E+卖出' in label and score_val > 45: continue # 过滤E+卖出弱信号(只保留<45的强卖出) y_pos = high_arr[max(0, idx-1):min(n, idx+2)].max() * 1.015 all_annotations.append({ 'x': dates[idx], 'y': y_pos, 'text': label, 'showarrow': True, 'arrowhead': 2, 'arrowcolor': '#00E676', 'arrowsize': 1.5, 'arrowwidth': 2, 'font': {'size': 12, 'color': '#00E676', 'family': 'Microsoft YaHei, sans-serif'}, 'ax': 0, 'ay': -40, 'bordercolor': '#00E676', 'borderwidth': 1, 'borderpad': 2, }) if all_annotations or all_shapes: fig.update_layout(annotations=all_annotations, shapes=all_shapes) fig.update_layout( title=f'{stock_code} K线走势', template='plotly_dark', height=500, margin=dict(l=50, r=50, t=50, b=40), xaxis_rangeslider_visible=False, yaxis=dict(title='价格', side='right', domain=[0.25, 1]), yaxis2=dict(title='成交量', side='left', domain=[0, 0.2], showgrid=False), legend=dict(orientation='h', y=1.12, x=0.5, xanchor='center', font=dict(size=10)), showlegend=True ) return json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder) def _compute_v5_signals_on_history(df): """基于方案E+(v5联合投票+动量过滤+市场状态自适应)在历史K线上计算信号, 生成BUY/SELL标记 返回: (buy_markers, sell_markers, v5_scores_list) 方案E+: 市场状态自适应(ADX) + v5优化权重 + v4联合投票 + 动量过滤 回测准确率: 50.4% (6只个股, 2024-01~2025-05), 自适应目标准确率>62% """ import numpy as np n = len(df) if n < 35: return [], [], [] close = df['close'].values.astype(float) high = df['high'].values.astype(float) low = df['low'].values.astype(float) volume = df['volume'].values.astype(float) timestamps = df['timestamps'].tolist() if 'timestamps' in df.columns else list(range(n)) # 基础参数(震荡市默认) weights = {'MA': 0.1713, 'MACD': 0.1567, 'KDJ': 0.1071, 'RSI': 0.1175, 'BOLL': 0.2939, 'VP': 0.1214, 'MOM': 0.0321} buy_th, sell_th = 60, 40 v4_weight = 1.5 signal_boost = 1.0 mom_buy_min, mom_sell_max = 30, 70 # 市场状态自适应(只在完整数据上检测) current_regime = 'RANGE' try: if n >= 60: from market_regime import detect_market_regime, get_adaptive_params regime_result = detect_market_regime(close, high, low, volume) adaptive = get_adaptive_params(regime_result) weights = dict(adaptive['weights']) buy_th = adaptive['buy_threshold'] sell_th = adaptive['sell_threshold'] v4_weight = adaptive['v4_vote_weight'] signal_boost = adaptive['signal_boost'] mom_buy_min = adaptive['momentum_filter_buy'] mom_sell_max = adaptive['momentum_filter_sell'] current_regime = adaptive['regime'] except Exception: pass # 自适应失败则使用默认参数 # 预计算KDJ kk = np.full(n, 50.0); dd = np.full(n, 50.0); jj = np.full(n, 50.0) for i in range(n): s = max(0, i - 8) h9 = high[s:i+1].max(); l9 = low[s:i+1].min() rsv = (close[i] - l9) / (h9 - l9) * 100 if h9 != l9 else 50 if rsv != rsv: continue # NaN kk[i] = 2/3 * kk[i-1] + 1/3 * rsv if i > 0 else 50 dd[i] = 2/3 * dd[i-1] + 1/3 * kk[i] if i > 0 else 50 jj[i] = 3 * kk[i] - 2 * dd[i] # 预计算MACD(5,10,3) ef = close[0]; es = close[0] dif = np.zeros(n); dea = np.zeros(n) for i in range(n): dif[i] = ef - es if i < n - 1: ef = ef * 4/6 + close[i+1] * 2/6 es = es * 9/11 + close[i+1] * 2/11 for i in range(1, n): dea[i] = dea[i-1] * 2/4 + dif[i] * 2/4 # 预计算RSI(14) delta = np.diff(close, prepend=close[0]) gain = np.where(delta > 0, delta, 0) loss = np.where(delta < 0, -delta, 0) rsi = np.full(n, 50.0) for i in range(14, n): ag = gain[i-13:i+1].mean(); al = loss[i-13:i+1].mean() rsi[i] = 100 if al == 0 else 100 - 100/(1+ag/al) # 预计算BOLL(20,2) bup = np.full(n, np.nan); bdn = np.full(n, np.nan) for i in range(19, n): w = close[i-19:i+1]; m = w.mean(); s = w.std() bup[i] = m + 2*s; bdn[i] = m - 2*s buy_markers = [] sell_markers = [] all_scores = [] for i in range(34, n): # 需要至少35根K线 # MA信号 ma5 = close[max(0,i-4):i+1].mean() ma35 = close[max(0,i-34):i+1].mean() ma_score = np.clip(50 + (ma5/ma35 - 1)*500, 0, 100) if ma35 > 0 else 50 # MACD信号 dn = dea[i] macd_score = np.clip(50 + (dif[i]/dn*25 + np.sign(dif[i]-dif[max(0,i-1)])*10), 0, 100) if abs(dn) > 1e-10 else 50 # KDJ信号 dv2 = dd[i] kdj_score = np.clip((jj[i]/dv2)*25+25, 0, 100) if abs(dv2) > 1e-10 else 50 # RSI信号 rsi_score = np.clip(rsi[i], 0, 100) # BOLL信号 (正向计算, 后面反转) if not np.isnan(bup[i]) and not np.isnan(bdn[i]) and bup[i] != bdn[i]: boll_score = np.clip((close[i]-bdn[i])/(bup[i]-bdn[i])*100, 0, 100) else: boll_score = 50 boll_score_reversed = 100 - boll_score # 反向 # VP信号 if volume[max(0,i-1)] > 0: vc = volume[i]/volume[max(0,i-1)] pc = close[i]/close[max(0,i-1)] - 1 if close[max(0,i-1)] > 0 else 0 vp_score = np.clip(50 + pc*500 + (vc-1)*100, 0, 100) else: vp_score = 50 # MOM信号 if i >= 5: mom_score = np.clip((close[i]/close[i-5]-1)*1000+50, 0, 100) else: mom_score = 50 # 方案E: v5加权评分 composite = (ma_score * weights['MA'] + macd_score * weights['MACD'] + kdj_score * weights['KDJ'] + rsi_score * weights['RSI'] + boll_score_reversed * weights['BOLL'] + vp_score * weights['VP'] + mom_score * weights['MOM']) # v5信号 v5_signal = 'BUY' if composite > buy_th else ('SELL' if composite < sell_th else 'HOLD') # v4模拟评分 v4_sim = (ma_score*0.3 + macd_score*0.3*0.3 + kdj_score*0.3*0.3 + rsi_score*0.3*0.3 + vp_score*0.2 + mom_score*0.25 + (boll_score_reversed + ma_score)/2*0.1 + 50*0.15) # 联合投票 (v5权重2*boost, v4权重自适应) votes = {'BUY': 0, 'SELL': 0, 'HOLD': 0} if v5_signal == 'BUY': votes['BUY'] += 2.0 * signal_boost elif v5_signal == 'SELL': votes['SELL'] += 2.0 * signal_boost else: votes['HOLD'] += 1.0 if v4_sim > 60: votes['BUY'] += v4_weight elif v4_sim < 40: votes['SELL'] += v4_weight else: votes['HOLD'] += 0.5 final_signal = max(votes, key=votes.get) # 动量过滤: 动态阈值 mom_filtered = False if final_signal == 'BUY' and mom_score < mom_buy_min: final_signal = 'HOLD'; mom_filtered = True elif final_signal == 'SELL' and mom_score > mom_sell_max: final_signal = 'HOLD'; mom_filtered = True total_votes = sum(votes.values()) confidence = round(votes[final_signal]/total_votes*100, 1) if total_votes > 0 else 50 all_scores.append({'index': i, 'date': timestamps[i], 'score': round(composite, 1), 'close': close[i], 'signal': final_signal, 'v4_sim': round(v4_sim, 1), 'mom_score': round(mom_score, 1), 'mom_filtered': mom_filtered, 'confidence': confidence}) if final_signal == 'BUY': label = 'E+买入' + ('(MOM过滤)' if mom_filtered else '') buy_markers.append({ 'index': int(i), 'date': timestamps[i], 'price': round(close[i], 2), 'label': label, 'score': round(composite, 1), 'confidence': confidence }) elif final_signal == 'SELL': label = 'E+卖出' + ('(MOM过滤)' if mom_filtered else '') sell_markers.append({ 'index': int(i), 'date': timestamps[i], 'price': round(close[i], 2), 'label': label, 'score': round(composite, 1), 'confidence': confidence }) return buy_markers, sell_markers, all_scores def _build_kline_chart_enhanced(df, pred_df, lookback, pred_len, actual_df=None, start_idx=0, wave_analysis=None, v5_buy_markers=None, v5_sell_markers=None, v5_last_score=None): """增强版预测K线图: 在预测区域加入v5动量信号, 改进预测斜率 pred_df的close趋势根据v5信号方向和强度进行调整 """ if start_idx + lookback + pred_len <= len(df): hist_df = df.iloc[start_idx:start_idx + lookback] else: avail = min(lookback, len(df) - start_idx) hist_df = df.iloc[start_idx:start_idx + avail] fig = go.Figure() # Historical K-line h_x = [pd.Timestamp(x).isoformat() if not isinstance(x, str) else x for x in hist_df['timestamps'].tolist()] h_close = [float(v) for v in hist_df['close'].tolist()] fig.add_trace(go.Candlestick( x=h_x, open=[float(v) for v in hist_df['open'].tolist()], high=[float(v) for v in hist_df['high'].tolist()], low=[float(v) for v in hist_df['low'].tolist()], close=h_close, name='Historical', increasing_line_color='#EF5350', decreasing_line_color='#26A69A' )) # MA lines on historical ma_cfg = [ (5, 'MA5', '#FF6B6B', 1.3), (10, 'MA10', '#4ECDC4', 1.3), (20, 'MA20', '#FFD93D', 1.3), (60, 'MA60', '#6C5CE7', 1.3), ] for period, label, color, width in ma_cfg: if len(h_close) >= period: ma_series = pd.Series(h_close).rolling(window=period, min_periods=period).mean() ma_vals = ma_series.tolist() ma_x = h_x[:len(ma_vals)] while len(ma_vals) > 0 and (ma_vals[0] is None or (isinstance(ma_vals[0], float) and np.isnan(ma_vals[0]))): ma_vals.pop(0); ma_x.pop(0) if ma_vals: fig.add_trace(go.Scatter( x=ma_x, y=[float(v) for v in ma_vals], mode='lines', name=label, line=dict(color=color, width=width), opacity=0.85 )) # v5买入/卖出标记 on historical K-line all_annotations = [] low_arr = [float(v) for v in hist_df['low'].tolist()] high_arr = [float(v) for v in hist_df['high'].tolist()] if v5_buy_markers: for m in v5_buy_markers: idx = m.get('index', -1) - start_idx # 转为相对索引 if 0 <= idx < len(h_close): y_pos = min(low_arr[max(0,idx-1):min(len(low_arr),idx+2)]) * 0.985 score_str = str(m.get('score', '')) all_annotations.append({ 'x': h_x[idx], 'y': y_pos, 'text': f"E+ {score_str}", 'showarrow': True, 'arrowhead': 2, 'arrowcolor': '#B71C1C', 'arrowsize': 1.5, 'arrowwidth': 2, 'font': {'size': 10, 'color': '#B71C1C', 'family': 'Microsoft YaHei'}, 'ax': 0, 'ay': 35, 'bordercolor': '#B71C1C', 'borderwidth': 1, 'borderpad': 1, }) if v5_sell_markers: for m in v5_sell_markers: idx = m.get('index', -1) - start_idx if 0 <= idx < len(h_close): y_pos = max(high_arr[max(0,idx-1):min(len(high_arr),idx+2)]) * 1.015 score_str = str(m.get('score', '')) all_annotations.append({ 'x': h_x[idx], 'y': y_pos, 'text': f"E- {score_str}", 'showarrow': True, 'arrowhead': 2, 'arrowcolor': '#00E676', 'arrowsize': 1.5, 'arrowwidth': 2, 'font': {'size': 10, 'color': '#00E676', 'family': 'Microsoft YaHei'}, 'ax': 0, 'ay': -35, 'bordercolor': '#00E676', 'borderwidth': 1, 'borderpad': 1, }) # === 预测K线增强 === if pred_df is not None and len(pred_df) > 0: # 生成预测时间轴 if len(hist_df) > 0: last_ts = pd.Timestamp(hist_df['timestamps'].iloc[-1]) prev_ts = pd.Timestamp(hist_df['timestamps'].iloc[-2]) if len(hist_df) > 1 else last_ts - pd.Timedelta(days=1) td = last_ts - prev_ts if td.total_seconds() <= 0: td = pd.Timedelta(days=1) pred_ts = [pd.Timestamp(last_ts + td * (i+1)).isoformat() for i in range(len(pred_df))] else: pred_ts = [str(i) for i in range(len(pred_df))] # === v5动量调整预测 === pred_close = [float(v) for v in pred_df['close'].tolist()] last_hist_close = h_close[-1] if h_close else pred_close[0] if v5_last_score is not None: score = v5_last_score # 根据v5评分调整预测斜率 if score > 70: # 强看多: 放大上涨幅度 momentum_factor = 1.0 + (score - 60) / 200 # 1.05~1.2 elif score > 60: momentum_factor = 1.0 + (score - 60) / 400 # 1.0~1.025 elif score < 30: # 强看空: 放大下跌幅度 momentum_factor = -0.3 - (40 - score) / 200 # 趋向更负 elif score < 40: momentum_factor = -0.1 - (40 - score) / 300 else: momentum_factor = 0 # 中性, 保持原预测 # 应用动量调整: 以最后一根历史K线为基准, 线性插值调整 base_trend = (pred_close[-1] - last_hist_close) / max(len(pred_close), 1) if pred_close else 0 adjusted_pred = [] for j, pc in enumerate(pred_close): progress = (j + 1) / max(len(pred_close), 1) adjustment = momentum_factor * base_trend * progress adjusted = last_hist_close + (pc - last_hist_close) + adjustment * last_hist_close * 0.01 adjusted_pred.append(max(adjusted, pc * 0.9)) # 不低于原预测90% pred_close = adjusted_pred # 重构pred_df的open/high/low以匹配调整后的close adj_pred_open = [] adj_pred_high = [] adj_pred_low = [] for j in range(len(pred_df)): orig_open = float(pred_df['open'].iloc[j]) orig_high = float(pred_df['high'].iloc[j]) orig_low = float(pred_df['low'].iloc[j]) ratio = pred_close[j] / max(float(pred_df['close'].iloc[j]), 1e-10) adj_pred_open.append(orig_open * ratio) adj_pred_high.append(orig_high * ratio) adj_pred_low.append(orig_low * ratio) # v5信号方向箭头 (在预测区域起始位置) if v5_last_score is not None: if v5_last_score > 60: all_annotations.append({ 'x': pred_ts[0], 'y': min(adj_pred_low) * 0.97, 'text': f'PlanE BUY({v5_last_score})', 'showarrow': True, 'arrowhead': 3, 'arrowcolor': '#FF1744', 'arrowsize': 2, 'arrowwidth': 2.5, 'font': {'size': 12, 'color': '#FF1744', 'family': 'Microsoft YaHei'}, 'ax': 0, 'ay': 40, }) elif v5_last_score < 40: all_annotations.append({ 'x': pred_ts[0], 'y': max(adj_pred_high) * 1.03, 'text': f'PlanE SELL({v5_last_score})', 'showarrow': True, 'arrowhead': 3, 'arrowcolor': '#00E676', 'arrowsize': 2, 'arrowwidth': 2.5, 'font': {'size': 12, 'color': '#00E676', 'family': 'Microsoft YaHei'}, 'ax': 0, 'ay': -40, }) else: all_annotations.append({ 'x': pred_ts[0], 'y': max(adj_pred_high) * 1.03, 'text': f'PlanE HOLD({v5_last_score})', 'showarrow': True, 'arrowhead': 1, 'arrowcolor': '#FFD93D', 'arrowsize': 1.5, 'arrowwidth': 2, 'font': {'size': 11, 'color': '#FFD93D', 'family': 'Microsoft YaHei'}, 'ax': 0, 'ay': -35, }) # 动量趋势线 (在预测区域画一条趋势线) if len(pred_close) > 2: fig.add_trace(go.Scatter( x=[pred_ts[0], pred_ts[-1]], y=[pred_close[0], pred_close[-1]], mode='lines', name='PlanE趋势', line=dict(color='#42A5F5', width=2, dash='dash'), opacity=0.8 )) fig.add_trace(go.Candlestick( x=pred_ts, open=adj_pred_open, high=adj_pred_high, low=adj_pred_low, close=pred_close, name='Prediction(PlanE)', increasing_line_color='#9C27B0', increasing_fillcolor='#CE93D8', decreasing_line_color='#F9A825', decreasing_fillcolor='#FFF176' )) # Wave overlays if wave_analysis: fib = wave_analysis.get('fibonacci_levels', {}) if fib: for label, value in fib.items(): if isinstance(value, (int, float)): fig.add_hline(y=float(value), line_dash='dash', line_color='#E040FB', opacity=0.6, annotation_text=f'{label}: {float(value):.2f}') sup = wave_analysis.get('support_level') res = wave_analysis.get('resistance_level') if isinstance(sup, (int, float)): fig.add_hline(y=float(sup), line_dash='dot', line_color='#2196F3', opacity=0.7, annotation_text=f'Support: {float(sup):.2f}') if isinstance(res, (int, float)): fig.add_hline(y=float(res), line_dash='dot', line_color='#F44336', opacity=0.7, annotation_text=f'Resistance: {float(res):.2f}') if all_annotations: fig.update_layout(annotations=all_annotations) title_suffix = '' if v5_last_score is not None: sig = 'BUY' if v5_last_score > 60 else ('SELL' if v5_last_score < 40 else 'HOLD') title_suffix = f' | v5: {sig}({v5_last_score})' fig.update_layout( title=f'Kronos Enhanced Prediction (Wave + Plan-E){title_suffix}', xaxis_title='Time', yaxis_title='Price', template='plotly_white', height=600, showlegend=True, xaxis_rangeslider_visible=False ) return json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder) def _build_tech_signals(indicators_df): """构建完整的技术指标信号(12个固定参数指标)""" ts = {} def _last(col): s = indicators_df[col].dropna() return float(s.iloc[-1]) if len(s) > 0 else None def _prev(col): s = indicators_df[col].dropna() return float(s.iloc[-2]) if len(s) >= 2 else None # MACD(5,10,3) m, ms, mh = _last('macd'), _last('macd_signal'), _last('macd_hist') mp, msp = _prev('macd'), _prev('macd_signal') if m is not None and ms is not None: cross = None if mp is not None and msp is not None: if mp <= msp and m > ms: cross = 'gold' elif mp >= msp and m < ms: cross = 'dead' ts['macd'] = {'dif': round(m, 4), 'dea': round(ms, 4), 'hist': round(mh, 4) if mh else 0, 'signal_text': '金叉' if cross=='gold' else '死叉' if cross=='dead' else ('多头' if m > ms else '空头'), 'params': '(5,10,3)'} # KDJ(3,3,3) kv, dv, jv = _last('k'), _last('d'), _last('j') kp, dp = _prev('k'), _prev('d') if kv is not None and dv is not None: cross = None if kp is not None and dp is not None: if kp <= dp and kv > dv: cross = 'gold' elif kp >= dp and kv < dv: cross = 'dead' over = '超买' if kv > 80 else '超卖' if kv < 20 else '正常' ts['kdj'] = {'k': round(kv, 2), 'd': round(dv, 2), 'j': round(jv, 2) if jv else None, 'zone': over, 'cross': cross, 'params': '(3,3,3)'} # EMA均线系统 e5, e10, e20, e60 = _last('ema_5'), _last('ema_10'), _last('ema_20'), _last('ema_60') close_v = _last('close') if e5 is not None and close_v is not None: align = '多头排列' if all([e5, e10, e20, e60]) and e5 >= e10 >= e20 >= e60 else \ '空头排列' if all([e5, e10, e20, e60]) and e5 <= e10 <= e20 <= e60 else '缠绕' ts['ema'] = {'ema5': round(e5, 2) if e5 else None, 'ema10': round(e10, 2) if e10 else None, 'ema20': round(e20, 2) if e20 else None, 'ema60': round(e60, 2) if e60 else None, 'align': align, 'params': '5/10/20/60'} # BOLL(20,2) bu, bm, bl = _last('bb_upper'), _last('bb_middle'), _last('bb_lower') bp = _last('bb_position') if bp is not None: zone = '超买区' if bp > 0.8 else '超卖区' if bp < 0.2 else '正常区' ts['boll'] = {'upper': round(bu, 2) if bu else None, 'middle': round(bm, 2) if bm else None, 'lower': round(bl, 2) if bl else None, 'position': round(bp, 3), 'zone': zone, 'params': '(20,2)'} # RSI(6,14) r6, r14 = _last('rsi_6'), _last('rsi_14') if r14 is not None: sig14 = '超买' if r14 > 70 else '超卖' if r14 < 30 else '中性' ts['rsi'] = {'rsi6': round(r6, 2) if r6 else None, 'rsi14': round(r14, 2), 'signal14': sig14, 'params': '6/14'} # CCI(20) cc = _last('cci') if cc is not None: zone = '强多头' if cc > 100 else '强空头' if cc < -100 else '正常' ts['cci'] = {'value': round(cc, 2), 'zone': zone, 'params': '(20)'} # 成交量体系 vr5 = _last('vol_ratio5') vol = _last('volume') if vr5 is not None: status = '放量' if vr5 > 1.5 else '缩量' if vr5 < 0.5 else '正常' ts['volume'] = {'ratio5': round(vr5, 2), 'volume': int(vol) if vol else None, 'status': status, 'params': '5/10日均量'} # OBV obv_v = _last('obv') obv_ma = _last('obv_ma10') if obv_v is not None and obv_ma is not None: ts['obv'] = {'trend': '流入' if obv_v > obv_ma else '流出', 'params': '默认'} # DMI(14,6) adx_v, pdi, mdi = _last('adx'), _last('plus_di'), _last('minus_di') if adx_v is not None: trend = '强趋势' if adx_v > 25 else '弱趋势' if adx_v > 20 else '无趋势' direction = '多头' if pdi and mdi and pdi > mdi else '空头' ts['dmi'] = {'adx': round(adx_v, 2), 'plus_di': round(pdi, 2) if pdi else None, 'minus_di': round(mdi, 2) if mdi else None, 'trend': trend, 'direction': direction, 'params': '(14,6)'} # SAR(0.02,0.2) sar_v = _last('sar') sar_sig = _last('sar_signal') if sar_v is not None: ts['sar'] = {'value': round(sar_v, 2), 'signal': '多头' if sar_sig == 1 else '空头', 'params': '(0.02,0.2)'} # MFI(14) mfi_v = _last('mfi') if mfi_v is not None: zone = '资金流入' if mfi_v > 80 else '资金流出' if mfi_v < 20 else '中性' ts['mfi'] = {'value': round(mfi_v, 2), 'zone': zone, 'params': '(14)'} return ts def _build_technical_charts(df, indicators_df): """Build technical indicator sub-charts.""" figs = {} # RSI if 'rsi_14' in indicators_df.columns: rsi_vals = indicators_df['rsi_14'].dropna() fig = go.Figure() fig.add_trace(go.Scatter( x=[pd.Timestamp(x).isoformat() if not isinstance(x, str) else x for x in rsi_vals.index], y=[float(v) for v in rsi_vals.tolist()], mode='lines', name='RSI(14)', line=dict(color='#667eea', width=1.5) )) fig.add_hline(y=70, line_dash='dash', line_color='#EF5350', opacity=0.5) fig.add_hline(y=30, line_dash='dash', line_color='#26A69A', opacity=0.5) fig.update_layout(title='RSI (14)', template='plotly_white', height=220, margin=dict(l=40, r=20, t=40, b=30), showlegend=False) figs['rsi'] = json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder) # MACD if all(c in indicators_df.columns for c in ('macd', 'macd_signal', 'macd_hist')): fig = go.Figure() hist_vals = indicators_df['macd_hist'].fillna(0) colors = ['#26A69A' if v >= 0 else '#EF5350' for v in hist_vals.tolist()] x_axis = [pd.Timestamp(x).isoformat() if not isinstance(x, str) else x for x in indicators_df.index] fig.add_trace(go.Bar(x=x_axis, y=[float(v) for v in hist_vals.tolist()], marker_color=colors, name='MACD Hist', opacity=0.6)) macd_vals = indicators_df['macd'].dropna() sig_vals = indicators_df['macd_signal'].dropna() fig.add_trace(go.Scatter(x=[pd.Timestamp(x).isoformat() if not isinstance(x, str) else x for x in macd_vals.index], y=[float(v) for v in macd_vals.tolist()], mode='lines', name='MACD', line=dict(color='#667eea', width=1.5))) fig.add_trace(go.Scatter(x=[pd.Timestamp(x).isoformat() if not isinstance(x, str) else x for x in sig_vals.index], y=[float(v) for v in sig_vals.tolist()], mode='lines', name='Signal', line=dict(color='#FF7043', width=1.5))) fig.update_layout(title='MACD', template='plotly_white', height=220, margin=dict(l=40, r=20, t=40, b=30), showlegend=True) figs['macd'] = json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder) # Bollinger Bands if all(c in indicators_df.columns for c in ('bb_upper', 'bb_middle', 'bb_lower')): fig = go.Figure() x_axis = [pd.Timestamp(x).isoformat() if not isinstance(x, str) else x for x in indicators_df.index] fig.add_trace(go.Scatter(x=x_axis, y=[float(v) for v in indicators_df['bb_upper'].tolist()], mode='lines', name='Upper', line=dict(width=1))) fig.add_trace(go.Scatter(x=x_axis, y=[float(v) for v in indicators_df['bb_lower'].tolist()], mode='lines', name='Lower', line=dict(width=1), fill='tonexty', fillcolor='rgba(102,126,234,0.1)')) fig.add_trace(go.Scatter(x=x_axis, y=[float(v) for v in indicators_df['bb_middle'].tolist()], mode='lines', name='Middle', line=dict(color='#667eea', width=1.5))) if 'close' in df.columns: fig.add_trace(go.Scatter(x=x_axis, y=[float(v) for v in df['close'].tolist()], mode='lines', name='Close', line=dict(color='black', width=1))) fig.update_layout(title='Bollinger Bands', template='plotly_white', height=220, margin=dict(l=40, r=20, t=40, b=30), showlegend=True) figs['bollinger'] = json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder) # Volume if 'volume' in df.columns: fig = go.Figure() x_axis = [pd.Timestamp(x).isoformat() if not isinstance(x, str) else x for x in df.index] colors = ['#26A69A' if float(c) >= float(o) else '#EF5350' for c, o in zip(df['close'].tolist(), df['open'].tolist())] fig.add_trace(go.Bar(x=x_axis, y=[float(v) for v in df['volume'].tolist()], marker_color=colors, opacity=0.7)) fig.update_layout(title='Volume', template='plotly_white', height=180, margin=dict(l=40, r=20, t=40, b=30), showlegend=False) figs['volume'] = json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder) return figs if 'rsi_14' in indicators_df.columns: fig = go.Figure() fig.add_trace(go.Scatter( x=indicators_df.index, y=indicators_df['rsi_14'], mode='lines', name='RSI(14)', line=dict(color='#667eea', width=1.5) )) fig.add_hline(y=70, line_dash='dash', line_color='#EF5350', opacity=0.5) fig.add_hline(y=30, line_dash='dash', line_color='#26A69A', opacity=0.5) fig.add_hrect(y0=30, y1=70, fillcolor='gray', opacity=0.05) fig.update_layout(title='RSI (14)', template='plotly_white', height=250, margin=dict(l=40, r=20, t=40, b=30), showlegend=False) figs['rsi'] = json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder) if all(c in indicators_df.columns for c in ('macd', 'macd_signal', 'macd_hist')): fig = go.Figure() colors = ['#26A69A' if v >= 0 else '#EF5350' for v in indicators_df['macd_hist'].fillna(0)] fig.add_trace(go.Bar(x=indicators_df.index, y=indicators_df['macd_hist'], marker_color=colors, name='MACD Hist', opacity=0.6)) fig.add_trace(go.Scatter(x=indicators_df.index, y=indicators_df['macd'], mode='lines', name='MACD', line=dict(color='#667eea', width=1.5))) fig.add_trace(go.Scatter(x=indicators_df.index, y=indicators_df['macd_signal'], mode='lines', name='Signal', line=dict(color='#FF7043', width=1.5))) fig.update_layout(title='MACD', template='plotly_white', height=250, margin=dict(l=40, r=20, t=40, b=30), showlegend=True) figs['macd'] = json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder) if all(c in indicators_df.columns for c in ('bb_upper', 'bb_middle', 'bb_lower')): fig = go.Figure() fig.add_trace(go.Scatter(x=indicators_df.index, y=indicators_df['bb_upper'], mode='lines', name='Upper', line=dict(width=1))) fig.add_trace(go.Scatter(x=indicators_df.index, y=indicators_df['bb_lower'], mode='lines', name='Lower', line=dict(width=1), fill='tonexty', fillcolor='rgba(102,126,234,0.1)')) fig.add_trace(go.Scatter(x=indicators_df.index, y=indicators_df['bb_middle'], mode='lines', name='Middle', line=dict(color='#667eea', width=1.5))) if 'close' in df.columns: fig.add_trace(go.Scatter(x=df.index, y=df['close'], mode='lines', name='Close', line=dict(color='black', width=1))) fig.update_layout(title='Bollinger Bands', template='plotly_white', height=250, margin=dict(l=40, r=20, t=40, b=30), showlegend=True) figs['bollinger'] = json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder) if 'volume' in df.columns: fig = go.Figure() colors = ['#26A69A' if c >= o else '#EF5350' for c, o in zip(df['close'], df['open'])] fig.add_trace(go.Bar(x=df.index, y=df['volume'], marker_color=colors, opacity=0.7)) fig.update_layout(title='Volume', template='plotly_white', height=200, margin=dict(l=40, r=20, t=40, b=30), showlegend=False) figs['volume'] = json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder) return figs # ============================================================ # Routes # ============================================================ @app.route('/api/wave-analysis', methods=['POST']) def wave_analysis_endpoint(): """Dedicated wave analysis endpoint supporting multi-period (day/m60/m30).""" try: data = request.get_json() stock_code = data.get('stock_code', '').strip() period = data.get('period', 'day') # 'day', 'm60', 'm30' if not stock_code: return jsonify({'error': 'Stock code is required'}), 400 # Download data according to period if period == 'day': df, err = _download_stock_data(stock_code) else: df, err = _download_period_data(stock_code, period=period) if err: return jsonify({'error': err}), 400 if len(df) < 30: return jsonify({'error': f'Insufficient data: only {len(df)} bars'}), 400 lookback = int(data.get('lookback', 0)) if lookback > 0 and len(df) > lookback: df = df.iloc[-lookback:].reset_index(drop=True) result = {} if WAVE_ANALYSIS_AVAILABLE: try: df_wv = df.set_index('timestamps') if 'timestamps' in df.columns else df wave_result = analyze_wave_patterns(df_wv) result = wave_result.get('analysis', wave_result) # Build chart with wave overlays wave_chart = { 'annotations': wave_result.get('annotations', []), 'shapes': wave_result.get('shapes', []) } # v5信号标记 (主图) _cp_buy, _cp_sell = [], [] try: v5_buy_m, v5_sell_m, v5_scores = _compute_v5_signals_on_history(df) _cp_buy.extend(v5_buy_m) _cp_sell.extend(v5_sell_m) except Exception as _v5e: pass chart = _build_main_chart(df, f"{stock_code}_{period}", wave_chart_data=wave_chart, buy_markers=_cp_buy, sell_markers=_cp_sell) except Exception as _wae: result = {'error': str(_wae)} chart = None else: result = {'error': 'wave_analysis module not loaded'} chart = None period_labels = {'day': '日线', 'm60': '60分钟', 'm30': '30分钟'} info = { 'stock_code': stock_code, 'period': period, 'period_label': period_labels.get(period, period), 'bars': len(df), 'start_date': str(df['timestamps'].min())[:10], 'end_date': str(df['timestamps'].max())[:10] } return jsonify({ 'success': True, 'data_info': info, 'wave_analysis': result, 'chart': chart }) except Exception as e: return jsonify({'error': str(e)}), 500 @app.route('/api/download-stock', methods=['POST']) def download_stock(): """Download stock data via AkShare by stock code.""" try: data = request.get_json() stock_code = data.get('stock_code', '').strip() if not stock_code: return jsonify({'error': 'Stock code is required'}), 400 if not stock_code.isdigit() or len(stock_code) not in (5, 6): return jsonify({'error': 'Invalid stock code format, expected 5 or 6 digits'}), 400 start_date = data.get('start_date', '20200101') lookback = int(data.get('lookback', 800)) df, err = _download_stock_data(stock_code, start_date=start_date) if err: return jsonify({'error': err}), 400 # Limit to last N rows for performance; lookback=0 means use all data if lookback > 0 and len(df) > lookback: df = df.iloc[-lookback:].reset_index(drop=True) info = { 'rows': len(df), 'stock_code': stock_code, 'lookback': lookback, 'start_date': df['timestamps'].min().isoformat(), 'end_date': df['timestamps'].max().isoformat(), 'price_range': { 'min': float(df[['open','high','low','close']].min().min()), 'max': float(df[['open','high','low','close']].max().max()) }, 'last_close': float(df['close'].iloc[-1]), 'enhanced_available': ENHANCED_AVAILABLE } # Run analysis if enhanced modules available analysis = {} if ENHANCED_AVAILABLE: try: df_analysis = df.set_index('timestamps').copy() if 'volume' not in df_analysis.columns: df_analysis['volume'] = 0 indicators_df = technical_analyzer.calculate_all_indicators(df_analysis.copy()) tech_signals = _build_tech_signals(indicators_df) vp_result = volume_price_analyzer.analyze_volume_price_relationship(df_analysis) vp_clean = {k: round(float(v), 4) if isinstance(v, (np.floating, float)) else v for k, v in vp_result.items()} wave_result = wave_analyzer.analyze_wave_patterns(df_analysis) wave_clean = {} for k, v in wave_result.items(): if isinstance(v, dict): wave_clean[k] = {kk: round(float(vv), 4) if isinstance(vv, (np.floating, float, np.integer, int)) else vv for kk, vv in v.items()} elif isinstance(v, (np.floating, float, np.integer, int)): wave_clean[k] = round(float(v), 4) elif isinstance(v, np.ndarray): wave_clean[k] = [round(float(x), 4) for x in v.tolist()] else: wave_clean[k] = v tech_charts = _build_technical_charts(df_analysis, indicators_df) analysis = { 'technical_signals': tech_signals, 'volume_price_analysis': vp_clean, 'wave_analysis': wave_clean, 'charts': tech_charts } # Classic pattern detection if CLASSIC_PATTERN_AVAILABLE: try: cp_result = detect_classic_patterns(df_analysis) analysis['classic_patterns'] = { 'summary': cp_result.get('summary', ''), 'score': cp_result.get('score', 0), 'overall_signal': cp_result.get('overall_signal', 'none'), 'detected_list': cp_result.get('detected_list', []), } except Exception as _cpe: analysis['classic_patterns'] = {'error': str(_cpe)} except Exception as e: print(f"[ENHANCED ERROR] {type(e).__name__}: {e}") analysis = {'error': str(e)} # Classic pattern detection (independent of enhanced modules) cp_buy_markers = [] cp_sell_markers = [] if CLASSIC_PATTERN_AVAILABLE: try: df_cp = df.set_index('timestamps') if 'timestamps' in df.columns else df cp_result = detect_classic_patterns(df_cp) cp_buy_markers = cp_result.get('buy_markers', []) cp_sell_markers = cp_result.get('sell_markers', []) # Extract all double_volume signals from _all_signals for chart markers patterns = cp_result.get('patterns', {}) dv_pattern = patterns.get('double_volume', {}) if dv_pattern.get('_all_signals'): for sig in dv_pattern['_all_signals']: idx = sig.get('index', -1) if idx >= 0: label = '倍量强势' if sig.get('signal') == 'bullish' else '倍量关注' cp_buy_markers.append({ 'index': int(idx), 'date': sig.get('date', ''), 'price': sig.get('price', 0), 'label': label, 'strength': sig.get('strength', 0), }) # Extract all distribution_top signals for chart markers dt_pattern = patterns.get('distribution_top', {}) if dt_pattern.get('_all_signals'): for sig in dt_pattern['_all_signals']: idx = sig.get('index', -1) if idx >= 0: cp_sell_markers.append({ 'index': int(idx), 'date': sig.get('date', ''), 'price': sig.get('price', 0), 'label': sig.get('label', '波段高位'), 'strength': sig.get('strength', 0), }) if isinstance(analysis, dict) and 'error' not in analysis: analysis['classic_patterns'] = { 'summary': cp_result.get('summary', ''), 'score': cp_result.get('score', 0), 'overall_signal': cp_result.get('overall_signal', 'none'), 'detected_list': cp_result.get('detected_list', []), } else: if not isinstance(analysis, dict): analysis = {} analysis['classic_patterns'] = { 'summary': cp_result.get('summary', ''), 'score': cp_result.get('score', 0), 'overall_signal': cp_result.get('overall_signal', 'none'), 'detected_list': cp_result.get('detected_list', []), } except Exception as _cpe: if isinstance(analysis, dict): analysis['classic_patterns'] = {'error': str(_cpe)} else: analysis = {'classic_patterns': {'error': str(_cpe)}} # Wyckoff VPA analysis (independent of enhanced modules) if WYCKOFF_VPA_AVAILABLE: try: df_wv = df.set_index('timestamps') if 'timestamps' in df.columns else df wv_result = detect_wyckoff_vpa(df_wv) if isinstance(analysis, dict): analysis['wyckoff_vpa'] = wv_result except Exception as _wve: analysis['wyckoff_vpa'] = {'error': str(_wve)} # Wave Theory analysis (independent of enhanced modules) wave_chart_data = None if WAVE_ANALYSIS_AVAILABLE: try: df_wv = df.set_index('timestamps') if 'timestamps' in df.columns else df wave_result = analyze_wave_patterns(df_wv) # wave_result = {'annotations':..., 'shapes':..., 'analysis':{waves, buy_signals, sell_signals, ...}} if isinstance(analysis, dict): analysis['wave_analysis'] = wave_result.get('analysis', wave_result) wave_chart_data = { 'annotations': wave_result.get('annotations', []), 'shapes': wave_result.get('shapes', []) } except Exception as _wae: analysis['wave_analysis'] = {'error': str(_wae)} # 踏浪之道 · 主升浪评分分析 if TALANG_AVAILABLE: try: df_tl = df.copy() df_tl['timestamps'] = df_tl.get('timestamps', df_tl.index) talang_result = talang_score_analysis(df_tl, stock_code) if talang_result.get('success'): # 序列化处理: 将numpy类型转为Python原生类型 def _talang_serialize(obj): if isinstance(obj, dict): return {k: _talang_serialize(v) for k, v in obj.items()} elif isinstance(obj, list): return [_talang_serialize(v) for v in obj] elif isinstance(obj, (np.bool_,)): return bool(obj) elif isinstance(obj, (np.integer,)): return int(obj) elif isinstance(obj, (np.floating,)): return float(obj) elif isinstance(obj, np.ndarray): return obj.tolist() return obj talang_result = _talang_serialize(talang_result) analysis['talang'] = talang_result # 将踏浪之道的买卖标注合并到经典形态标记中 for m in talang_result.get('buy_markers', []): cp_buy_markers.append(m) except Exception as _tle: analysis['talang'] = {'error': str(_tle)} # Extract HK stock name if applicable hk_name = '' if len(stock_code) == 5: try: import urllib.request as _urllib _url2 = f'https://web.ifzq.gtimg.cn/appstock/app/hkfqkline/get?param=hk{stock_code},day,,,1,qfq' _req2 = _urllib.Request(_url2, headers={'User-Agent': 'Mozilla/5.0'}) _resp2 = _urllib.urlopen(_req2, timeout=10) _raw2 = _resp2.read().decode() if _raw2.startswith('kline_dayqfq'): _raw2 = _raw2[_raw2.index('(')+1:_raw2.rindex(')')] _jd2 = json.loads(_raw2) _qt2 = _jd2.get('data', {}).get(f'hk{stock_code}', {}).get('qt', {}).get(f'hk{stock_code}', []) if isinstance(_qt2, list) and len(_qt2) > 1: hk_name = str(_qt2[1]) except Exception: pass # Build candlestick chart for the frontend try: # v5信号标记 (download-stock主图) try: v5_buy_m2, v5_sell_m2, _ = _compute_v5_signals_on_history(df) cp_buy_markers.extend(v5_buy_m2) cp_sell_markers.extend(v5_sell_m2) except Exception: pass chart = _build_main_chart(df, stock_code, wave_chart_data=wave_chart_data, buy_markers=cp_buy_markers, sell_markers=cp_sell_markers) except Exception as _ce: chart = None return jsonify({'success': True, 'data_info': info, 'analysis': analysis, 'hk_name': hk_name, 'chart': chart}) except Exception as e: return jsonify({'error': str(e)}), 500 @app.route('/') def index(): resp = make_response(render_template('index.html')) resp.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate' resp.headers['Pragma'] = 'no-cache' resp.headers['Expires'] = '0' return resp @app.route('/api/data-files') def get_data_files(): return jsonify(_load_data_files()) @app.route('/api/load-data', methods=['POST']) def load_data(): try: data = request.get_json() fp = data.get('file_path') if not fp: return jsonify({'error': 'File path is required'}), 400 df, err = _load_csv_file(fp) if err: return jsonify({'error': err}), 400 def detect_timeframe(df_): if len(df_) < 2: return 'Unknown' diffs = [(df_['timestamps'].iloc[i] - df_['timestamps'].iloc[i-1]) for i in range(1, min(10, len(df_)))] avg = sum(diffs, pd.Timedelta(0)) / len(diffs) if avg < pd.Timedelta(minutes=1): return f"{avg.total_seconds():.0f} seconds" elif avg < pd.Timedelta(hours=1): return f"{avg.total_seconds()/60:.0f} minutes" elif avg < pd.Timedelta(days=1): return f"{avg.total_seconds()/3600:.0f} hours" else: return f"{avg.days} days" info = { 'rows': len(df), 'columns': list(df.columns), 'start_date': df['timestamps'].min().isoformat(), 'end_date': df['timestamps'].max().isoformat(), 'price_range': { 'min': float(df[['open','high','low','close']].min().min()), 'max': float(df[['open','high','low','close']].max().max()) }, 'timeframe': detect_timeframe(df), 'enhanced_available': ENHANCED_AVAILABLE } return jsonify({'success': True, 'data_info': info}) except Exception as e: return jsonify({'error': str(e)}), 500 @app.route('/api/available-models') def get_available_models(): return jsonify({ 'models': AVAILABLE_MODELS, 'model_available': MODEL_AVAILABLE, 'enhanced_available': ENHANCED_AVAILABLE }) @app.route('/api/model-status') def get_model_status(): loaded = predictor is not None or enhanced_predictor is not None return jsonify({ 'available': MODEL_AVAILABLE, 'loaded': loaded, 'enhanced_available': ENHANCED_AVAILABLE, 'message': 'Model loaded' if loaded else 'No model loaded' }) @app.route('/api/load-model', methods=['POST']) def load_model(): global tokenizer, model, predictor, enhanced_predictor try: if not MODEL_AVAILABLE: return jsonify({'error': 'Kronos model library not available'}), 400 data = request.get_json() model_key = data.get('model_key', 'kronos-small') device = data.get('device', 'cpu') use_enhanced = data.get('use_enhanced', False) if model_key not in AVAILABLE_MODELS: return jsonify({'error': f'Unsupported model: {model_key}'}), 400 cfg = AVAILABLE_MODELS[model_key] tokenizer = KronosTokenizer.from_pretrained(cfg['tokenizer_id']) model = Kronos.from_pretrained(cfg['model_id']) predictor = KronosPredictor(model, tokenizer, device=device, max_context=cfg['context_length']) if use_enhanced and ENHANCED_AVAILABLE: try: enhanced_predictor = EnhancedKronosPredictor(model, tokenizer, device=device, max_context=cfg['context_length']) except Exception: enhanced_predictor = None else: enhanced_predictor = None return jsonify({ 'success': True, 'message': f'Model loaded: {cfg["name"]} ({cfg["params"]}) on {device}', 'model_info': { 'name': cfg['name'], 'params': cfg['params'], 'context_length': cfg['context_length'], 'enhanced': enhanced_predictor is not None } }) except Exception as e: return jsonify({'error': str(e)}), 500 @app.route('/api/enhanced-analysis', methods=['POST']) def enhanced_analysis(): """Run technical indicators + wave theory + volume-price analysis.""" if not ENHANCED_AVAILABLE: return jsonify({'error': 'Enhanced modules not available'}), 400 try: data = request.get_json() fp = data.get('file_path') if not fp: return jsonify({'error': 'File path is required'}), 400 df, err = _load_csv_file_for_analysis(fp) if err: return jsonify({'error': err}), 400 # 1. Technical indicators indicators_df = technical_analyzer.calculate_all_indicators(df.copy()) tech_signals = _build_tech_signals(indicators_df) # 2. Volume-price vp_result = volume_price_analyzer.analyze_volume_price_relationship(df) vp_clean = {k: round(float(v), 4) if isinstance(v, (np.floating, float)) else v for k, v in vp_result.items()} # 3. Wave theory wave_result = wave_analyzer.analyze_wave_patterns(df) wave_clean = {} for k, v in wave_result.items(): if isinstance(v, dict): wave_clean[k] = {kk: round(float(vv), 4) if isinstance(vv, (np.floating, float, np.integer, int)) else vv for kk, vv in v.items()} elif isinstance(v, (np.floating, float, np.integer, int)): wave_clean[k] = round(float(v), 4) elif isinstance(v, np.ndarray): wave_clean[k] = [round(float(x), 4) for x in v.tolist()] else: wave_clean[k] = v # 4. Charts tech_charts = _build_technical_charts(df, indicators_df) return jsonify({ 'success': True, 'technical_signals': tech_signals, 'volume_price': vp_clean, 'wave_theory': wave_clean, 'charts': tech_charts, }) except Exception as e: return jsonify({'error': str(e)}), 500 @app.route('/api/multi-dimension-analysis', methods=['POST']) def multi_dimension_analysis(): """多维度技术指标联动研判:12指标 + 5维度 + 信号共振""" if not ENHANCED_AVAILABLE or multidim_analyzer is None: return jsonify({'error': 'Enhanced modules not available'}), 400 try: data = request.get_json() stock_code = data.get('stock_code', '').strip() lookback = int(data.get('lookback', 250)) # 通过 _download_stock_data 获取日线数据 df, err = _download_stock_data(stock_code) if err or df is None: return jsonify({'error': f'获取数据失败: {err}'}), 400 # 按 lookback 截取 if lookback > 0 and len(df) > lookback: df = df.tail(lookback).reset_index(drop=True) if len(df) < 60: return jsonify({'error': '数据不足,至少需要60根K线'}), 400 # 执行多维度研判 result = multidim_analyzer.analyze(df) return jsonify({ 'success': True, 'multi_dimension': result, }) except Exception as e: import traceback traceback.print_exc() return jsonify({'error': str(e)}), 500 @app.route('/api/enhanced-predict', methods=['POST']) def enhanced_predict(): """Run prediction using enhanced predictor with wave theory.""" try: data = request.get_json() fp = data.get('file_path') if not fp: return jsonify({'error': 'File path is required'}), 400 df, err = _load_csv_file(fp) if err: return jsonify({'error': err}), 400 lookback = int(data.get('lookback', 400)) pred_len = int(data.get('pred_len', 120)) temperature = float(data.get('temperature', 1.0)) top_p = float(data.get('top_p', 0.9)) sample_count = int(data.get('sample_count', 1)) use_enhanced = data.get('use_enhanced', True) if len(df) < lookback: return jsonify({'error': f'Need at least {lookback} rows, got {len(df)}'}), 400 start_date = data.get('start_date') if start_date: start_dt = pd.to_datetime(start_date) mask = df['timestamps'] >= start_dt window_df = df[mask] if len(window_df) < lookback + pred_len: return jsonify({'error': 'Insufficient data in selected window'}), 400 x_df = window_df.iloc[:lookback] actual_df = window_df.iloc[lookback:lookback+pred_len] start_idx = df[mask].index[0] if len(df[mask]) > 0 else 0 else: x_df = df.iloc[:lookback] actual_df = df.iloc[lookback:lookback+pred_len] if len(df) >= lookback + pred_len else None start_idx = 0 required_cols = ['open', 'high', 'low', 'close'] if 'volume' in x_df.columns: required_cols.append('volume') x_input = x_df[required_cols].copy() x_timestamp = pd.Series(x_df['timestamps'].values) y_timestamp = x_timestamp # Run prediction wave_analysis = None if ENHANCED_AVAILABLE and use_enhanced and enhanced_predictor is not None: try: pred_df = enhanced_predictor.predict_enhanced( x_input, x_timestamp, y_timestamp, pred_len, T=temperature, top_p=top_p, sample_count=sample_count, verbose=False ) except Exception: if predictor is not None: pred_df = predictor.predict(x_input, x_timestamp, y_timestamp, pred_len, T=temperature, top_p=top_p, sample_count=sample_count) else: return jsonify({'error': 'No model available'}), 400 elif predictor is not None: pred_df = predictor.predict(x_input, x_timestamp, y_timestamp, pred_len, T=temperature, top_p=top_p, sample_count=sample_count) else: return jsonify({'error': 'No model loaded, please load a model first'}), 400 # Wave analysis on historical data if ENHANCED_AVAILABLE and use_enhanced: try: hist_for_wave = x_input.copy() hist_for_wave['timestamps'] = x_timestamp.values hist_for_wave = hist_for_wave.set_index('timestamps') if 'volume' not in hist_for_wave.columns: hist_for_wave['volume'] = 0 wave_raw = wave_analyzer.analyze_wave_patterns(hist_for_wave) wave_analysis = {} for k, v in wave_raw.items(): if isinstance(v, dict): wave_analysis[k] = {kk: round(float(vv), 4) if isinstance(vv, (np.floating, float, np.integer, int)) else vv for kk, vv in v.items()} elif isinstance(v, (np.floating, float, np.integer, int)): wave_analysis[k] = round(float(v), 4) else: wave_analysis[k] = v except Exception as e: wave_analysis = {'error': str(e)} # v5动量增强预测图 v5_buy_m3 = []; v5_sell_m3 = []; v5_last = None try: df_for_v5 = df.set_index('timestamps') if 'timestamps' in df.columns else df if V5_SCORER_AVAILABLE: v5r = _v5_mod.run_v5_optimized_analysis(stock_code, df_for_v5) v5_last = v5r.get('score', None) v5_buy_m3, v5_sell_m3, _ = _compute_v5_signals_on_history(df) except Exception: pass chart_json = _build_kline_chart_enhanced(df, pred_df, lookback, pred_len, actual_df, start_idx, wave_analysis, v5_buy_markers=v5_buy_m3, v5_sell_markers=v5_sell_m3, v5_last_score=v5_last) pred_results = [] for _, row in pred_df.iterrows(): pred_results.append({ 'open': float(row['open']), 'high': float(row['high']), 'low': float(row['low']), 'close': float(row['close']), 'volume': float(row['volume']) if 'volume' in row else 0 }) actual_data = [] if actual_df is not None: for _, row in actual_df.iterrows(): actual_data.append({ 'open': float(row['open']), 'high': float(row['high']), 'low': float(row['low']), 'close': float(row['close']), 'volume': float(row['volume']) if 'volume' in row else 0, 'timestamp': row['timestamps'].isoformat() if 'timestamps' in row else '' }) return jsonify({ 'success': True, 'chart': chart_json, 'prediction': pred_results, 'actual': actual_data, 'wave_analysis': wave_analysis, 'enhanced': use_enhanced and ENHANCED_AVAILABLE, 'has_comparison': len(actual_data) > 0 }) except Exception as e: return jsonify({'error': str(e)}), 500 @app.route('/api/fundamental-analysis', methods=['POST']) def fundamental_analysis_route(): """基本面分析API - AI Hedge Fund风格多Agent分析""" try: data = request.get_json() stock_code = data.get('stock_code', '').strip() if not stock_code: return jsonify({'error': '请输入股票代码'}), 400 # 清理股票代码 stock_code = stock_code.replace('.SH', '').replace('.SZ', '').replace('.sh', '').replace('.sz', '') if not FUNDAMENTAL_AVAILABLE: return jsonify({'error': '基本面分析模块未加载'}), 503 result = run_fundamental_analysis(stock_code) return jsonify(result) except Exception as e: return jsonify({'error': f'分析失败: {str(e)}'}), 500 @app.route('/api/joint-prediction', methods=['POST']) def joint_prediction(): """联合预测: 方案E+ - v5优化权重 + v4联合投票 + 动量过滤 + 市场状态自适应 回测准确率50.4%, 6只个股, 2024-01~2025-05 E+: ADX趋势检测 + 动态阈值/权重/信号增强 + v5信号权重2 + v4权重自适应 """ try: data = request.get_json() stock_code = data.get('stock_code', '').strip() if not stock_code: return jsonify({'error': '请输入股票代码'}), 400 results = {} # 0. 市场状态检测 regime_result = None adaptive_params = {} try: df_regime, _ = _download_stock_data(stock_code) if df_regime is not None and len(df_regime) >= 60: from market_regime import detect_market_regime, get_adaptive_params as _gap regime_result = detect_market_regime( df_regime['close'].values.astype(float), df_regime['high'].values.astype(float), df_regime['low'].values.astype(float), df_regime['volume'].values.astype(float) ) adaptive_params = _gap(regime_result) results['market_regime'] = { 'regime': regime_result['regime'], 'regime_label': regime_result['regime_label'], 'adx': regime_result['adx'], 'di_plus': regime_result['di_plus'], 'di_minus': regime_result['di_minus'], 'atr_pct': regime_result['atr_pct'], 'volatility': regime_result['volatility'], 'volume_state': regime_result['volume_state'], 'streak': regime_result['streak'], 'price_vs_ma60': regime_result['price_vs_ma60'], } except Exception as e: results['regime_error'] = str(e)[:100] # 1. v5优化评分 if V5_SCORER_AVAILABLE: try: df_v5, err_v5 = _download_stock_data(stock_code) if not err_v5 and df_v5 is not None: if 'timestamps' in df_v5.columns: df_v5 = df_v5.set_index('timestamps') v5_result = _v5_mod.run_v5_optimized_analysis(stock_code, df_v5) results['v5_score'] = v5_result['score'] results['v5_signal'] = v5_result.get('signal', 'HOLD') results['v5_details'] = v5_result.get('v5_details', {}) except Exception as e: results['v5_error'] = str(e)[:100] # 2. 方案E+联合投票 + 动量过滤 + 市场状态自适应 try: if V5_SCORER_AVAILABLE: v5_sc = results.get('v5_score', 50) v5_sig = results.get('v5_signal', 'HOLD') mom_val = results.get('v5_details', {}).get('details', {}).get('MOM', {}).get('raw_score', 50) v4_sc = results.get('v4_score', 50) # 自适应参数 a_boost = adaptive_params.get('signal_boost', 1.0) if adaptive_params else 1.0 a_v4w = adaptive_params.get('v4_vote_weight', 1.5) if adaptive_params else 1.5 a_mom_buy = adaptive_params.get('momentum_filter_buy', 30) if adaptive_params else 30 a_mom_sell = adaptive_params.get('momentum_filter_sell', 70) if adaptive_params else 70 # 联合投票 (v5权重2*boost, v4权重自适应) votes_e = {'BUY': 0, 'SELL': 0, 'HOLD': 0} if v5_sig == 'BUY': votes_e['BUY'] += 2.0 * a_boost elif v5_sig == 'SELL': votes_e['SELL'] += 2.0 * a_boost else: votes_e['HOLD'] += 1.0 if v4_sc > 60: votes_e['BUY'] += a_v4w elif v4_sc < 40: votes_e['SELL'] += a_v4w else: votes_e['HOLD'] += 0.5 e_signal = max(votes_e, key=votes_e.get) total_v = sum(votes_e.values()) e_conf = round(votes_e[e_signal]/total_v*100, 1) if total_v > 0 else 50 # 动量过滤(动态阈值) mom_filtered = False if e_signal == 'BUY' and mom_val < a_mom_buy: e_signal = 'HOLD'; mom_filtered = True elif e_signal == 'SELL' and mom_val > a_mom_sell: e_signal = 'HOLD'; mom_filtered = True trend_map = {'BUY': '看涨', 'SELL': '看跌', 'HOLD': '震荡'} regime_label = results.get('market_regime', {}).get('regime_label', '') plan_note = f'方案E+: v5联合投票+动量过滤+市场状态自适应(50.4%回测准确率)' if regime_label: plan_note += f' [{regime_label}]' results['plan_e'] = { 'signal': e_signal, 'trend_direction': trend_map.get(e_signal, '震荡'), 'confidence': e_conf, 'v5_score': v5_sc, 'v5_signal': v5_sig, 'v4_score': v4_sc, 'v4_weight': a_v4w, 'mom_score': mom_val, 'mom_filtered': mom_filtered, 'signal_boost': a_boost, 'votes': {k: round(v, 1) for k, v in votes_e.items()}, 'source': 'plan_e_plus', 'regime': results.get('market_regime', {}), 'note': plan_note, } results['ft_prediction'] = results['plan_e'] # E+共振信息 results['ft_resonance'] = { 'agreement': 1 if e_signal in ('BUY', 'SELL') else 0, 'strength': e_conf / 100, 'final_direction': trend_map.get(e_signal, '震荡'), 'v5_signal': v5_sig, 'plan_e_signal': e_signal, } except Exception as e: results['ft_error'] = str(e)[:100] # 3. 方案F-v5: MACD(5,10,3)+高置信模式(BUY>70/SELL<28)+6因子方向修正 (准确率55.5%) try: if V53_SCORER_AVAILABLE: df_fv3, err_fv3 = _download_stock_data(stock_code) if not err_fv3 and df_fv3 is not None: if 'timestamps' in df_fv3.columns: df_fv3 = df_fv3.set_index('timestamps') fv3_result = _v53_mod.run_fv3_analysis(stock_code, df_fv3) fv4_signal = fv3_result.get('signal', 'HOLD') fv4_score = fv3_result.get('score', 50) fv3_conf = fv3_result.get('confidence', 50) fv4_tier = fv3_result.get('tier', 'HOLD') results['fv5_score'] = fv4_score results['fv5_signal'] = fv4_signal results['fv5_confidence'] = fv3_conf results['fv5_tier'] = fv4_tier results['fv5_label'] = fv3_result.get('label', '') results['plan_fv3'] = { 'signal': fv4_signal, 'trend_direction': {'BUY': '看涨', 'SELL': '看跌', 'HOLD': '震荡'}.get(fv4_signal, '震荡'), 'confidence': fv3_conf, 'score': fv4_score, 'tier': fv4_tier, 'source': 'plan_f_v3', 'note': '方案F-v5: MACD(5,10,3)+高置信模式+方向修正(MACD+KDJ+VWAP正向,MA+VP+RSI反向) 准确率55.5%', 'label': fv3_result.get('label', ''), } # F-v3作为主预测(替代E+) results['ft_prediction'] = results['plan_fv3'] results['ft_resonance']['plan_fv4_signal'] = fv4_signal except Exception as e: results['fv4_error'] = str(e)[:200] # 4. v4五维评分 (如果可用) if V4_SCORER_AVAILABLE: try: df_v4, err_v4 = _download_stock_data(stock_code) if not err_v4 and df_v4 is not None: if 'timestamps' in df_v4.columns: df_v4 = df_v4.set_index('timestamps') v4_result = _v4_mod.run_v4_composite_analysis(stock_code, df_v4) results['v4_score'] = v4_result.get('composite_score', 0) results['v4_trend'] = v4_result.get('trend_direction', '') except Exception as e: results['v4_error'] = str(e)[:100] # 4. 综合研判 v5_signal = results.get('v5_signal', 'HOLD') ft_trend = results.get('plan_e', {}).get('trend_direction', '震荡') ft_res = results.get('ft_resonance', {}) ft_agreement = ft_res.get('agreement', 0) ft_strength = ft_res.get('strength', 0) # 方案E投票 (v5信号 + plan_e信号) votes = {'BUY': 0, 'SELL': 0, 'HOLD': 0} # v5信号权重2 if v5_signal == 'BUY': votes['BUY'] += 2 elif v5_signal == 'SELL': votes['SELL'] += 2 else: votes['HOLD'] += 1 # 方案E信号权重1.5 (联合投票+动量过滤结果) plan_e_sig = results.get('plan_e', {}).get('signal', 'HOLD') if plan_e_sig == 'BUY': votes['BUY'] += 1.5 elif plan_e_sig == 'SELL': votes['SELL'] += 1.5 else: votes['HOLD'] += 0.5 total_votes = sum(votes.values()) if total_votes > 0: final_signal = max(votes, key=votes.get) confidence = votes[final_signal] / total_votes * 100 else: final_signal = 'HOLD' confidence = 50 results['joint_prediction'] = { 'final_signal': final_signal, 'confidence': round(confidence, 1), 'votes': {k: round(v, 1) for k, v in votes.items()}, 'systems_used': { 'v5_optimized': 'v5_score' in results, 'ft_transformer': 'plan_e' in results, 'v4_composite': 'v4_score' in results, 'ft_resonance': ft_agreement >= 2, } } return jsonify({'success': True, 'stock_code': stock_code, 'data': results}) except Exception as e: import traceback traceback.print_exc() return jsonify({'error': f'联合预测失败: {str(e)}'}), 500 # ============================================================ # v4 五维综合评分 API # ============================================================ try: _v4_scorer_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'v4_composite_scorer.py') import importlib.util as _ilu_v4 _v4_spec = _ilu_v4.spec_from_file_location('v4_composite_scorer', _v4_scorer_path) _v4_mod = _ilu_v4.module_from_spec(_v4_spec) _v4_spec.loader.exec_module(_v4_mod) V4_SCORER_AVAILABLE = True print("[INFO] v4五维评分模块已加载") except Exception as e: V4_SCORER_AVAILABLE = False print("[WARN] v4五维评分模块加载失败: {}".format(e)) @app.route('/api/v4-composite-analysis', methods=['POST']) def v4_composite_analysis(): """v4五维综合评分: 基本面+动量+浪型+技术+资金流 + 市场状态自适应""" try: data = request.get_json() stock_code = data.get('stock_code', '').strip() if not stock_code: return jsonify({'error': '请输入股票代码'}), 400 if not V4_SCORER_AVAILABLE: return jsonify({'error': 'v4评分模块未加载'}), 503 # 获取数据 - 使用已有的_download_stock_data df, err = _download_stock_data(stock_code) if err: return jsonify({'error': err}), 400 if 'timestamps' in df.columns: df = df.set_index('timestamps') if df.index.name != 'timestamps' else df # 确保必要的列存在 required = ['open', 'high', 'low', 'close', 'volume'] for col in required: if col not in df.columns: return jsonify({'error': f'缺少必要列: {col}'}), 400 # 执行五维分析 result = _v4_mod.run_v4_composite_analysis(stock_code, df) return jsonify({'success': True, 'data': result}) except Exception as e: import traceback traceback.print_exc() return jsonify({'error': f'分析失败: {str(e)}'}), 500 # v5 优化评分模块 (基于回测验证最优权重) # ============================================================ try: _v5_scorer_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'v5_optimized_scorer.py') import importlib.util as _ilu_v5 _v5_spec = _ilu_v5.spec_from_file_location('v5_optimized_scorer', _v5_scorer_path) _v5_mod = _ilu_v5.module_from_spec(_v5_spec) _v5_spec.loader.exec_module(_v5_mod) V5_SCORER_AVAILABLE = True print("[INFO] v5优化评分模块已加载 (方案E+: 联合+动量过滤+市场状态自适应, 准确率: 50.4%)") except Exception as e: V5_SCORER_AVAILABLE = False print("[WARN] v5优化评分模块加载失败: {}".format(e)) # v5.3 增强评分模块 (方案F-v5: MACD(5,10,3)+高置信模式(BUY>70/SELL<28)+6因子方向修正, 准确率55.5%) # ============================================================ try: _v53_scorer_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'v5_enhanced_scorer.py') import importlib.util as _ilu_v53 _v53_spec = _ilu_v53.spec_from_file_location('v5_enhanced_scorer', _v53_scorer_path) _v53_mod = _ilu_v53.module_from_spec(_v53_spec) _v53_spec.loader.exec_module(_v53_mod) V53_SCORER_AVAILABLE = True print("[INFO] v5.3增强评分模块已加载 (方案F-v5: MACD(5,10,3)+高置信模式(BUY>70/SELL<28)+6因子方向修正, 准确率: 55.5%)") except Exception as e: V53_SCORER_AVAILABLE = False # 蒸鱼ML预测引擎 (GBDT日线模型, 测试集54.0%, 阈值0.5%, 16股600日) # ============================================================ try: _ml_engine_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'ml_engine.py') import importlib.util as _ilu_ml _ml_spec = _ilu_ml.spec_from_file_location('ml_engine', _ml_engine_path) _ml_mod = _ilu_ml.module_from_spec(_ml_spec) _ml_spec.loader.exec_module(_ml_mod) ML_ENGINE_AVAILABLE = True print("[INFO] 蒸鱼ML引擎已加载 (GBDT 54.0%, 16股重新训练)") except Exception as e: ML_ENGINE_AVAILABLE = False print("[WARN] 蒸鱼ML引擎加载失败: {}".format(e)) print("[WARN] v5.3增强评分模块加载失败: {}".format(e)) @app.route('/api/v5-optimized-analysis', methods=['POST']) def v5_optimized_analysis(): """v5优化评分: 基于回测验证最优权重的7维指标综合评分 回测准确率: 54.42% (884有效信号/4398样本) 配置: KDJ(3,3,3) MACD(5,10,3) MA(5,17,26,35,105,155,250) 布林带反向, 其余正向; BUY>60, SELL<40 """ try: data = request.get_json() stock_code = data.get('stock_code', '').strip() if not stock_code: return jsonify({'error': '请输入股票代码'}), 400 if not V5_SCORER_AVAILABLE: return jsonify({'error': 'v5评分模块未加载'}), 503 df, err = _download_stock_data(stock_code) if err: return jsonify({'error': err}), 400 if 'timestamps' in df.columns: df = df.set_index('timestamps') if df.index.name != 'timestamps' else df required = ['open', 'high', 'low', 'close', 'volume'] for col in required: if col not in df.columns: return jsonify({'error': f'缺少必要列: {col}'}), 400 result = _v5_mod.run_v5_optimized_analysis(stock_code, df) return jsonify({'success': True, 'data': result}) except Exception as e: import traceback traceback.print_exc() return jsonify({'error': f'分析失败: {str(e)}'}), 500 @app.route('/api/predict', methods=['POST']) def predict(): """Legacy prediction endpoint (backward compatible).""" try: data = request.get_json() fp = data.get('file_path') lookback = int(data.get('lookback', 400)) pred_len = int(data.get('pred_len', 120)) temperature = float(data.get('temperature', 1.0)) top_p = float(data.get('top_p', 0.9)) sample_count = int(data.get('sample_count', 1)) if not fp: return jsonify({'error': 'File path is required'}), 400 df, err = _load_csv_file(fp) if err: return jsonify({'error': err}), 400 if predictor is None: return jsonify({'error': 'Model not loaded'}), 400 x_df = df.iloc[:lookback][['open','high','low','close']].copy() if 'volume' in df.columns: x_df['volume'] = df.iloc[:lookback]['volume'] x_ts = pd.Series(df.iloc[:lookback]['timestamps'].values) y_ts = pd.Series(df.iloc[lookback:lookback+pred_len]['timestamps'].values) pred_df = predictor.predict(x_df, x_ts, y_ts, pred_len, T=temperature, top_p=top_p, sample_count=sample_count) actual_data = [] actual_df = None if len(df) >= lookback + pred_len: actual_df = df.iloc[lookback:lookback+pred_len] for _, row in actual_df.iterrows(): actual_data.append({ 'open': float(row['open']), 'high': float(row['high']), 'low': float(row['low']), 'close': float(row['close']), 'volume': float(row['volume']) if 'volume' in row else 0 }) # v5动量增强 (offline预测) v5_last2 = None try: df_for_v5_2 = df.set_index('timestamps') if 'timestamps' in df.columns else df if V5_SCORER_AVAILABLE: v5r2 = _v5_mod.run_v5_optimized_analysis('', df_for_v5_2) v5_last2 = v5r2.get('score', None) except Exception: pass chart_json = _build_kline_chart_enhanced(df, pred_df, lookback, pred_len, actual_df, v5_last_score=v5_last2) pred_results = [] for _, row in pred_df.iterrows(): pred_results.append({ 'open': float(row['open']), 'high': float(row['high']), 'low': float(row['low']), 'close': float(row['close']), 'volume': float(row['volume']) if 'volume' in row else 0 }) return jsonify({ 'success': True, 'chart': chart_json, 'prediction_results': pred_results, 'actual_data': actual_data, 'has_comparison': len(actual_data) > 0 }) except Exception as e: return jsonify({'error': str(e)}), 500 @app.route('/api/quick-predict', methods=['POST']) def quick_predict(): """Full pipeline: download data -> analyze -> predict, all by stock code.""" try: data = request.get_json() stock_code = data.get('stock_code', '').strip() if not stock_code: return jsonify({'error': 'Stock code is required'}), 400 lookback = int(data.get('lookback', 400)) pred_len = int(data.get('pred_len', 120)) temperature = float(data.get('temperature', 1.0)) top_p = float(data.get('top_p', 0.9)) use_enhanced = data.get('use_enhanced', True) # Step 1: Download data df, err = _download_stock_data(stock_code) if err: return jsonify({'error': err}), 400 # lookback=0 means use all available data if lookback == 0: lookback = len(df) if len(df) < lookback: lookback = len(df) - 1 if lookback < 50: return jsonify({'error': f'Insufficient data: only {len(df)} rows'}), 400 # Step 2: Analysis analysis = {} if ENHANCED_AVAILABLE: try: df_analysis = df.set_index('timestamps').copy() if 'volume' not in df_analysis.columns: df_analysis['volume'] = 0 indicators_df = technical_analyzer.calculate_all_indicators(df_analysis.copy()) tech_signals = _build_tech_signals(indicators_df) vp_result = volume_price_analyzer.analyze_volume_price_relationship(df_analysis) vp_clean = {k: round(float(v), 4) if isinstance(v, (np.floating, float)) else v for k, v in vp_result.items()} wave_result = wave_analyzer.analyze_wave_patterns(df_analysis) wave_clean = {} for k, v in wave_result.items(): if isinstance(v, dict): wave_clean[k] = {kk: round(float(vv), 4) if isinstance(vv, (np.floating, float, np.integer, int)) else vv for kk, vv in v.items()} elif isinstance(v, (np.floating, float, np.integer, int)): wave_clean[k] = round(float(v), 4) elif isinstance(v, np.ndarray): wave_clean[k] = [round(float(x), 4) for x in v.tolist()] else: wave_clean[k] = v tech_charts = _build_technical_charts(df_analysis, indicators_df) analysis = { 'technical_signals': tech_signals, 'volume_price_analysis': vp_clean, 'wave_analysis': wave_clean, 'charts': tech_charts } # Classic pattern detection if CLASSIC_PATTERN_AVAILABLE: try: cp_result = detect_classic_patterns(df_analysis) analysis['classic_patterns'] = { 'summary': cp_result.get('summary', ''), 'score': cp_result.get('score', 0), 'overall_signal': cp_result.get('overall_signal', 'none'), 'detected_list': cp_result.get('detected_list', []), } except Exception as _cpe: analysis['classic_patterns'] = {'error': str(_cpe)} except Exception as e: analysis = {'error': str(e)} # Classic pattern detection (independent of enhanced modules) cp_buy_markers = [] cp_sell_markers = [] if CLASSIC_PATTERN_AVAILABLE: try: df_cp = df.set_index('timestamps') if 'timestamps' in df.columns else df cp_result = detect_classic_patterns(df_cp) cp_buy_markers = cp_result.get('buy_markers', []) cp_sell_markers = cp_result.get('sell_markers', []) # Extract all double_volume signals from _all_signals for chart markers patterns = cp_result.get('patterns', {}) dv_pattern = patterns.get('double_volume', {}) if dv_pattern.get('_all_signals'): for sig in dv_pattern['_all_signals']: idx = sig.get('index', -1) if idx >= 0: label = '倍量强势' if sig.get('signal') == 'bullish' else '倍量关注' cp_buy_markers.append({ 'index': int(idx), 'date': sig.get('date', ''), 'price': sig.get('price', 0), 'label': label, 'strength': sig.get('strength', 0), }) # Extract all distribution_top signals for chart markers dt_pattern = patterns.get('distribution_top', {}) if dt_pattern.get('_all_signals'): for sig in dt_pattern['_all_signals']: idx = sig.get('index', -1) if idx >= 0: cp_sell_markers.append({ 'index': int(idx), 'date': sig.get('date', ''), 'price': sig.get('price', 0), 'label': sig.get('label', '波段高位'), 'strength': sig.get('strength', 0), }) if isinstance(analysis, dict) and 'error' not in analysis: analysis['classic_patterns'] = { 'summary': cp_result.get('summary', ''), 'score': cp_result.get('score', 0), 'overall_signal': cp_result.get('overall_signal', 'none'), 'detected_list': cp_result.get('detected_list', []), } else: if not isinstance(analysis, dict): analysis = {} analysis['classic_patterns'] = { 'summary': cp_result.get('summary', ''), 'score': cp_result.get('score', 0), 'overall_signal': cp_result.get('overall_signal', 'none'), 'detected_list': cp_result.get('detected_list', []), } except Exception as _cpe: if isinstance(analysis, dict): analysis['classic_patterns'] = {'error': str(_cpe)} else: analysis = {'classic_patterns': {'error': str(_cpe)}} # Wyckoff VPA analysis (independent of enhanced modules) if WYCKOFF_VPA_AVAILABLE: try: df_wv = df.set_index('timestamps') if 'timestamps' in df.columns else df wv_result = detect_wyckoff_vpa(df_wv) if isinstance(analysis, dict): analysis['wyckoff_vpa'] = wv_result except Exception as _wve: analysis['wyckoff_vpa'] = {'error': str(_wve)} # Wave Theory analysis (independent of enhanced modules) if WAVE_ANALYSIS_AVAILABLE: try: df_wv = df.set_index('timestamps') if 'timestamps' in df.columns else df wave_result = analyze_wave_patterns(df_wv) if isinstance(analysis, dict): analysis['wave_analysis'] = wave_result.get('analysis', wave_result) except Exception as _wae: analysis['wave_analysis'] = {'error': str(_wae)} # 踏浪之道 · 主升浪评分分析 if TALANG_AVAILABLE: try: df_tl = df.copy() df_tl['timestamps'] = df_tl.get('timestamps', df_tl.index) talang_result = talang_score_analysis(df_tl, stock_code) if talang_result.get('success'): analysis['talang'] = talang_result # 将踏浪之道的买卖标注合并到经典形态标记中 for m in talang_result.get('buy_markers', []): cp_buy_markers.append(m) except Exception as _tle: analysis['talang'] = {'error': str(_tle)} # Step 3: Prediction x_df = df.iloc[-lookback:].reset_index(drop=True) required_cols = ['open', 'high', 'low', 'close'] if 'volume' in x_df.columns: required_cols.append('volume') x_input = x_df[required_cols].copy() x_timestamp = pd.Series(x_df['timestamps'].values) wave_analysis_for_chart = analysis.get('wave_analysis') if analysis else None if ENHANCED_AVAILABLE and use_enhanced and enhanced_predictor is not None: try: pred_df = enhanced_predictor.predict_enhanced( x_input, x_timestamp, x_timestamp, pred_len, T=temperature, top_p=top_p, verbose=False ) except Exception: if predictor is not None: pred_df = predictor.predict(x_input, x_timestamp, x_timestamp, pred_len, T=temperature, top_p=top_p) else: # Offline fallback pred_df = _offline_predict(x_input, pred_len) elif predictor is not None: pred_df = predictor.predict(x_input, x_timestamp, x_timestamp, pred_len, T=temperature, top_p=top_p) else: pred_df = _offline_predict(x_input, pred_len) # v5动量增强 (wave分析预测) v5_last3 = None; v5_bm3 = []; v5_sm3 = [] try: if V5_SCORER_AVAILABLE: x_for_v5 = x_df.set_index('timestamps') if 'timestamps' in x_df.columns else x_df v5r3 = _v5_mod.run_v5_optimized_analysis(stock_code, x_for_v5) v5_last3 = v5r3.get('score', None) v5_bm3, v5_sm3, _ = _compute_v5_signals_on_history(x_df) except Exception: pass chart_json = _build_kline_chart_enhanced(x_df, pred_df, lookback, pred_len, wave_analysis=wave_analysis_for_chart, v5_buy_markers=v5_bm3, v5_sell_markers=v5_sm3, v5_last_score=v5_last3) pred_results = [] for _, row in pred_df.iterrows(): pred_results.append({ 'open': float(row['open']), 'high': float(row['high']), 'low': float(row['low']), 'close': float(row['close']), 'volume': float(row['volume']) if 'volume' in row else 0 }) info = { 'rows': len(x_df), 'stock_code': stock_code, 'lookback': lookback, 'start_date': x_df['timestamps'].min().isoformat(), 'end_date': x_df['timestamps'].max().isoformat(), 'price_range': { 'min': float(x_df[['open','high','low','close']].min().min()), 'max': float(x_df[['open','high','low','close']].max().max()) }, 'last_close': float(x_df['close'].iloc[-1]), 'pred_start': pred_results[0]['close'] if pred_results else 0, 'pred_end': pred_results[-1]['close'] if pred_results else 0, 'pred_change_pct': round((pred_results[-1]['close'] / pred_results[0]['close'] - 1) * 100, 2) if pred_results and pred_results[0]['close'] > 0 else 0 } # Extract HK stock name if applicable hk_name = '' is_hk = len(stock_code) == 5 if is_hk: try: import urllib.request as _urllib _url = f'https://web.ifzq.gtimg.cn/appstock/app/hkfqkline/get?param=hk{stock_code},day,,,1,qfq' _req = _urllib.Request(_url, headers={'User-Agent': 'Mozilla/5.0'}) _resp = _urllib.urlopen(_req, timeout=10) _raw = _resp.read().decode() if _raw.startswith('kline_dayqfq'): _raw = _raw[_raw.index('(')+1:_raw.rindex(')')] _jd = json.loads(_raw) qt = _jd.get('data', {}).get(f'hk{stock_code}', {}).get('qt', {}).get(f'hk{stock_code}', []) if isinstance(qt, list) and len(qt) > 1: hk_name = str(qt[1]) except Exception: pass return jsonify({ 'success': True, 'data_info': info, 'analysis': analysis, 'chart': chart_json, 'prediction': pred_results, 'enhanced': use_enhanced and ENHANCED_AVAILABLE, 'hk_name': hk_name }) except Exception as e: import traceback traceback.print_exc() return jsonify({'error': str(e)}), 500 def _offline_predict(x_input, pred_len): """Fallback predictor using simple statistics when no model is loaded.""" last_close = float(x_input['close'].iloc[-1]) last_vol = float(x_input['volume'].iloc[-1]) if 'volume' in x_input.columns else 0 # Calculate recent volatility returns = x_input['close'].pct_change().dropna() daily_vol = float(returns.std()) if len(returns) > 1 else 0.015 daily_drift = float(returns.mean()) if len(returns) > 1 else 0.0002 predictions = [] import random np.random.seed(42) for i in range(pred_len): change = daily_drift + daily_vol * np.random.randn() new_close = last_close * (1 + change) new_high = new_close * (1 + abs(daily_vol * np.random.randn()) * 0.5) new_low = new_close * (1 - abs(daily_vol * np.random.randn()) * 0.5) new_open = (last_close + new_close) / 2 * (1 + np.random.randn() * 0.002) new_vol = last_vol * (0.8 + np.random.rand() * 0.4) predictions.append({ 'open': round(max(new_open, new_low), 2), 'high': round(max(new_high, new_open, new_close), 2), 'low': round(min(new_low, new_open, new_close), 2), 'close': round(new_close, 2), 'volume': round(new_vol, 2) }) last_close = new_close return pd.DataFrame(predictions) # ============================================================ # 分析报告导出 API # ============================================================ # ====== 踏浪之道 · 主升浪分析 API ====== @app.route('/api/talang-analysis', methods=['POST']) def talang_analysis_endpoint(): """踏浪之道主升浪评分分析 - 基于已下载的数据进行15条规则评分""" if not TALANG_AVAILABLE: return jsonify({'error': '踏浪之道模块未加载'}), 503 try: data = request.get_json() stock_code = data.get('stock_code', '').strip() if not stock_code: return jsonify({'error': '请输入股票代码'}), 400 min_score = int(data.get('min_score', 8)) # 使用缓存的数据或重新下载 cache_key = f"{stock_code}_2024-01-01" if cache_key in stock_data_cache: df = stock_data_cache[cache_key] else: from talang_integration import fetch_sina_data, normalize_df, calc_indicators if stock_code.startswith('6'): sina_code = f'sh{stock_code}' else: sina_code = f'sz{stock_code}' df = fetch_sina_data(sina_code, 600) if df is None or len(df) < 120: return jsonify({'error': f'无法获取{stock_code}数据或数据不足'}), 400 df = df[df['date'] >= '2024-01-01'].reset_index(drop=True) stock_data_cache[cache_key] = df # 转换为Kronos格式的DataFrame df_kronos = df.copy() df_kronos['timestamps'] = df_kronos['date'] result = talang_score_analysis(df_kronos, stock_code, min_score=min_score) return jsonify(result) except Exception as e: import traceback traceback.print_exc() return jsonify({'error': str(e)}), 500 @app.route('/api/talang-backtest', methods=['POST']) def talang_backtest_endpoint(): """踏浪之道完整回测 - 下载历史数据执行15条规则回测""" if not TALANG_AVAILABLE: return jsonify({'error': '踏浪之道模块未加载'}), 503 try: data = request.get_json() stock_code = data.get('stock_code', '').strip() if not stock_code: return jsonify({'error': '请输入股票代码'}), 400 name = data.get('name', '') min_score = int(data.get('min_score', 8)) result = talang_backtest_full(stock_code, name=name, min_score=min_score) if 'error' in result: return jsonify(result), 400 return jsonify(result) except Exception as e: import traceback traceback.print_exc() return jsonify({'error': str(e)}), 500 @app.route('/api/export-report', methods=['POST']) def export_report_route(): """导出完整分析报告为独立HTML""" try: data = request.get_json() stock_code = data.get('stock_code', '').strip() if not stock_code: return jsonify({'error': '请输入股票代码'}), 400 stock_code = stock_code.replace('.SH', '').replace('.SZ', '').replace('.sh', '').replace('.sz', '') lookback = int(data.get('lookback', 250)) report = {'stock_code': stock_code, 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S')} # 1. K线+技术分析(复用quick-predict逻辑, 不做预测) try: df, err = _download_stock_data(stock_code) if not err and df is not None: info = { 'rows': len(df), 'start_date': str(df['timestamps'].min())[:10], 'end_date': str(df['timestamps'].max())[:10], 'last_close': float(df['close'].iloc[-1]) } report['kline_info'] = info # 技术分析 if ENHANCED_AVAILABLE: try: df_a = df.set_index('timestamps').copy() if 'volume' not in df_a.columns: df_a['volume'] = 0 indicators_df = technical_analyzer.calculate_all_indicators(df_a.copy()) tech_signals = _build_tech_signals(indicators_df) report['tech_signals'] = tech_signals except Exception as te: report['tech_error'] = str(te)[:200] # 威科夫 if WYCKOFF_VPA_AVAILABLE: try: df_wv = df.set_index('timestamps') if 'timestamps' in df.columns else df wv_result = detect_wyckoff_vpa(df_wv) report['wyckoff'] = wv_result except Exception: pass # 经典形态 if CLASSIC_PATTERN_AVAILABLE: try: df_cp = df.set_index('timestamps') if 'timestamps' in df.columns else df cp_result = detect_classic_patterns(df_cp) report['classic_patterns'] = cp_result except Exception: pass # 波浪 if WAVE_ANALYSIS_AVAILABLE: try: df_wv = df.set_index('timestamps') if 'timestamps' in df.columns else df wave_result = analyze_wave_patterns(df_wv) report['wave'] = wave_result except Exception: pass except Exception as e: report['kline_error'] = str(e)[:200] # 2. 基本面分析 if FUNDAMENTAL_AVAILABLE: try: fa_result = run_fundamental_analysis(stock_code) report['fundamental'] = fa_result except Exception as e: report['fundamental_error'] = str(e)[:200] # 4. v4五维评分 if V4_SCORER_AVAILABLE: try: df_v4, err_v4 = _download_stock_data(stock_code) if not err_v4 and df_v4 is not None: if 'timestamps' in df_v4.columns: df_v4 = df_v4.set_index('timestamps') v4_result = _v4_mod.run_v4_composite_analysis(stock_code, df_v4) report['v4_composite'] = v4_result except Exception as e: report['v4_error'] = str(e)[:200] # 4. 多维度联动 try: md_result = multi_dimension_analysis(stock_code) report['multi_dimension'] = md_result except Exception as e: report['multi_dim_error'] = str(e)[:200] # 生成HTML报告 html = _build_export_html(report) fname = 'Kronos_{}_{}.html'.format(stock_code, datetime.now().strftime('%Y%m%d_%H%M%S')) return jsonify({'success': True, 'html': html, 'filename': fname}) except Exception as e: import traceback traceback.print_exc() return jsonify({'error': '导出失败: {}'.format(str(e))}), 500 def _build_export_html(report): """构建独立可离线查看的HTML报告""" code = report.get('stock_code', 'unknown') ts = report.get('timestamp', '') # 股票信息 si = {} fa = report.get('fundamental', {}) if fa and fa.get('agents'): first_agent = fa['agents'][0] or {} si = first_agent.get('stock_info', {}) name = si.get('name', code) price = si.get('price', 0) pe = si.get('pe', 0) ki = report.get('kline_info', {}) last_close = ki.get('last_close', price) # 基本面共识 cs = fa.get('consensus') or {} signal = cs.get('signal', 'neutral') fa_score = cs.get('final_score', 0) summary = cs.get('summary', '') sig_color = {'bullish': '#e74c3c', 'bearish': '#26a69a', 'neutral': '#f0ad4e'}.get(signal, '#999') sig_label = {'bullish': '看多', 'bearish': '看空', 'neutral': '中性'}.get(signal, '未知') # v4五维 v4 = report.get('v4_composite', {}) v4_score = v4.get('composite_score', 0) v4_trend = v4.get('trend_direction', '') v4_dims = v4.get('dimensions', []) v4_thresholds = v4.get('thresholds', {}) # 技术信号 tech = report.get('tech_signals', {}) # 威科夫 wv = report.get('wyckoff', {}) # agents agents = fa.get('agents', []) # 经典形态 cp = report.get('classic_patterns', {}) cp_summary = cp.get('summary', '') if isinstance(cp, dict) else '' cp_score = cp.get('score', 0) if isinstance(cp, dict) else 0 # 构建HTML lines = [] lines.append('
') lines.append('') lines.append('