From 168f06ecf52c341ac4fb33047b475b4cee910fd2 Mon Sep 17 00:00:00 2001 From: Lxy Date: Mon, 18 May 2026 23:48:43 +0800 Subject: [PATCH] =?UTF-8?q?feat=20:=20=E5=A2=9E=E5=8A=A0=E6=99=BA=E8=83=BD?= =?UTF-8?q?=E5=88=86=E6=9E=90=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/__pycache__/main.cpython-311.pyc | Bin 5530 -> 6513 bytes app/api/__pycache__/ai_config.cpython-311.pyc | Bin 0 -> 8628 bytes .../futures_analysis.cpython-311.pyc | Bin 0 -> 24896 bytes app/api/ai_config.py | 177 ++++ app/api/futures_analysis.py | 455 +++++++++ app/main.py | 16 +- app/static/ai_config.css | 517 ++++++++++ app/static/ai_config.html | 189 ++++ app/static/ai_config.js | 298 ++++++ app/static/futures_analysis.css | 938 ++++++++++++++++++ app/static/futures_analysis.html | 283 ++++++ app/static/futures_analysis.js | 825 +++++++++++++++ app/static/index.html | 9 + 13 files changed, 3706 insertions(+), 1 deletion(-) create mode 100644 app/api/__pycache__/ai_config.cpython-311.pyc create mode 100644 app/api/__pycache__/futures_analysis.cpython-311.pyc create mode 100644 app/api/ai_config.py create mode 100644 app/api/futures_analysis.py create mode 100644 app/static/ai_config.css create mode 100644 app/static/ai_config.html create mode 100644 app/static/ai_config.js create mode 100644 app/static/futures_analysis.css create mode 100644 app/static/futures_analysis.html create mode 100644 app/static/futures_analysis.js diff --git a/app/__pycache__/main.cpython-311.pyc b/app/__pycache__/main.cpython-311.pyc index 13e81c5c55614beccc74de27373790c0d9757e94..c52b950dcf0cc7ea231363c849d65caa5b64c42e 100644 GIT binary patch delta 1788 zcma)6OKclO7@l3P~iHr=h`!U|jgQF8N9Q)kY`#CwH~)<1^{jP;1&1axq9?g!)ul+8uJI z+e7UPVi9JN9n^*nSe?KO$mV3HvTm?*0G_=1kN@>&N;pEeYnwsn9?a4Rd9Y4s!iG32 zn6Gf#puAUxP^cR`83c=96>PZse)Bd5D9q9b_28bZUZud;lc{%r2`&g{f;oiyl~>sSAbyQ?i}mPFL9o+wrAOGF z99DMOcEHOTN0r7|rD?6wxQ;ZXIPd5@`T)c!t$u{nnexhugzn=73O!d)#dbYgW}inw zZw0%riamn61n(+K_lCHjt!!#EGz!&^CZ9XnFCVsIn_$1+ub8=3W*aD)aAXNj1^F*&Rx z>)Knw(Of(ZzOyM_j*BF%`B84vJz2CtC2Bpm9imoIUc$shRaUBk#l=JnOG7a}wt`6; z1?9(!y+HZs8?DFBkDpq1hX9 ziAa`T#$;Z3*f`*$z?#9&XA}O5r@f3?lO;Hwcx%n#KkUa~xgUzDaru%J=zBMA{qfbE zy-&Z}yScG{`|JJ9&(~Tin>Vx~r&DBxMy-fnMJ;7>4ybRylsZAIqrYoVy>nByxtRA( z72Hz==XB9IT`)Okil&)7H>1_FX7uxk!P4>3lB3%@y|8!p_HQ>o*njt<-+%b>Fug`N zNhRKY=k=6RN@x+B2{5Y@8Y{TR3eNGObG%@hD4Hhn+=P~o($s{5TD27=*Q{VZC9jYd z!CjKNw18MgyLMN;cOq{J(uWHYkMf{tY5aM>ygJp8#*zeWg3BVtB+az6lNG)>B8oD3 z1&l=Htht-#;DBoNB9dq!h z>eH)2g1}46Wn?l-Rk~#?!*Qv#y60(|2;G`eMl`N!$iX!bNk-L?T&AMkPili{kzRqb zBnMpj+m;?G5$jLhxjgz`&m(2Z*1U7x_6>9C5z_sNtiK>@-quyL`5uhrk+py(i)b>B zCXc|4v7)W(?t+Gm6wyc?jU3bvyru~3rnOWulQTbSh*p8<<<01Eb bpxTR!ol=98X%14R<49#xj4dB>9w_NwHGu7@ delta 975 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 literal 0 HcmV?d00001 diff --git a/app/api/__pycache__/futures_analysis.cpython-311.pyc b/app/api/__pycache__/futures_analysis.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dbb109316bcb8203425e8bb5c6c220a7ca048550 GIT binary patch literal 24896 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 szLa0+#oORewcwG%S5i@Ms3 dict: + """加载AI配置""" + _ensure_config_dir() + if not AI_CONFIG_FILE.exists(): + return { + "models": [], + "active_model": None, + "analysis_settings": { + "enable_technical_analysis": True, + "enable_fundamental_analysis": False, + "enable_sentiment_analysis": False, + "risk_tolerance": "medium", + "max_position_pct": 10 + } + } + with open(AI_CONFIG_FILE, "r", encoding="utf-8") as f: + return json.load(f) + + +def _save_ai_config(config: dict): + """保存AI配置""" + _ensure_config_dir() + with open(AI_CONFIG_FILE, "w", encoding="utf-8") as f: + json.dump(config, f, ensure_ascii=False, indent=4) + + +@router.get("", response_model=AIConfigResponse) +def get_ai_config(): + """获取当前AI模型配置""" + try: + config = _load_ai_config() + return {"success": True, "data": config} + except Exception as e: + logger.error(f"加载AI配置失败: {e}") + return {"success": False, "message": str(e)} + + +@router.post("", response_model=AIConfigResponse) +def save_ai_config(config: SaveAIConfigRequest): + """保存AI模型配置""" + try: + config_dict = { + "models": config.models, + "active_model": config.active_model, + "analysis_settings": config.analysis_settings or {} + } + _save_ai_config(config_dict) + return {"success": True, "message": "AI配置保存成功"} + except Exception as e: + logger.error(f"保存AI配置失败: {e}") + return {"success": False, "message": str(e)} + + +@router.post("/test", response_model=AIConfigResponse) +def test_ai_connection(model_config: AIModelConfig): + """测试AI模型连接""" + try: + import httpx + + headers = { + "Authorization": f"Bearer {model_config.api_key}", + "Content-Type": "application/json" + } + + data = { + "model": model_config.model_id, + "messages": [{"role": "user", "content": "Hello"}], + "max_tokens": 10 + } + + with httpx.Client(timeout=30) as client: + response = client.post( + f"{model_config.api_base}/chat/completions", + headers=headers, + json=data + ) + + if response.status_code == 200: + return {"success": True, "message": "连接测试成功"} + else: + return {"success": False, "message": f"连接失败: {response.status_code} - {response.text}"} + + except Exception as e: + logger.error(f"AI连接测试失败: {e}") + return {"success": False, "message": f"连接测试失败: {str(e)}"} + + +@router.get("/providers") +def get_ai_providers(): + """获取支持的AI提供商列表""" + providers = [ + { + "id": "openai", + "name": "OpenAI", + "api_base": "https://api.openai.com/v1", + "models": ["gpt-4o", "gpt-4-turbo", "gpt-3.5-turbo"] + }, + { + "id": "anthropic", + "name": "Anthropic Claude", + "api_base": "https://api.anthropic.com/v1", + "models": ["claude-3-opus", "claude-3-sonnet", "claude-3-haiku"] + }, + { + "id": "google", + "name": "Google Gemini", + "api_base": "https://generativelanguage.googleapis.com/v1beta", + "models": ["gemini-pro", "gemini-pro-vision"] + }, + { + "id": "aliyun", + "name": "阿里云通义千问", + "api_base": "https://dashscope.aliyuncs.com/compatible-mode/v1", + "models": ["qwen-max", "qwen-plus", "qwen-turbo"] + }, + { + "id": "baidu", + "name": "百度文心一言", + "api_base": "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop", + "models": ["ernie-4.0", "ernie-3.5", "ernie-speed"] + }, + { + "id": "zhipu", + "name": "智谱清言", + "api_base": "https://open.bigmodel.cn/api/paas/v4", + "models": ["glm-4", "glm-3-turbo"] + } + ] + return {"success": True, "data": providers} diff --git a/app/api/futures_analysis.py b/app/api/futures_analysis.py new file mode 100644 index 0000000..4f4b0a8 --- /dev/null +++ b/app/api/futures_analysis.py @@ -0,0 +1,455 @@ +""" +期货智析接口 - 提供期货分析数据 +""" +import json +import logging +from pathlib import Path +from typing import Optional + +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session + +from app.database import get_db +from app.services.cache import get_cached_data, get_latest_cached + +logger = logging.getLogger(__name__) +router = APIRouter(prefix="/futures", tags=["期货智析"]) + +CONFIG_DIR = Path(__file__).resolve().parent.parent.parent / "config" +SYMBOLS_CONFIG_FILE = CONFIG_DIR / "symbols_config.json" + + +def _load_symbols_config() -> dict: + """加载品种配置文件""" + if not SYMBOLS_CONFIG_FILE.exists(): + return {"futures": {}, "stock": {}} + with open(SYMBOLS_CONFIG_FILE, "r", encoding="utf-8") as f: + return json.load(f) + + +@router.get("/list") +def get_futures_list(db: Session = Depends(get_db)): + """获取所有期货品种列表及摘要信息(从symbols_config.json读取)""" + config = _load_symbols_config() + futures_config = config.get("futures", {}) + + if not futures_config: + return {"success": True, "data": []} + + futures_data = [] + for name, symbol_code in futures_config.items(): + cached = get_cached_data(db, symbol_code, "futures") + if cached and cached.get("timeframes"): + all_candles = [] + for period, candles in cached.get("timeframes", {}).items(): + all_candles.extend(candles) + + if all_candles: + latest_candle = all_candles[-1] + open_price = float(latest_candle.get("open", 0)) + close_price = float(latest_candle.get("close", 0)) + high_price = float(latest_candle.get("high", 0)) + low_price = float(latest_candle.get("low", 0)) + + change = close_price - open_price + change_pct = (change / open_price * 100) if open_price > 0 else 0 + + futures_data.append({ + "symbol": symbol_code, + "name": name, + "price": close_price, + "change": round(change, 2), + "changePct": round(change_pct, 2), + "suggestion": _get_suggestion(close_price, open_price, change_pct), + "suggestionType": "up" if change >= 0 else "down", + "periods": _get_period_trends(all_candles), + "successRate": _calc_success_rate(all_candles), + "trendScore": _calc_trend_score(all_candles), + "resistance": round(high_price * 1.02, 2), + "support": round(low_price * 0.98, 2), + "open": open_price, + "high": high_price, + "low": low_price, + "volume": sum(float(c.get("volume", 0)) for c in all_candles) + }) + else: + futures_data.append({ + "symbol": symbol_code, + "name": name, + "price": 0, + "change": 0, + "changePct": 0, + "suggestion": "等待数据", + "suggestionType": "neutral", + "periods": {"5": "neutral", "15": "neutral", "30": "neutral", "60": "neutral"}, + "successRate": 0, + "trendScore": 0, + "resistance": 0, + "support": 0, + "open": 0, + "high": 0, + "low": 0, + "volume": 0 + }) + + return {"success": True, "data": futures_data} + + +@router.get("/detail/{symbol}") +def get_futures_detail(symbol: str, db: Session = Depends(get_db)): + """获取指定期货品种的详细分析数据""" + cached = get_cached_data(db, symbol, "futures") + if not cached: + raise HTTPException(status_code=404, detail=f"未找到 {symbol} 的缓存数据") + + all_candles = [] + for period, candles in cached.get("timeframes", {}).items(): + all_candles.extend(candles) + + if not all_candles: + raise HTTPException(status_code=404, detail=f"未找到 {symbol} 的K线数据") + + latest_candle = all_candles[-1] + open_price = float(latest_candle.get("open", 0)) + close_price = float(latest_candle.get("close", 0)) + high_price = float(latest_candle.get("high", 0)) + low_price = float(latest_candle.get("low", 0)) + + change = close_price - open_price + change_pct = (change / open_price * 100) if open_price > 0 else 0 + + resistance1 = round(high_price * 1.01, 2) + resistance2 = round(high_price * 1.03, 2) + resistance3 = round(high_price * 1.05, 2) + support1 = round(low_price * 0.99, 2) + support2 = round(low_price * 0.97, 2) + support3 = round(low_price * 0.95, 2) + + suggestion = _get_suggestion(close_price, open_price, change_pct) + suggestion_type = "up" if change >= 0 else "down" + trend_score = _calc_trend_score(all_candles) + + data = { + "symbol": symbol, + "name": _get_futures_name(symbol), + "price": close_price, + "change": round(change, 2), + "changePct": round(change_pct, 2), + "suggestion": suggestion, + "suggestionType": suggestion_type, + "suggestionReason": _get_suggestion_reason(symbol, suggestion), + "open": open_price, + "high": high_price, + "low": low_price, + "volume": sum(float(c.get("volume", 0)) for c in all_candles), + "entryPrice": round(close_price * 0.995, 2) if change >= 0 else round(close_price * 1.005, 2), + "targetPrice": resistance1 if change >= 0 else support1, + "stopLoss": support1 if change >= 0 else resistance1, + "riskLevel": "低" if trend_score >= 80 else "中" if trend_score >= 60 else "高", + "macd": _calc_macd(all_candles), + "rsi": _calc_rsi(all_candles), + "boll": _calc_boll(all_candles), + "kdj": _calc_kdj(all_candles), + "resistances": [resistance1, resistance2, resistance3], + "supports": [support1, support2, support3], + "periodConsistency": _get_period_trends(all_candles) + } + + return {"success": True, "data": data} + + +@router.get("/kline/{symbol}") +def get_kline_data(symbol: str, period: str = "15", db: Session = Depends(get_db)): + """获取指定品种和周期的K线数据""" + period_map = { + "5": "5min", + "15": "15min", + "30": "30min", + "60": "60min", + "1440": "daily", + "daily": "daily" + } + db_period = period_map.get(period, f"{period}min") + + cached = get_cached_data(db, symbol, "futures", [db_period]) + if not cached or not cached.get("timeframes"): + raise HTTPException(status_code=404, detail=f"未找到 {symbol} {db_period} 的缓存数据") + + candles = cached["timeframes"].get(db_period, []) + kline_data = [] + for c in candles: + time_str = c.get("datetime", c.get("time", "")) + if time_str and len(time_str) >= 16: + time_str = time_str[:16].replace("T", " ") + kline_data.append([ + time_str, + str(c.get("open", 0)), + str(c.get("close", 0)), + str(c.get("low", 0)), + str(c.get("high", 0)), + str(int(c.get("volume", 0))) + ]) + + return {"success": True, "data": kline_data} + + +def _get_futures_name(symbol: str) -> str: + """根据合约代码获取品种名称""" + name_map = { + "AU": "黄金", "AG": "白银", "CU": "铜", "AL": "铝", + "ZN": "锌", "NI": "镍", "SN": "锡", "PB": "铅", + "RB": "螺纹钢", "HC": "热卷", "I": "铁矿石", "J": "焦炭", + "JM": "焦煤", "ZC": "动力煤", "MA": "甲醇", "TA": "PTA", + "EG": "乙二醇", "PP": "聚丙烯", "L": "塑料", "V": "PVC", + "M": "豆粕", "RM": "菜粕", "C": "玉米", "CS": "淀粉", + "A": "豆一", "B": "豆二", "Y": "豆油", "P": "棕榈油", + "OI": "菜油", "CF": "棉花", "SR": "白糖", "AP": "苹果", + "JD": "鸡蛋", "LH": "生猪", "FU": "燃料油", "LU": "低硫燃油", + "SC": "原油", "EC": "集运指数", "BU": "沥青", "RU": "橡胶", + "NR": "20号胶", "SP": "纸浆", "SS": "不锈钢", "SA": "纯碱", + "FG": "玻璃", "UR": "尿素", "SF": "硅铁", "SM": "锰硅", + "IF": "沪深300", "IC": "中证500", "IH": "上证50", "IM": "中证1000", + "T": "10年期国债", "TF": "5年期国债", "TS": "2年期国债", "TL": "30年期国债", + } + return name_map.get(symbol, symbol) + + +def _get_suggestion(close: float, open: float, change_pct: float) -> str: + """根据价格走势给出操作建议""" + if change_pct > 2: + return "逢低做多" + elif change_pct > 0.5: + return "逢低做多" + elif change_pct > -0.5: + return "观望等待" + elif change_pct > -2: + return "逢高做空" + else: + return "逢高做空" + + +def _get_suggestion_reason(symbol: str, suggestion: str) -> str: + """获取建议理由""" + reasons = { + "逢低做多": "技术面突破,趋势明确,建议逢低介入", + "逢高做空": "技术面走弱,下行压力增大", + "观望等待": "多空力量均衡,等待方向明确" + } + return reasons.get(suggestion, "等待进一步信号") + + +def _get_period_trends(candles: list) -> dict: + """计算各周期趋势 - 根据不同周期取不同长度的K线计算""" + period_config = { + "5": {"bars": 10, "threshold": 0.003}, + "15": {"bars": 15, "threshold": 0.005}, + "30": {"bars": 20, "threshold": 0.008}, + "60": {"bars": 30, "threshold": 0.01} + } + + result = {} + + for period, cfg in period_config.items(): + bars = cfg["bars"] + threshold = cfg["threshold"] + + if len(candles) < bars: + result[period] = "neutral" + continue + + recent = candles[-bars:] + first_close = float(recent[0].get("close", 0)) + last_close = float(recent[-1].get("close", 0)) + + if first_close <= 0: + result[period] = "neutral" + continue + + change_pct = (last_close - first_close) / first_close + + if change_pct > threshold: + result[period] = "up" + elif change_pct < -threshold: + result[period] = "down" + else: + result[period] = "neutral" + + return result + + +def _calc_success_rate(candles: list) -> int: + """计算交易成功率(简化版)""" + if len(candles) < 10: + return 50 + + wins = 0 + for i in range(1, len(candles)): + prev_close = float(candles[i-1].get("close", 0)) + curr_close = float(candles[i].get("close", 0)) + if curr_close >= prev_close: + wins += 1 + + return int(wins / (len(candles) - 1) * 100) + + +def _calc_trend_score(candles: list) -> int: + """计算趋势评分(0-100)""" + if len(candles) < 5: + return 50 + + recent = candles[-10:] + closes = [float(c.get("close", 0)) for c in recent] + + if len(closes) < 2: + return 50 + + up_count = sum(1 for i in range(1, len(closes)) if closes[i] >= closes[i-1]) + score = int(up_count / (len(closes) - 1) * 100) + + return max(0, min(100, score)) + + +def _calc_ema(data: list, period: int) -> list: + """计算EMA,返回与输入等长的列表,前面用None填充""" + ema = [None] * len(data) + multiplier = 2 / (period + 1) + + if len(data) < period: + return ema + + ema[period - 1] = sum(data[:period]) / period + + for i in range(period, len(data)): + ema[i] = (data[i] - ema[i-1]) * multiplier + ema[i-1] + + return ema + + +def _calc_macd(candles: list) -> dict: + """计算MACD指标""" + if len(candles) < 26: + return {"signal": "中性", "detail": "数据不足"} + + closes = [float(c.get("close", 0)) for c in candles] + ema12 = _calc_ema(closes, 12) + ema26 = _calc_ema(closes, 26) + + dif_list = [] + for i in range(len(closes)): + if ema12[i] is not None and ema26[i] is not None: + dif_list.append(ema12[i] - ema26[i]) + else: + dif_list.append(None) + + # 只对有效DIF值计算DEA,避免None替换为0导致计算错误 + dif_valid = [d for d in dif_list if d is not None] + if dif_valid: + dea_valid = _calc_ema(dif_valid, 9) + dea_list = [None] * (len(dif_list) - len(dif_valid)) + dea_valid + else: + dea_list = [None] * len(dif_list) + + dif = dif_list[-1] + dea = dea_list[-1] + + if dif is not None and dea is not None: + if dif > dea: + signal = "金叉" + elif dif < dea: + signal = "死叉" + else: + signal = "中性" + else: + signal = "中性" + + return {"signal": signal, "detail": f"DIF: {dif:.4f}"} + + +def _calc_rsi(candles: list) -> dict: + """计算RSI指标""" + if len(candles) < 15: + return {"value": 50, "status": "正常"} + + closes = [float(c.get("close", 0)) for c in candles[-15:]] + gains = [] + losses = [] + + for i in range(1, len(closes)): + diff = closes[i] - closes[i-1] + gains.append(max(0, diff)) + losses.append(max(0, -diff)) + + avg_gain = sum(gains) / len(gains) if gains else 0 + avg_loss = sum(losses) / len(losses) if losses else 0 + + if avg_loss == 0: + rsi = 100 + else: + rs = avg_gain / avg_loss + rsi = 100 - (100 / (1 + rs)) + + rsi = int(rsi) + if rsi > 70: + status = "超买" + elif rsi < 30: + status = "超卖" + else: + status = "正常" + + return {"value": rsi, "status": status} + + +def _calc_boll(candles: list) -> dict: + """计算布林带""" + if len(candles) < 20: + return {"signal": "中轨", "detail": "区间: --"} + + closes = [float(c.get("close", 0)) for c in candles[-20:]] + ma = sum(closes) / len(closes) + std = (sum((c - ma) ** 2 for c in closes) / len(closes)) ** 0.5 + + upper = ma + 2 * std + lower = ma - 2 * std + current = closes[-1] + + if current > upper: + signal = "上轨外" + elif current < lower: + signal = "下轨外" + elif current > ma: + signal = "中轨上" + else: + signal = "中轨" + + return {"signal": signal, "detail": f"区间: {lower:.0f}-{upper:.0f}"} + + +def _calc_kdj(candles: list) -> dict: + """计算KDJ指标""" + if len(candles) < 9: + return {"signal": "中性", "detail": "K: -- D: --"} + + highs = [float(c.get("high", 0)) for c in candles[-9:]] + lows = [float(c.get("low", 0)) for c in candles[-9:]] + closes = [float(c.get("close", 0)) for c in candles[-9:]] + + highest = max(highs) + lowest = min(lows) + current = closes[-1] + + if highest == lowest: + rsv = 50 + else: + rsv = (current - lowest) / (highest - lowest) * 100 + + k = int(rsv * 2 / 3 + 50 / 3) + d = int(k * 2 / 3 + 50 / 3) + + if k > d: + signal = "偏多" + elif k < d: + signal = "偏空" + else: + signal = "中性" + + return {"signal": signal, "detail": f"K: {k} D: {d}"} diff --git a/app/main.py b/app/main.py index 9de17a8..6e7cfa9 100644 --- a/app/main.py +++ b/app/main.py @@ -12,7 +12,7 @@ from fastapi.responses import FileResponse from app.database import engine, Base from app.config import HOST, PORT, LOG_LEVEL -from app.api import data, tasks, config +from app.api import data, tasks, config, futures_analysis, ai_config from app.services.scheduler import start_scheduler, stop_scheduler # 配置日志 @@ -87,6 +87,20 @@ def ui_page(): app.include_router(data.router, prefix="/api/v1") app.include_router(tasks.router, prefix="/api/v1") app.include_router(config.router, prefix="/api/v1") +app.include_router(futures_analysis.router, prefix="/api/v1") +app.include_router(ai_config.router, prefix="/api/v1") + + +@app.get("/futures-analysis") +def futures_analysis_page(): + """期货智析页面""" + return FileResponse(str(STATIC_DIR / "futures_analysis.html")) + + +@app.get("/ai-config") +def ai_config_page(): + """AI模型配置页面""" + return FileResponse(str(STATIC_DIR / "ai_config.html")) @app.get("/api/v1/health") diff --git a/app/static/ai_config.css b/app/static/ai_config.css new file mode 100644 index 0000000..e0678aa --- /dev/null +++ b/app/static/ai_config.css @@ -0,0 +1,517 @@ +:root { + --bg-primary: #0d0f14; + --bg-secondary: #151820; + --bg-card: #1a1d28; + --bg-card-hover: #222633; + --border-color: #2a2d3a; + --text-primary: #e8eaed; + --text-secondary: #9aa0ab; + --text-muted: #6b7280; + --green: #22c55e; + --green-bg: rgba(34, 197, 94, 0.15); + --green-border: rgba(34, 197, 94, 0.3); + --red: #ef4444; + --red-bg: rgba(239, 68, 68, 0.15); + --blue: #3b82f6; + --purple: #8b5cf6; + --orange: #f59e0b; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif; + background: var(--bg-primary); + color: var(--text-primary); + line-height: 1.5; +} + +.app-container { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.top-nav { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 24px; + background: var(--bg-secondary); + border-bottom: 1px solid var(--border-color); +} + +.nav-left { + display: flex; + align-items: center; + gap: 16px; +} + +.back-link { + color: var(--text-secondary); + text-decoration: none; + font-size: 14px; + display: flex; + align-items: center; + gap: 6px; + transition: color 0.2s; +} + +.back-link:hover { + color: var(--text-primary); +} + +.page-title { + display: flex; + align-items: center; + gap: 10px; + font-size: 16px; + font-weight: 600; +} + +.page-title i { + color: var(--green); +} + +.nav-right { + display: flex; + align-items: center; + gap: 16px; +} + +.nav-icon-btn { + color: var(--text-secondary); + text-decoration: none; + font-size: 16px; + padding: 6px; + border-radius: 6px; + transition: all 0.2s; +} + +.nav-icon-btn:hover { + color: var(--text-primary); + background: var(--bg-card); +} + +.main-content { + flex: 1; + padding: 24px; + max-width: 900px; + margin: 0 auto; + width: 100%; +} + +.config-container { + display: flex; + flex-direction: column; + gap: 20px; +} + +.config-card { + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 20px; +} + +.card-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; + padding-bottom: 12px; + border-bottom: 1px solid var(--border-color); +} + +.card-header h3 { + display: flex; + align-items: center; + gap: 10px; + font-size: 16px; + font-weight: 600; +} + +.card-header h3 i { + color: var(--green); +} + +/* 提供商网格 */ +.provider-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); + gap: 12px; +} + +.provider-card { + padding: 16px; + background: var(--bg-secondary); + border: 2px solid var(--border-color); + border-radius: 10px; + text-align: center; + cursor: pointer; + transition: all 0.2s; +} + +.provider-card:hover { + border-color: var(--text-muted); + background: var(--bg-card-hover); +} + +.provider-card.active { + border-color: var(--green); + background: var(--green-bg); +} + +.provider-card i { + font-size: 28px; + margin-bottom: 8px; + color: var(--text-secondary); +} + +.provider-card.active i { + color: var(--green); +} + +.provider-card .provider-name { + font-size: 13px; + font-weight: 500; +} + +/* 表单 */ +.form-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16px; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 6px; +} + +.form-group label { + font-size: 13px; + color: var(--text-secondary); +} + +.form-control { + padding: 10px 14px; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 8px; + color: var(--text-primary); + font-size: 14px; + outline: none; + transition: border-color 0.2s; +} + +.form-control:focus { + border-color: var(--green); +} + +.input-with-toggle { + position: relative; + display: flex; + align-items: center; +} + +.input-with-toggle .form-control { + flex: 1; + padding-right: 40px; +} + +.toggle-visibility { + position: absolute; + right: 10px; + background: none; + border: none; + color: var(--text-muted); + cursor: pointer; + padding: 4px; +} + +.form-range { + width: 100%; + height: 6px; + border-radius: 3px; + background: var(--bg-secondary); + outline: none; + -webkit-appearance: none; +} + +.form-range::-webkit-slider-thumb { + -webkit-appearance: none; + width: 18px; + height: 18px; + border-radius: 50%; + background: var(--green); + cursor: pointer; +} + +.range-labels { + display: flex; + justify-content: space-between; + font-size: 11px; + color: var(--text-muted); +} + +.form-actions { + display: flex; + align-items: center; + gap: 12px; + margin-top: 16px; +} + +.test-result { + font-size: 13px; +} + +.test-result.success { + color: var(--green); +} + +.test-result.error { + color: var(--red); +} + +/* 设置列表 */ +.settings-list { + display: flex; + flex-direction: column; + gap: 16px; +} + +.setting-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 0; + border-bottom: 1px solid var(--border-color); +} + +.setting-item:last-child { + border-bottom: none; +} + +.setting-info { + display: flex; + flex-direction: column; + gap: 2px; +} + +.setting-name { + font-size: 14px; + font-weight: 500; +} + +.setting-desc { + font-size: 12px; + color: var(--text-muted); +} + +/* 开关 */ +.switch { + position: relative; + width: 48px; + height: 26px; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 26px; + transition: 0.3s; +} + +.slider:before { + position: absolute; + content: ""; + height: 20px; + width: 20px; + left: 2px; + bottom: 2px; + background: var(--text-muted); + border-radius: 50%; + transition: 0.3s; +} + +input:checked + .slider { + background: var(--green-bg); + border-color: var(--green-border); +} + +input:checked + .slider:before { + transform: translateX(22px); + background: var(--green); +} + +/* 模型列表 */ +.models-list { + display: flex; + flex-direction: column; + gap: 10px; +} + +.model-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 8px; +} + +.model-info { + display: flex; + align-items: center; + gap: 12px; +} + +.model-status { + width: 10px; + height: 10px; + border-radius: 50%; +} + +.model-status.active { + background: var(--green); + box-shadow: 0 0 8px var(--green); +} + +.model-status.inactive { + background: var(--text-muted); +} + +.model-name { + font-size: 14px; + font-weight: 500; +} + +.model-provider { + font-size: 12px; + color: var(--text-muted); +} + +.model-actions { + display: flex; + gap: 8px; +} + +.model-actions button { + padding: 6px 12px; + font-size: 12px; + border-radius: 6px; + border: none; + cursor: pointer; + transition: all 0.2s; +} + +.btn-set-active { + background: var(--green-bg); + color: var(--green); + border: 1px solid var(--green-border); +} + +.btn-set-active:hover { + background: var(--green); + color: white; +} + +.btn-delete { + background: var(--red-bg); + color: var(--red); + border: 1px solid var(--red-border); +} + +.btn-delete:hover { + background: var(--red); + color: white; +} + +/* 按钮 */ +.btn { + padding: 10px 20px; + border-radius: 8px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + border: none; + display: inline-flex; + align-items: center; + gap: 8px; + transition: all 0.2s; +} + +.btn-sm { + padding: 6px 12px; + font-size: 12px; +} + +.btn-lg { + padding: 12px 28px; + font-size: 15px; +} + +.btn-primary { + background: var(--green); + color: white; +} + +.btn-primary:hover { + background: #16a34a; +} + +.btn-secondary { + background: var(--bg-secondary); + color: var(--text-primary); + border: 1px solid var(--border-color); +} + +.btn-secondary:hover { + background: var(--bg-card-hover); +} + +.save-actions { + display: flex; + justify-content: center; + gap: 16px; + padding: 20px 0; +} + +/* 响应式 */ +@media (max-width: 768px) { + .form-grid { + grid-template-columns: 1fr; + } + + .provider-grid { + grid-template-columns: repeat(3, 1fr); + } + + .save-actions { + flex-direction: column; + } + + .save-actions .btn { + width: 100%; + justify-content: center; + } +} diff --git a/app/static/ai_config.html b/app/static/ai_config.html new file mode 100644 index 0000000..22e50cb --- /dev/null +++ b/app/static/ai_config.html @@ -0,0 +1,189 @@ + + + + + + AI模型配置 - 期货智析 + + + + +
+
+ + +
+ +
+
+ +
+
+

AI提供商

+
+
+ +
+
+ + +
+
+

API配置

+
+
+
+ + +
+
+ +
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+

模型参数

+
+
+
+ + +
+ 精确 + 创造 +
+
+
+ + +
+
+
+ + +
+
+

分析设置

+
+
+
+
+ 技术分析 + 基于K线和技术指标进行分析 +
+ +
+
+
+ 基本面分析 + 结合宏观经济和行业数据 +
+ +
+
+
+ 情绪分析 + 分析市场情绪和新闻舆情 +
+ +
+
+ + +
+
+ + +
+
+
+ + +
+
+

已保存的模型

+ +
+
+ +
+
+ + +
+ + +
+
+
+
+ + + + diff --git a/app/static/ai_config.js b/app/static/ai_config.js new file mode 100644 index 0000000..320b1b6 --- /dev/null +++ b/app/static/ai_config.js @@ -0,0 +1,298 @@ +const API_BASE = '/api/ai-config'; + +let currentConfig = null; +let selectedProvider = 'openai'; + +document.addEventListener('DOMContentLoaded', function() { + loadProviders(); + loadConfig(); + initEventListeners(); +}); + +function initEventListeners() { + document.getElementById('api-provider').addEventListener('change', function() { + selectedProvider = this.value; + updateProviderModels(); + }); + + document.getElementById('temperature').addEventListener('input', function() { + document.getElementById('temp-value').textContent = this.value; + }); +} + +async function loadProviders() { + try { + const response = await fetch(`${API_BASE}/providers`); + const data = await response.json(); + if (data.success) { + renderProviders(data.data); + } + } catch (error) { + console.error('加载提供商失败:', error); + renderProviders(getDefaultProviders()); + } +} + +function getDefaultProviders() { + return [ + { id: 'openai', name: 'OpenAI', icon: 'fas fa-brain' }, + { id: 'anthropic', name: 'Claude', icon: 'fas fa-robot' }, + { id: 'google', name: 'Gemini', icon: 'fas fa-gem' }, + { id: 'aliyun', name: '通义千问', icon: 'fas fa-cloud' }, + { id: 'baidu', name: '文心一言', icon: 'fas fa-comments' }, + { id: 'zhipu', name: '智谱清言', icon: 'fas fa-lightbulb' } + ]; +} + +function renderProviders(providers) { + const grid = document.getElementById('provider-grid'); + const iconMap = { + 'openai': 'fas fa-brain', + 'anthropic': 'fas fa-robot', + 'google': 'fas fa-gem', + 'aliyun': 'fas fa-cloud', + 'baidu': 'fas fa-comments', + 'zhipu': 'fas fa-lightbulb', + 'custom': 'fas fa-cog' + }; + + grid.innerHTML = providers.map(p => ` +
+ +
${p.name}
+
+ `).join(''); + + grid.querySelectorAll('.provider-card').forEach(card => { + card.addEventListener('click', function() { + grid.querySelectorAll('.provider-card').forEach(c => c.classList.remove('active')); + this.classList.add('active'); + selectedProvider = this.dataset.provider; + document.getElementById('api-provider').value = selectedProvider; + updateProviderModels(); + }); + }); +} + +function updateProviderModels() { + const modelSelect = document.getElementById('model-id'); + const modelMap = { + 'openai': ['gpt-4o', 'gpt-4-turbo', 'gpt-3.5-turbo'], + 'anthropic': ['claude-3-opus', 'claude-3-sonnet', 'claude-3-haiku'], + 'google': ['gemini-pro', 'gemini-pro-vision'], + 'aliyun': ['qwen-max', 'qwen-plus', 'qwen-turbo'], + 'baidu': ['ernie-4.0', 'ernie-3.5', 'ernie-speed'], + 'zhipu': ['glm-4', 'glm-3-turbo'], + 'custom': ['custom-model'] + }; + + const apiBaseMap = { + 'openai': 'https://api.openai.com/v1', + 'anthropic': 'https://api.anthropic.com/v1', + 'google': 'https://generativelanguage.googleapis.com/v1beta', + 'aliyun': 'https://dashscope.aliyuncs.com/compatible-mode/v1', + 'baidu': 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop', + 'zhipu': 'https://open.bigmodel.cn/api/paas/v4', + 'custom': '' + }; + + const models = modelMap[selectedProvider] || ['custom-model']; + modelSelect.innerHTML = models.map(m => ``).join(''); + document.getElementById('api-base').value = apiBaseMap[selectedProvider] || ''; +} + +async function loadConfig() { + try { + const response = await fetch(API_BASE); + const result = await response.json(); + if (result.success && result.data) { + currentConfig = result.data; + populateForm(currentConfig); + renderModelsList(currentConfig.models || []); + } + } catch (error) { + console.error('加载配置失败:', error); + } +} + +function populateForm(config) { + if (config.models && config.models.length > 0) { + const activeModel = config.models.find(m => m.enabled) || config.models[0]; + document.getElementById('api-provider').value = activeModel.provider || 'openai'; + document.getElementById('api-key').value = activeModel.api_key || ''; + document.getElementById('api-base').value = activeModel.api_base || ''; + document.getElementById('model-id').value = activeModel.model_id || 'gpt-4o'; + document.getElementById('temperature').value = activeModel.temperature || 0.7; + document.getElementById('temp-value').textContent = activeModel.temperature || 0.7; + document.getElementById('max-tokens').value = activeModel.max_tokens || 2000; + } + + if (config.analysis_settings) { + document.getElementById('enable-technical').checked = config.analysis_settings.enable_technical_analysis !== false; + document.getElementById('enable-fundamental').checked = config.analysis_settings.enable_fundamental_analysis === true; + document.getElementById('enable-sentiment').checked = config.analysis_settings.enable_sentiment_analysis === true; + document.getElementById('risk-tolerance').value = config.analysis_settings.risk_tolerance || 'medium'; + document.getElementById('max-position').value = config.analysis_settings.max_position_pct || 10; + } +} + +function renderModelsList(models) { + const list = document.getElementById('models-list'); + if (!models || models.length === 0) { + list.innerHTML = '
暂无已保存的模型
'; + return; + } + + list.innerHTML = models.map((model, index) => ` +
+
+
+
+
${model.model_name || model.model_id}
+
${getProviderName(model.provider || model.api_base)}
+
+
+
+ ${!model.enabled ? `` : '默认'} + +
+
+ `).join(''); +} + +function getProviderName(apiBase) { + const map = { + 'openai': 'OpenAI', + 'anthropic': 'Anthropic', + 'google': 'Google', + 'aliyun': '阿里云', + 'baidu': '百度', + 'zhipu': '智谱' + }; + return map[apiBase] || apiBase; +} + +function toggleApiKeyVisibility() { + const input = document.getElementById('api-key'); + const icon = document.querySelector('.toggle-visibility i'); + if (input.type === 'password') { + input.type = 'text'; + icon.className = 'fas fa-eye-slash'; + } else { + input.type = 'password'; + icon.className = 'fas fa-eye'; + } +} + +async function testConnection() { + const resultEl = document.getElementById('test-result'); + resultEl.textContent = '测试中...'; + resultEl.className = 'test-result'; + + const config = { + model_name: document.getElementById('model-id').value, + api_key: document.getElementById('api-key').value, + api_base: document.getElementById('api-base').value, + model_id: document.getElementById('model-id').value, + temperature: parseFloat(document.getElementById('temperature').value), + max_tokens: parseInt(document.getElementById('max-tokens').value), + enabled: true + }; + + try { + const response = await fetch(`${API_BASE}/test`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(config) + }); + const data = await response.json(); + + if (data.success) { + resultEl.textContent = '✓ 连接成功'; + resultEl.className = 'test-result success'; + } else { + resultEl.textContent = '✗ ' + data.message; + resultEl.className = 'test-result error'; + } + } catch (error) { + resultEl.textContent = '✗ 连接失败: ' + error.message; + resultEl.className = 'test-result error'; + } +} + +async function saveConfig() { + const models = currentConfig?.models || []; + const existingIndex = models.findIndex(m => m.provider === selectedProvider); + + const newModel = { + model_name: document.getElementById('model-id').value, + provider: selectedProvider, + api_key: document.getElementById('api-key').value, + api_base: document.getElementById('api-base').value, + model_id: document.getElementById('model-id').value, + temperature: parseFloat(document.getElementById('temperature').value), + max_tokens: parseInt(document.getElementById('max-tokens').value), + enabled: true + }; + + if (existingIndex >= 0) { + models[existingIndex] = { ...models[existingIndex], ...newModel }; + } else { + models.push(newModel); + } + + const config = { + models: models, + active_model: selectedProvider, + analysis_settings: { + enable_technical_analysis: document.getElementById('enable-technical').checked, + enable_fundamental_analysis: document.getElementById('enable-fundamental').checked, + enable_sentiment_analysis: document.getElementById('enable-sentiment').checked, + risk_tolerance: document.getElementById('risk-tolerance').value, + max_position_pct: parseInt(document.getElementById('max-position').value) + } + }; + + try { + const response = await fetch(API_BASE, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(config) + }); + const data = await response.json(); + + if (data.success) { + alert('配置保存成功!'); + currentConfig = config; + renderModelsList(models); + } else { + alert('保存失败: ' + data.message); + } + } catch (error) { + alert('保存失败: ' + error.message); + } +} + +function setActiveModel(index) { + if (!currentConfig || !currentConfig.models) return; + + currentConfig.models.forEach((m, i) => { + m.enabled = i === index; + }); + + saveConfig(); +} + +function deleteModel(index) { + if (!confirm('确定要删除这个模型吗?')) return; + + if (!currentConfig || !currentConfig.models) return; + + currentConfig.models.splice(index, 1); + saveConfig(); +} + +function addNewModel() { + document.getElementById('api-key').value = ''; + document.getElementById('api-key').focus(); +} diff --git a/app/static/futures_analysis.css b/app/static/futures_analysis.css new file mode 100644 index 0000000..f256196 --- /dev/null +++ b/app/static/futures_analysis.css @@ -0,0 +1,938 @@ +:root { + --bg-primary: #0d0f14; + --bg-secondary: #151820; + --bg-card: #1a1d28; + --bg-card-hover: #222633; + --border-color: #2a2d3a; + --text-primary: #e8eaed; + --text-secondary: #9aa0ab; + --text-muted: #6b7280; + --green: #22c55e; + --green-bg: rgba(34, 197, 94, 0.15); + --green-border: rgba(34, 197, 94, 0.3); + --red: #ef4444; + --red-bg: rgba(239, 68, 68, 0.15); + --red-border: rgba(239, 68, 68, 0.3); + --orange: #f59e0b; + --orange-bg: rgba(245, 158, 11, 0.15); + --blue: #3b82f6; + --purple: #8b5cf6; + --accent: #22c55e; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif; + background: var(--bg-primary); + color: var(--text-primary); + line-height: 1.5; +} + +.app-container { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +/* 顶部导航 */ +.top-nav { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 24px; + background: var(--bg-secondary); + border-bottom: 1px solid var(--border-color); + position: sticky; + top: 0; + z-index: 100; +} + +.nav-left { + display: flex; + align-items: center; +} + +.logo { + display: flex; + align-items: center; + gap: 12px; +} + +.logo-icon { + width: 36px; + height: 36px; + background: var(--green-bg); + border: 1px solid var(--green-border); + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + color: var(--green); + font-size: 18px; +} + +.logo-text { + display: flex; + flex-direction: column; +} + +.logo-title { + font-size: 16px; + font-weight: 600; + color: var(--text-primary); +} + +.logo-subtitle { + font-size: 11px; + color: var(--text-secondary); +} + +.nav-center { + display: flex; + gap: 8px; +} + +.nav-item { + padding: 8px 16px; + color: var(--text-secondary); + text-decoration: none; + font-size: 14px; + font-weight: 500; + border-radius: 6px; + transition: all 0.2s; + position: relative; +} + +.nav-item:hover { + color: var(--text-primary); + background: var(--bg-card); +} + +.nav-item.active { + color: var(--green); +} + +.nav-item.active::after { + content: ''; + position: absolute; + bottom: -4px; + left: 50%; + transform: translateX(-50%); + width: 24px; + height: 2px; + background: var(--green); + border-radius: 1px; +} + +.nav-right { + display: flex; + align-items: center; + gap: 16px; +} + +.datetime { + display: flex; + align-items: center; + gap: 6px; + color: var(--text-secondary); + font-size: 13px; +} + +.nav-icon-btn { + color: var(--text-secondary); + text-decoration: none; + font-size: 16px; + padding: 6px; + border-radius: 6px; + transition: all 0.2s; +} + +.nav-icon-btn:hover { + color: var(--text-primary); + background: var(--bg-card); +} + +.notification { + position: relative; + color: var(--text-secondary); + font-size: 16px; + cursor: pointer; + padding: 6px; +} + +.notification .badge { + position: absolute; + top: 0; + right: 0; + width: 16px; + height: 16px; + background: var(--red); + border-radius: 50%; + font-size: 10px; + display: flex; + align-items: center; + justify-content: center; + color: white; +} + +/* 主内容区 */ +.main-content { + flex: 1; + padding: 20px 24px; + max-width: 1400px; + margin: 0 auto; + width: 100%; +} + +.view { + display: none; +} + +.view.active { + display: block; +} + +/* 工具栏 */ +.toolbar { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; +} + +.search-box { + flex: 1; + max-width: 600px; + display: flex; + align-items: center; + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 10px 14px; + gap: 10px; +} + +.search-box i { + color: var(--text-muted); +} + +.search-box input { + flex: 1; + background: none; + border: none; + outline: none; + color: var(--text-primary); + font-size: 14px; +} + +.search-box input::placeholder { + color: var(--text-muted); +} + +.view-toggle { + display: flex; + gap: 4px; +} + +.toggle-btn { + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: 6px; + color: var(--text-secondary); + cursor: pointer; + transition: all 0.2s; +} + +.toggle-btn.active { + background: var(--green); + border-color: var(--green); + color: white; +} + +/* 筛选栏 */ +.filter-bar { + display: flex; + justify-content: space-between; + align-items: center; + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: 10px; + padding: 12px 16px; + margin-bottom: 16px; +} + +.filter-group, .sort-group { + display: flex; + align-items: center; + gap: 8px; +} + +.filter-label { + color: var(--text-secondary); + font-size: 13px; +} + +.filter-btn { + padding: 6px 12px; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 6px; + color: var(--text-secondary); + font-size: 13px; + cursor: pointer; + transition: all 0.2s; +} + +.filter-btn:hover { + border-color: var(--text-muted); + color: var(--text-primary); +} + +.filter-btn.active { + background: var(--green); + border-color: var(--green); + color: white; +} + +.sort-select { + padding: 6px 12px; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 6px; + color: var(--text-primary); + font-size: 13px; + cursor: pointer; + outline: none; +} + +/* 统计栏 */ +.stats-bar { + display: flex; + gap: 20px; + margin-bottom: 20px; + font-size: 14px; + color: var(--text-secondary); +} + +.stats-bar strong { + color: var(--text-primary); +} + +.stat-up { + color: var(--green); +} + +.stat-down { + color: var(--red); +} + +/* 品种卡片网格 */ +.futures-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); + gap: 16px; +} + +.futures-card { + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 18px; + cursor: pointer; + transition: all 0.2s; +} + +.futures-card:hover { + background: var(--bg-card-hover); + border-color: var(--text-muted); + transform: translateY(-2px); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 12px; +} + +.card-title { + display: flex; + align-items: center; + gap: 8px; +} + +.card-name { + font-size: 16px; + font-weight: 600; +} + +.card-code { + font-size: 12px; + color: var(--text-muted); + background: var(--bg-secondary); + padding: 2px 6px; + border-radius: 4px; +} + +.card-price { + text-align: right; +} + +.price-value { + font-size: 20px; + font-weight: 600; +} + +.price-change { + font-size: 13px; + display: flex; + align-items: center; + gap: 4px; + justify-content: flex-end; +} + +.up { + color: var(--green); +} + +.down { + color: var(--red); +} + +.suggestion-badge { + display: inline-block; + padding: 4px 10px; + border-radius: 4px; + font-size: 12px; + font-weight: 500; + margin-bottom: 12px; +} + +.suggestion-badge.up { + background: var(--green-bg); + color: var(--green); + border: 1px solid var(--green-border); +} + +.suggestion-badge.down { + background: var(--red-bg); + color: var(--red); + border: 1px solid var(--red-border); +} + +.suggestion-badge.neutral { + background: rgba(107, 114, 128, 0.15); + color: var(--text-muted); + border: 1px solid rgba(107, 114, 128, 0.3); +} + +.card-section { + margin-bottom: 12px; +} + +.section-label { + font-size: 12px; + color: var(--text-secondary); + margin-bottom: 8px; + display: flex; + align-items: center; + gap: 6px; +} + +.period-tags { + display: flex; + gap: 6px; + flex-wrap: wrap; +} + +.period-tag { + padding: 4px 10px; + border-radius: 6px; + font-size: 12px; + display: flex; + align-items: center; + gap: 4px; +} + +.period-tag.up { + background: var(--green-bg); + color: var(--green); + border: 1px solid var(--green-border); +} + +.period-tag.down { + background: var(--red-bg); + color: var(--red); + border: 1px solid var(--red-border); +} + +.period-tag.neutral { + background: rgba(107, 114, 128, 0.1); + color: var(--text-muted); + border: 1px solid rgba(107, 114, 128, 0.2); +} + +.progress-bar { + height: 6px; + background: var(--bg-secondary); + border-radius: 3px; + overflow: hidden; + margin-bottom: 4px; +} + +.progress-fill { + height: 100%; + border-radius: 3px; + transition: width 0.3s; +} + +.progress-fill.up { + background: var(--green); +} + +.progress-fill.down { + background: var(--red); +} + +.progress-fill.orange { + background: var(--orange); +} + +.progress-info { + display: flex; + justify-content: space-between; + font-size: 12px; +} + +.progress-label { + color: var(--text-secondary); +} + +.progress-value { + font-weight: 500; +} + +.key-levels-row { + display: flex; + justify-content: space-between; + font-size: 12px; +} + +.level-label { + color: var(--text-secondary); +} + +.level-value { + font-weight: 500; +} + +.card-footer { + display: flex; + justify-content: flex-end; + padding-top: 12px; + border-top: 1px solid var(--border-color); +} + +.detail-link { + color: var(--text-secondary); + font-size: 12px; + text-decoration: none; + display: flex; + align-items: center; + gap: 4px; + transition: color 0.2s; +} + +.detail-link:hover { + color: var(--green); +} + +/* 详情视图 */ +.detail-header { + margin-bottom: 20px; +} + +.back-btn { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px 16px; + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: 8px; + color: var(--text-secondary); + font-size: 14px; + cursor: pointer; + margin-bottom: 16px; + transition: all 0.2s; +} + +.back-btn:hover { + background: var(--bg-card-hover); + color: var(--text-primary); +} + +.detail-title-bar { + display: flex; + align-items: center; + justify-content: space-between; + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 16px 20px; +} + +.price-info { + display: flex; + flex-direction: column; +} + +.current-price { + font-size: 28px; + font-weight: 700; + color: var(--green); +} + +.price-change { + font-size: 14px; + margin-top: 4px; +} + +.quote-info { + display: flex; + gap: 24px; +} + +.quote-item { + display: flex; + flex-direction: column; + align-items: center; +} + +.quote-label { + font-size: 12px; + color: var(--text-muted); + margin-bottom: 4px; +} + +.quote-value { + font-size: 14px; + font-weight: 500; +} + +/* 周期选择 */ +.period-selector { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 16px; +} + +.period-selector i { + color: var(--green); +} + +.period-label { + color: var(--text-secondary); + font-size: 13px; + margin-right: 8px; +} + +.period-btn { + padding: 8px 16px; + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: 8px; + color: var(--text-secondary); + font-size: 13px; + cursor: pointer; + transition: all 0.2s; +} + +.period-btn:hover { + border-color: var(--text-muted); +} + +.period-btn.active { + background: var(--green); + border-color: var(--green); + color: white; +} + +/* 详情主体 */ +.detail-body { + display: grid; + grid-template-columns: 1fr 340px; + gap: 20px; +} + +.chart-section { + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 16px; +} + +.kline-chart { + width: 100%; + height: 500px; +} + +/* 分析面板 */ +.analysis-panel { + display: flex; + flex-direction: column; + gap: 16px; +} + +.panel-card { + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 16px; +} + +.panel-title { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + font-weight: 600; + margin-bottom: 14px; + color: var(--text-primary); +} + +.panel-title i { + color: var(--green); +} + +/* 交易建议 */ +.suggestion-box { + padding: 14px; + border-radius: 8px; + margin-bottom: 14px; + text-align: center; +} + +.suggestion-box.up { + background: var(--green-bg); + border: 1px solid var(--green-border); +} + +.suggestion-box.down { + background: var(--red-bg); + border: 1px solid var(--red-border); +} + +.suggestion-label { + font-size: 12px; + color: var(--text-secondary); + margin-bottom: 6px; +} + +.suggestion-action { + font-size: 18px; + font-weight: 600; + margin-bottom: 6px; +} + +.suggestion-box.up .suggestion-action { + color: var(--green); +} + +.suggestion-box.down .suggestion-action { + color: var(--red); +} + +.suggestion-reason { + font-size: 12px; + color: var(--text-secondary); +} + +.suggestion-details { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; +} + +.detail-row { + display: flex; + justify-content: space-between; + padding: 8px 10px; + background: var(--bg-secondary); + border-radius: 6px; + font-size: 12px; +} + +.detail-label { + color: var(--text-muted); +} + +.detail-value { + font-weight: 500; +} + +/* 技术指标 */ +.indicators-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; +} + +.indicator-item { + padding: 12px; + background: var(--bg-secondary); + border-radius: 8px; +} + +.indicator-name { + font-size: 12px; + color: var(--text-muted); + margin-bottom: 6px; +} + +.indicator-value { + font-size: 14px; + font-weight: 600; + margin-bottom: 4px; +} + +.indicator-detail { + font-size: 11px; + color: var(--text-secondary); +} + +/* 关键点位 */ +.levels-section { + margin-bottom: 12px; +} + +.levels-section:last-child { + margin-bottom: 0; +} + +.levels-header { + font-size: 12px; + font-weight: 600; + margin-bottom: 8px; +} + +.levels-header.resistance { + color: var(--red); +} + +.levels-header.support { + color: var(--green); +} + +.level-row { + display: flex; + justify-content: space-between; + padding: 8px 0; + border-bottom: 1px solid var(--border-color); + font-size: 13px; +} + +.level-row:last-child { + border-bottom: none; +} + +.level-row span:first-child { + color: var(--text-secondary); +} + +/* 多周期一致性 */ +.consistency-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 0; + border-bottom: 1px solid var(--border-color); +} + +.consistency-row:last-child { + border-bottom: none; +} + +.period-name { + font-size: 13px; + color: var(--text-secondary); +} + +.consistency-badge { + padding: 4px 10px; + border-radius: 4px; + font-size: 12px; + display: flex; + align-items: center; + gap: 4px; +} + +.consistency-badge.up { + background: var(--green-bg); + color: var(--green); +} + +.consistency-badge.down { + background: var(--red-bg); + color: var(--red); +} + +.consistency-badge.neutral { + background: rgba(107, 114, 128, 0.15); + color: var(--text-muted); +} + +/* 响应式 */ +@media (max-width: 1200px) { + .detail-body { + grid-template-columns: 1fr; + } + + .analysis-panel { + display: grid; + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 768px) { + .top-nav { + flex-wrap: wrap; + gap: 12px; + } + + .nav-center { + order: 3; + width: 100%; + justify-content: center; + } + + .futures-grid { + grid-template-columns: 1fr; + } + + .analysis-panel { + grid-template-columns: 1fr; + } + + .detail-title-bar { + flex-direction: column; + gap: 16px; + } + + .quote-info { + width: 100%; + justify-content: space-between; + } +} diff --git a/app/static/futures_analysis.html b/app/static/futures_analysis.html new file mode 100644 index 0000000..9a1e700 --- /dev/null +++ b/app/static/futures_analysis.html @@ -0,0 +1,283 @@ + + + + + + 期货智析 - 智能期货期权分析系统 + + + + +
+ +
+ + + +
+ + +
+ +
+ +
+ +
+ + +
+
+ +
+
+ 分类: + + + + + +
+
+ 排序: + +
+
+ + +
+ 8 个品种 + 7 + 1 +
+ + +
+ +
+
+ + +
+ +
+ +
+
+ ¥2,150 + + +196.00 (+10.06%) + +
+
+
+ 开盘 + 1,960 +
+
+ 最高 + 2,200 +
+
+ 最低 + 1,940 +
+
+ 持仓量 + 45,600 +
+
+
+
+ + +
+ + 周期选择 + + + + +
+ + +
+ +
+
+
+ + +
+ +
+
+ + 交易建议 +
+
+
操作建议
+
逢低做多
+
涨停突破,地缘风险推升运价
+
+
+
+ 建议入场 + 2,137.1 +
+
+ 目标价位 + 2,236 +
+
+ 止损价位 + 2,107 +
+
+ 风险等级 + +
+
+
+ + +
+
+ + 技术指标 +
+
+
+
MACD
+
金叉
+
DIF: -0.0147
+
+
+
RSI
+
47
+
正常
+
+
+
布林带
+
中轨
+
区间: 2086-2215
+
+
+
KDJ
+
中性
+
K: 71 D: 87
+
+
+
+ + +
+
+ + 关键点位 +
+
+
压力位
+
+ 压力 1 + 2,200 +
+
+ 压力 2 + 2,300 +
+
+ 压力 3 + 2,400 +
+
+
+
支撑位
+
+ 支撑 1 + 2,000 +
+
+ 支撑 2 + 1,900 +
+
+ 支撑 3 + 1,800 +
+
+
+ + +
+
+ + 多周期一致性 +
+
+
+ 5分钟 + 上涨 +
+
+ 15分钟 + 上涨 +
+
+ 30分钟 + 上涨 +
+
+ 60分钟 + 震荡 +
+
+
+
+
+
+
+
+ + + + + diff --git a/app/static/futures_analysis.js b/app/static/futures_analysis.js new file mode 100644 index 0000000..acae11e --- /dev/null +++ b/app/static/futures_analysis.js @@ -0,0 +1,825 @@ +const API_BASE = '/api/v1/futures'; + +let klineChart = null; +let currentSymbol = null; +let currentPeriod = '15'; +let allFuturesData = []; + +document.addEventListener('DOMContentLoaded', function() { + updateTime(); + setInterval(updateTime, 1000); + + initEventListeners(); + loadFuturesList(); +}); + +function updateTime() { + const now = new Date(); + const timeStr = now.getFullYear() + '/' + + String(now.getMonth() + 1).padStart(2, '0') + '/' + + String(now.getDate()).padStart(2, '0') + ' ' + + String(now.getHours()).padStart(2, '0') + ':' + + String(now.getMinutes()).padStart(2, '0') + ':' + + String(now.getSeconds()).padStart(2, '0'); + document.getElementById('current-time').textContent = timeStr; +} + +function initEventListeners() { + document.getElementById('back-btn').addEventListener('click', showListView); + + document.querySelectorAll('.period-btn').forEach(btn => { + btn.addEventListener('click', function() { + document.querySelectorAll('.period-btn').forEach(b => b.classList.remove('active')); + this.classList.add('active'); + currentPeriod = this.dataset.period; + loadKlineData(currentSymbol, currentPeriod); + }); + }); + + document.getElementById('search-input').addEventListener('input', function() { + filterFuturesList(this.value); + }); + + document.querySelectorAll('.filter-btn').forEach(btn => { + btn.addEventListener('click', function() { + document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active')); + this.classList.add('active'); + filterByCategory(this.dataset.category); + }); + }); + + document.getElementById('sort-select').addEventListener('change', function() { + sortFuturesList(this.value); + }); +} + +function showListView() { + document.getElementById('list-view').classList.add('active'); + document.getElementById('detail-view').classList.remove('active'); + if (klineChart) { + klineChart.dispose(); + klineChart = null; + } +} + +function showDetailView(symbol) { + currentSymbol = symbol; + document.getElementById('list-view').classList.remove('active'); + document.getElementById('detail-view').classList.add('active'); + loadFuturesDetail(symbol); + loadKlineData(symbol, currentPeriod); +} + +async function loadFuturesList() { + try { + const response = await fetch(`${API_BASE}/list`); + const data = await response.json(); + if (data.success) { + allFuturesData = data.data; + renderFuturesGrid(allFuturesData); + updateStats(allFuturesData); + } + } catch (error) { + console.error('加载品种列表失败:', error); + loadFuturesFromConfig(); + } +} + +async function loadFuturesFromConfig() { + try { + const response = await fetch('/api/v1/config'); + const config = await response.json(); + const futuresConfig = config.futures || {}; + + allFuturesData = Object.entries(futuresConfig).map(([name, symbol]) => ({ + symbol: symbol, + name: name, + price: 0, + change: 0, + changePct: 0, + suggestion: '等待数据', + suggestionType: 'neutral', + periods: { '5': 'neutral', '15': 'neutral', '30': 'neutral', '60': 'neutral' }, + successRate: 0, + trendScore: 0, + resistance: 0, + support: 0, + open: 0, + high: 0, + low: 0, + volume: 0 + })); + + renderFuturesGrid(allFuturesData); + updateStats(allFuturesData); + } catch (error) { + console.error('加载配置失败:', error); + const mockData = generateMockFuturesData(); + allFuturesData = mockData; + renderFuturesGrid(mockData); + updateStats(mockData); + } +} + +function generateMockFuturesData() { + return [ + { + symbol: 'EC', + name: '集运指数', + price: 2150, + change: 196, + changePct: 10.06, + suggestion: '逢低做多', + suggestionType: 'up', + periods: { '5': 'up', '15': 'up', '30': 'up', '60': 'up' }, + successRate: 80, + trendScore: 90, + resistance: 2200, + support: 2000, + open: 1960, + high: 2200, + low: 1940, + volume: 45600 + }, + { + symbol: 'AU', + name: '黄金', + price: 685.2, + change: 12.45, + changePct: 1.85, + suggestion: '逢低做多', + suggestionType: 'up', + periods: { '5': 'up', '15': 'up', '30': 'up', '60': 'up' }, + successRate: 78, + trendScore: 92, + resistance: 692, + support: 678, + open: 672.75, + high: 688, + low: 670, + volume: 128000 + }, + { + symbol: 'AG', + name: '白银', + price: 8250, + change: 165, + changePct: 2.04, + suggestion: '逢低做多', + suggestionType: 'up', + periods: { '5': 'up', '15': 'up', '30': 'up', '60': 'up' }, + successRate: 75, + trendScore: 88, + resistance: 8350, + support: 8100, + open: 8085, + high: 8280, + low: 8050, + volume: 95000 + }, + { + symbol: 'SC', + name: '原油', + price: 528.6, + change: 12.1, + changePct: 2.35, + suggestion: '逢低做多', + suggestionType: 'up', + periods: { '5': 'up', '15': 'up', '30': 'up', '60': 'neutral' }, + successRate: 72, + trendScore: 85, + resistance: 535, + support: 518, + open: 516.5, + high: 530, + low: 515, + volume: 78000 + }, + { + symbol: 'I', + name: '铁矿石', + price: 785.5, + change: 28, + changePct: 3.7, + suggestion: '逢低做多', + suggestionType: 'up', + periods: { '5': 'up', '15': 'up', '30': 'up', '60': 'up' }, + successRate: 68, + trendScore: 82, + resistance: 792, + support: 770, + open: 757.5, + high: 788, + low: 755, + volume: 156000 + }, + { + symbol: 'CU', + name: '铜', + price: 80610, + change: 112, + changePct: 0.14, + suggestion: '观望等待', + suggestionType: 'neutral', + periods: { '5': 'neutral', '15': 'up', '30': 'neutral', '60': 'up' }, + successRate: 58, + trendScore: 65, + resistance: 81200, + support: 79800, + open: 80498, + high: 80850, + low: 80200, + volume: 42000 + }, + { + symbol: 'P', + name: '棕榈油', + price: 8750, + change: 0, + changePct: 0, + suggestion: '观望等待', + suggestionType: 'neutral', + periods: { '5': 'neutral', '15': 'neutral', '30': 'neutral', '60': 'neutral' }, + successRate: 52, + trendScore: 50, + resistance: 8850, + support: 8650, + open: 8750, + high: 8780, + low: 8720, + volume: 65000 + }, + { + symbol: 'M', + name: '豆粕', + price: 2985, + change: -51, + changePct: -1.68, + suggestion: '逢高做空', + suggestionType: 'down', + periods: { '5': 'down', '15': 'down', '30': 'down', '60': 'neutral' }, + successRate: 65, + trendScore: 35, + resistance: 3050, + support: 2920, + open: 3036, + high: 3040, + low: 2980, + volume: 185000 + } + ]; +} + +function renderFuturesGrid(data) { + const grid = document.getElementById('futures-grid'); + grid.innerHTML = data.map(item => ` +
+
+
+ ${item.name} + (${item.symbol}) +
+
+
¥${formatNumber(item.price)}
+
+ + +${formatNumber(item.change)} (+${item.changePct.toFixed(2)}%) +
+
+
+ ${item.suggestion} +
+ +
+ 5分 + 15分 + 30分 + 60分 +
+
+
+ +
+
+
+
+ + ${item.successRate}% +
+
+
+ +
+
+
+
+ + ${item.trendScore}/100 +
+
+
+ +
+ 压力: ${formatNumber(item.resistance)} + 支撑: ${formatNumber(item.support)} +
+
+ +
+ `).join(''); +} + +function getArrow(type) { + if (type === 'up') return 'up'; + if (type === 'down') return 'down'; + return 'right'; +} + +function formatNumber(num) { + return num.toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 2 }); +} + +function updateStats(data) { + document.getElementById('total-count').textContent = data.length; + const upCount = data.filter(d => d.change >= 0).length; + const downCount = data.length - upCount; + document.getElementById('up-count').textContent = upCount; + document.getElementById('down-count').textContent = downCount; +} + +function filterFuturesList(keyword) { + keyword = keyword.toLowerCase(); + const filtered = allFuturesData.filter(item => + item.name.toLowerCase().includes(keyword) || + item.symbol.toLowerCase().includes(keyword) + ); + renderFuturesGrid(filtered); + updateStats(filtered); +} + +function filterByCategory(category) { + if (category === 'all') { + renderFuturesGrid(allFuturesData); + updateStats(allFuturesData); + } else { + const categoryMap = { + 'energy': ['SC', 'EC', 'FU', 'LU', 'BU', 'RU', 'NR', 'ZC'], + 'metal': ['AU', 'AG', 'CU', 'AL', 'ZN', 'NI', 'SN', 'PB', 'SS', 'RB', 'HC', 'I', 'J', 'JM', 'SF', 'SM'], + 'agriculture': ['M', 'RM', 'C', 'CS', 'A', 'B', 'Y', 'P', 'OI', 'CF', 'SR', 'AP', 'JD', 'LH', 'MA', 'TA', 'EG', 'PP', 'L', 'V', 'SA', 'FG', 'UR', 'SP'], + 'finance': ['IF', 'IC', 'IH', 'IM', 'T', 'TF', 'TS', 'TL'] + }; + const symbols = categoryMap[category] || []; + const filtered = allFuturesData.filter(item => { + const symbolBase = item.symbol.replace(/[0-9]/g, '').toUpperCase(); + return symbols.includes(symbolBase); + }); + renderFuturesGrid(filtered); + updateStats(filtered); + } +} + +function sortFuturesList(sortBy) { + let sorted = [...allFuturesData]; + switch(sortBy) { + case 'success_rate': + sorted.sort((a, b) => b.successRate - a.successRate); + break; + case 'trend_score': + sorted.sort((a, b) => b.trendScore - a.trendScore); + break; + case 'change_pct': + sorted.sort((a, b) => b.changePct - a.changePct); + break; + case 'name': + sorted.sort((a, b) => a.name.localeCompare(b.name, 'zh')); + break; + } + renderFuturesGrid(sorted); +} + +async function loadFuturesDetail(symbol) { + try { + const response = await fetch(`${API_BASE}/detail/${symbol}`); + const data = await response.json(); + if (data.success) { + updateDetailView(data.data); + } + } catch (error) { + console.error('加载详情失败:', error); + const item = allFuturesData.find(d => d.symbol === symbol); + if (item) { + updateDetailView({ + ...item, + entryPrice: item.price * 0.99, + targetPrice: item.resistance, + stopLoss: item.support, + riskLevel: item.trendScore >= 80 ? '低' : item.trendScore >= 60 ? '中' : '高', + macd: { signal: '金叉', detail: 'DIF: -0.0147' }, + rsi: { value: 47, status: '正常' }, + boll: { signal: '中轨', detail: '区间: 2086-2215' }, + kdj: { signal: '中性', detail: 'K: 71 D: 87' }, + resistances: [item.resistance, item.resistance * 1.05, item.resistance * 1.1], + supports: [item.support, item.support * 0.95, item.support * 0.9], + periodConsistency: { + '5': item.periods['5'], + '15': item.periods['15'], + '30': item.periods['30'], + '60': item.periods['60'] + }, + suggestionReason: '技术面突破,趋势明确' + }); + } + } +} + +function updateDetailView(data) { + document.getElementById('detail-price').textContent = '¥' + formatNumber(data.price); + document.getElementById('detail-price').className = 'current-price ' + (data.change >= 0 ? 'up' : 'down'); + + const changeEl = document.getElementById('detail-change'); + const changeIcon = data.change >= 0 ? 'up' : 'down'; + changeEl.className = 'price-change ' + (data.change >= 0 ? 'up' : 'down'); + changeEl.innerHTML = ` ${data.change >= 0 ? '+' : ''}${formatNumber(data.change)} (${data.changePct >= 0 ? '+' : ''}${data.changePct.toFixed(2)}%)`; + + document.getElementById('detail-open').textContent = formatNumber(data.open); + document.getElementById('detail-high').textContent = formatNumber(data.high); + document.getElementById('detail-low').textContent = formatNumber(data.low); + document.getElementById('detail-volume').textContent = formatNumber(data.volume); + + const suggestionBox = document.getElementById('suggestion-box'); + suggestionBox.className = 'suggestion-box ' + data.suggestionType; + document.getElementById('suggestion-action').textContent = data.suggestion; + document.getElementById('suggestion-reason').textContent = data.suggestionReason || ''; + + document.getElementById('entry-price').textContent = formatNumber(data.entryPrice || data.price * 0.99); + document.getElementById('target-price').textContent = formatNumber(data.targetPrice || data.resistance); + document.getElementById('stop-loss').textContent = formatNumber(data.stopLoss || data.support); + document.getElementById('risk-level').textContent = data.riskLevel || '中'; + + if (data.macd) { + document.getElementById('macd-signal').textContent = data.macd.signal; + document.getElementById('macd-detail').textContent = data.macd.detail; + } + if (data.rsi) { + document.getElementById('rsi-value').textContent = data.rsi.value; + document.getElementById('rsi-status').textContent = data.rsi.status; + } + if (data.boll) { + document.getElementById('boll-signal').textContent = data.boll.signal; + document.getElementById('boll-detail').textContent = data.boll.detail; + } + if (data.kdj) { + document.getElementById('kdj-signal').textContent = data.kdj.signal; + document.getElementById('kdj-detail').textContent = data.kdj.detail; + } + + if (data.resistances) { + for (let i = 0; i < 3; i++) { + const el = document.getElementById(`resistance-${i + 1}`); + if (el && data.resistances[i]) { + el.querySelector('.level-value').textContent = formatNumber(data.resistances[i]); + } + } + } + if (data.supports) { + for (let i = 0; i < 3; i++) { + const el = document.getElementById(`support-${i + 1}`); + if (el && data.supports[i]) { + el.querySelector('.level-value').textContent = formatNumber(data.supports[i]); + } + } + } + + if (data.periodConsistency) { + const container = document.getElementById('period-consistency'); + const periodNames = { '5': '5分钟', '15': '15分钟', '30': '30分钟', '60': '60分钟' }; + container.innerHTML = Object.entries(data.periodConsistency).map(([period, trend]) => ` +
+ ${periodNames[period]} + + + ${trend === 'up' ? '上涨' : trend === 'down' ? '下跌' : '震荡'} + +
+ `).join(''); + } +} + +async function loadKlineData(symbol, period) { + try { + const response = await fetch(`${API_BASE}/kline/${symbol}?period=${period}`); + const data = await response.json(); + if (data.success) { + renderKlineChart(data.data); + } + } catch (error) { + console.error('加载K线数据失败:', error); + const mockKline = generateMockKlineData(); + renderKlineChart(mockKline); + } +} + +function generateMockKlineData() { + const data = []; + let basePrice = 2100; + const now = new Date(); + now.setHours(13, 0, 0, 0); + + for (let i = 0; i < 60; i++) { + const time = new Date(now.getTime() + i * 15 * 60000); + const timeStr = String(time.getHours()).padStart(2, '0') + ':' + String(time.getMinutes()).padStart(2, '0'); + + const open = basePrice + (Math.random() - 0.5) * 20; + const close = open + (Math.random() - 0.45) * 25; + const high = Math.max(open, close) + Math.random() * 10; + const low = Math.min(open, close) - Math.random() * 10; + const volume = Math.floor(Math.random() * 1000 + 200); + + data.push([timeStr, open.toFixed(2), close.toFixed(2), low.toFixed(2), high.toFixed(2), volume]); + basePrice = close; + } + + return data; +} + +function renderKlineChart(data) { + if (klineChart) { + klineChart.dispose(); + } + + const chartDom = document.getElementById('kline-chart'); + klineChart = echarts.init(chartDom, 'dark'); + + const dates = data.map(d => d[0]); + const values = data.map(d => [parseFloat(d[1]), parseFloat(d[2]), parseFloat(d[3]), parseFloat(d[4])]); + const volumes = data.map(d => [parseInt(d[5]), d[2] >= d[1] ? 1 : -1]); + + const ma5 = calculateMA(data, 5); + const ma10 = calculateMA(data, 10); + const ma20 = calculateMA(data, 20); + const macdData = calculateMACD(data); + + const option = { + backgroundColor: 'transparent', + animation: false, + legend: { + data: ['K线', 'MA5', 'MA10', 'MA20', 'DIF', 'DEA', 'MACD'], + top: 10, + left: 10, + textStyle: { color: '#9aa0ab', fontSize: 11 } + }, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'cross', + crossStyle: { color: '#999' } + }, + backgroundColor: 'rgba(26, 29, 40, 0.95)', + borderColor: '#2a2d3a', + textStyle: { color: '#e8eaed', fontSize: 12 }, + formatter: function(params) { + if (!params || params.length === 0) return ''; + let result = `
${params[0].axisValue}
`; + params.forEach(p => { + if (p.seriesName === 'K线' && p.data) { + const [o, c, l, h] = p.data; + result += `开: ${o} 收: ${c}
低: ${l} 高: ${h}`; + } else if (p.seriesName === '成交量') { + result += `
成交量: ${p.data}`; + } else if (p.seriesName === 'DIF' || p.seriesName === 'DEA') { + result += `
${p.seriesName}: ${p.data}`; + } else if (p.seriesName === 'MACD') { + result += `
MACD: ${p.data}`; + } else { + result += `
${p.seriesName}: ${p.data}`; + } + }); + return result; + } + }, + axisPointer: { + link: [{ xAxisIndex: 'all' }], + label: { + backgroundColor: '#22c55e' + } + }, + grid: [ + { left: 70, right: 20, top: 60, height: '48%' }, + { left: 70, right: 20, top: '54%', height: '14%' }, + { left: 70, right: 20, top: '73%', height: '17%' } + ], + xAxis: [ + { + type: 'category', + data: dates, + boundaryGap: true, + axisLine: { lineStyle: { color: '#2a2d3a' } }, + axisLabel: { color: '#9aa0ab', fontSize: 10 }, + splitLine: { show: false } + }, + { + type: 'category', + gridIndex: 1, + data: dates, + boundaryGap: true, + axisLine: { lineStyle: { color: '#2a2d3a' } }, + axisLabel: { show: false }, + splitLine: { show: false } + }, + { + type: 'category', + gridIndex: 2, + data: dates, + boundaryGap: true, + axisLine: { lineStyle: { color: '#2a2d3a' } }, + axisLabel: { color: '#9aa0ab', fontSize: 10 }, + splitLine: { show: false } + } + ], + yAxis: [ + { + scale: true, + axisLine: { lineStyle: { color: '#2a2d3a' } }, + axisLabel: { color: '#9aa0ab' }, + splitLine: { lineStyle: { color: '#2a2d3a', type: 'dashed' } } + }, + { + scale: true, + gridIndex: 1, + axisLine: { show: false }, + axisTick: { show: false }, + axisLabel: { show: false }, + splitLine: { show: false } + }, + { + scale: true, + gridIndex: 2, + axisLine: { lineStyle: { color: '#2a2d3a' } }, + axisLabel: { color: '#9aa0ab', fontSize: 10 }, + splitLine: { lineStyle: { color: '#2a2d3a', type: 'dashed' } } + } + ], + dataZoom: [ + { + type: 'inside', + xAxisIndex: [0, 1, 2], + start: 50, + end: 100 + }, + { + show: true, + xAxisIndex: [0, 1, 2], + type: 'slider', + bottom: 5, + height: 18, + borderColor: 'transparent', + backgroundColor: '#1a1d28', + fillerColor: 'rgba(34, 197, 94, 0.15)', + handleStyle: { color: '#22c55e' }, + textStyle: { color: '#9aa0ab' } + } + ], + series: [ + { + name: 'K线', + type: 'candlestick', + data: values, + itemStyle: { + color: '#22c55e', + color0: '#ef4444', + borderColor: '#22c55e', + borderColor0: '#ef4444' + } + }, + { + name: 'MA5', + type: 'line', + data: ma5, + lineStyle: { width: 1, color: '#f59e0b' }, + symbol: 'none' + }, + { + name: 'MA10', + type: 'line', + data: ma10, + lineStyle: { width: 1, color: '#3b82f6' }, + symbol: 'none' + }, + { + name: 'MA20', + type: 'line', + data: ma20, + lineStyle: { width: 1, color: '#8b5cf6' }, + symbol: 'none' + }, + { + name: '成交量', + type: 'bar', + xAxisIndex: 1, + yAxisIndex: 1, + data: volumes.map(v => ({ + value: v[0], + itemStyle: { + color: v[1] >= 0 ? '#22c55e' : '#ef4444', + opacity: 0.6 + } + })) + }, + { + name: 'DIF', + type: 'line', + xAxisIndex: 2, + yAxisIndex: 2, + data: macdData.dif, + lineStyle: { width: 1.5, color: '#3b82f6' }, + symbol: 'none' + }, + { + name: 'DEA', + type: 'line', + xAxisIndex: 2, + yAxisIndex: 2, + data: macdData.dea, + lineStyle: { width: 1.5, color: '#f59e0b' }, + symbol: 'none' + }, + { + name: 'MACD', + type: 'bar', + xAxisIndex: 2, + yAxisIndex: 2, + data: macdData.macd.map((val, idx) => ({ + value: val, + itemStyle: { + color: val >= 0 ? '#22c55e' : '#ef4444', + opacity: 0.7 + } + })) + } + ] + }; + + klineChart.setOption(option); + + window.addEventListener('resize', () => { + klineChart && klineChart.resize(); + }); +} + +function calculateMA(data, dayCount) { + const result = []; + for (let i = 0; i < data.length; i++) { + if (i < dayCount - 1) { + result.push('-'); + continue; + } + let sum = 0; + for (let j = 0; j < dayCount; j++) { + sum += parseFloat(data[i - j][2]); + } + result.push(parseFloat((sum / dayCount).toFixed(2))); + } + return result; +} + +function calculateMACD(data) { + const closes = data.map(d => parseFloat(d[2])); + const ema12 = calcEMA(closes, 12); + const ema26 = calcEMA(closes, 26); + + const dif = []; + for (let i = 0; i < closes.length; i++) { + if (ema12[i] !== null && ema26[i] !== null) { + dif.push(ema12[i] - ema26[i]); + } else { + dif.push(0); + } + } + + const dea = calcEMA(dif, 9); + + const macd = dif.map((d, i) => 2 * (d - (dea[i] || 0))); + + return { dif, dea, macd }; +} + +function calcEMA(data, period) { + const result = new Array(data.length).fill(null); + const multiplier = 2 / (period + 1); + + if (data.length < period) return result; + + let sum = 0; + for (let i = 0; i < period; i++) { + sum += data[i]; + } + result[period - 1] = sum / period; + + for (let i = period; i < data.length; i++) { + result[i] = (data[i] - result[i - 1]) * multiplier + result[i - 1]; + } + + return result; +} diff --git a/app/static/index.html b/app/static/index.html index 862c8f5..628ded2 100644 --- a/app/static/index.html +++ b/app/static/index.html @@ -678,6 +678,15 @@ 运行日志 + + + + 期货智析 + + + + AI配置 +