From 168f06ecf52c341ac4fb33047b475b4cee910fd2 Mon Sep 17 00:00:00 2001 From: Lxy Date: Mon, 18 May 2026 23:48:43 +0800 Subject: [PATCH 01/16] =?UTF-8?q?feat=20:=20=E5=A2=9E=E5=8A=A0=E6=99=BA?= =?UTF-8?q?=E8=83=BD=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配置 + From fd64d4357449992f9082ecb2b9ba561e358fe1bb Mon Sep 17 00:00:00 2001 From: Lxy Date: Wed, 20 May 2026 23:19:09 +0800 Subject: [PATCH 02/16] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E4=B8=BB?= =?UTF-8?q?=E9=A2=98=E5=88=87=E6=8D=A2=E5=8A=9F=E8=83=BD;=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E8=87=AA=E9=80=89=E7=AD=89=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DESIGN.md | 185 +++ app/static/futures_analysis.css | 2151 ++++++++++++++++++++++++------ app/static/futures_analysis.html | 428 +++--- app/static/futures_analysis.js | 838 ++++++------ 4 files changed, 2686 insertions(+), 916 deletions(-) create mode 100644 DESIGN.md diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..2b71ea3 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,185 @@ +# Design System Inspired by Revolut + +## 1. Visual Theme & Atmosphere + +Revolut's website is fintech confidence distilled into pixels — a design system that communicates "your money is in capable hands" through massive typography, generous whitespace, and a disciplined neutral palette. The visual language is built on Aeonik Pro, a geometric grotesque that creates billboard-scale headlines at 136px with weight 500 and aggressive negative tracking (-2.72px). This isn't subtle branding; it's fintech at stadium scale. + +The color system is built on a comprehensive `--rui-*` (Revolut UI) token architecture with semantic naming for every state: danger (`#e23b4a`), warning (`#ec7e00`), teal (`#00a87e`), blue (`#494fdf`), deep-pink (`#e61e49`), and more. But the marketing surface itself is remarkably restrained — near-black (`#191c1f`) and pure white (`#ffffff`) dominate, with the colorful semantic tokens reserved for the product interface, not the marketing page. + +What distinguishes Revolut is its pill-everything button system. Every button uses 9999px radius — primary dark (`#191c1f`), secondary light (`#f4f4f4`), outlined (`transparent + 2px solid`), and ghost on dark (`rgba(244,244,244,0.1) + 2px solid`). The padding is generous (14px 32px–34px), creating large, confident touch targets. Combined with Inter for body text at various weights and positive letter-spacing (0.16px–0.24px), the result is a design that feels both premium and accessible — banking for the modern era. + +**Key Characteristics:** +- Aeonik Pro display at 136px weight 500 — billboard-scale fintech headlines +- Near-black (`#191c1f`) + white binary with comprehensive `--rui-*` semantic tokens +- Universal pill buttons (9999px radius) with generous padding (14px 32px) +- Inter for body text with positive letter-spacing (0.16px–0.24px) +- Rich semantic color system: blue, teal, pink, yellow, green, brown, danger, warning +- Zero shadows detected — depth through color contrast only +- Tight display line-heights (1.00) with relaxed body (1.50–1.56) + +## 2. Color Palette & Roles + +### Primary +- **Revolut Dark** (`#191c1f`): Primary dark surface, button background, near-black text +- **Pure White** (`#ffffff`): `--rui-color-action-label`, primary light surface +- **Light Surface** (`#f4f4f4`): Secondary button background, subtle surface + +### Brand / Interactive +- **Revolut Blue** (`#494fdf`): `--rui-color-blue`, primary brand blue +- **Action Blue** (`#4f55f1`): `--rui-color-action-photo-header-text`, header accent +- **Blue Text** (`#376cd5`): `--website-color-blue-text`, link blue + +### Semantic +- **Danger Red** (`#e23b4a`): `--rui-color-danger`, error/destructive +- **Deep Pink** (`#e61e49`): `--rui-color-deep-pink`, critical accent +- **Warning Orange** (`#ec7e00`): `--rui-color-warning`, warning states +- **Yellow** (`#b09000`): `--rui-color-yellow`, attention +- **Teal** (`#00a87e`): `--rui-color-teal`, success/positive +- **Light Green** (`#428619`): `--rui-color-light-green`, secondary success +- **Green Text** (`#006400`): `--website-color-green-text`, green text +- **Light Blue** (`#007bc2`): `--rui-color-light-blue`, informational +- **Brown** (`#936d62`): `--rui-color-brown`, warm neutral accent +- **Red Text** (`#8b0000`): `--website-color-red-text`, dark red text + +### Neutral Scale +- **Mid Slate** (`#505a63`): Secondary text +- **Cool Gray** (`#8d969e`): Muted text, tertiary +- **Gray Tone** (`#c9c9cd`): `--rui-color-grey-tone-20`, borders/dividers + +## 3. Typography Rules + +### Font Families +- **Display**: `Aeonik Pro` — geometric grotesque, no detected fallbacks +- **Body / UI**: `Inter` — standard system sans +- **Fallback**: `Arial` for specific button contexts + +### Hierarchy + +| Role | Font | Size | Weight | Line Height | Letter Spacing | Notes | +|------|------|------|--------|-------------|----------------|-------| +| Display Mega | Aeonik Pro | 136px (8.50rem) | 500 | 1.00 (tight) | -2.72px | Stadium-scale hero | +| Display Hero | Aeonik Pro | 80px (5.00rem) | 500 | 1.00 (tight) | -0.8px | Primary hero | +| Section Heading | Aeonik Pro | 48px (3.00rem) | 500 | 1.21 (tight) | -0.48px | Feature sections | +| Sub-heading | Aeonik Pro | 40px (2.50rem) | 500 | 1.20 (tight) | -0.4px | Sub-sections | +| Card Title | Aeonik Pro | 32px (2.00rem) | 500 | 1.19 (tight) | -0.32px | Card headings | +| Feature Title | Aeonik Pro | 24px (1.50rem) | 400 | 1.33 | normal | Light headings | +| Nav / UI | Aeonik Pro | 20px (1.25rem) | 500 | 1.40 | normal | Navigation, buttons | +| Body Large | Inter | 18px (1.13rem) | 400 | 1.56 | -0.09px | Introductions | +| Body | Inter | 16px (1.00rem) | 400 | 1.50 | 0.24px | Standard reading | +| Body Semibold | Inter | 16px (1.00rem) | 600 | 1.50 | 0.16px | Emphasized body | +| Body Bold Link | Inter | 16px (1.00rem) | 700 | 1.50 | 0.24px | Bold links | + +### Principles +- **Weight 500 as display default**: Aeonik Pro uses medium (500) for ALL headings — no bold. This creates authority through size and tracking, not weight. +- **Billboard tracking**: -2.72px at 136px is extremely compressed — text designed to be read at a glance, like airport signage. +- **Positive tracking on body**: Inter uses +0.16px to +0.24px, creating airy, well-spaced reading text that contrasts with the compressed headings. + +## 4. Component Stylings + +### Buttons + +**Primary Dark Pill** +- Background: `#191c1f` +- Text: `#ffffff` +- Padding: 14px 32px +- Radius: 9999px (full pill) +- Hover: opacity 0.85 +- Focus: `0 0 0 0.125rem` ring + +**Secondary Light Pill** +- Background: `#f4f4f4` +- Text: `#000000` +- Padding: 14px 34px +- Radius: 9999px +- Hover: opacity 0.85 + +**Outlined Pill** +- Background: transparent +- Text: `#191c1f` +- Border: `2px solid #191c1f` +- Padding: 14px 32px +- Radius: 9999px + +**Ghost on Dark** +- Background: `rgba(244, 244, 244, 0.1)` +- Text: `#f4f4f4` +- Border: `2px solid #f4f4f4` +- Padding: 14px 32px +- Radius: 9999px + +### Cards & Containers +- Radius: 12px (small), 20px (cards) +- No shadows — flat surfaces with color contrast +- Dark and light section alternation + +### Navigation +- Aeonik Pro 20px weight 500 +- Clean header, hamburger toggle at 12px radius +- Pill CTAs right-aligned + +## 5. Layout Principles + +### Spacing System +- Base unit: 8px +- Scale: 4px, 6px, 8px, 14px, 16px, 20px, 24px, 32px, 40px, 48px, 80px, 88px, 120px +- Large section spacing: 80px–120px + +### Border Radius Scale +- Standard (12px): Navigation, small buttons +- Card (20px): Feature cards +- Pill (9999px): All buttons + +## 6. Depth & Elevation + +| Level | Treatment | Use | +|-------|-----------|-----| +| Flat (Level 0) | No shadow | Everything — Revolut uses zero shadows | +| Focus | `0 0 0 0.125rem` ring | Accessibility focus | + +**Shadow Philosophy**: Revolut uses ZERO shadows. Depth comes entirely from the dark/light section contrast and the generous whitespace between elements. + +## 7. Do's and Don'ts + +### Do +- Use Aeonik Pro weight 500 for all display headings +- Apply 9999px radius to all buttons — pill shape is universal +- Use generous button padding (14px 32px) +- Keep the palette to near-black + white for marketing surfaces +- Apply positive letter-spacing on Inter body text + +### Don't +- Don't use shadows — Revolut is flat by design +- Don't use bold (700) for Aeonik Pro headings — 500 is the weight +- Don't use small buttons — the generous padding is intentional +- Don't apply semantic colors to marketing surfaces — they're for the product + +## 8. Responsive Behavior + +### Breakpoints +| Name | Width | Key Changes | +|------|-------|-------------| +| Mobile Small | <400px | Compact, single column | +| Mobile | 400–720px | Standard mobile | +| Tablet | 720–1024px | 2-column layouts | +| Desktop | 1024–1280px | Standard desktop | +| Large | 1280–1920px | Full layout | + +## 9. Agent Prompt Guide + +### Quick Color Reference +- Dark: Revolut Dark (`#191c1f`) +- Light: White (`#ffffff`) +- Surface: Light (`#f4f4f4`) +- Blue: Revolut Blue (`#494fdf`) +- Danger: Red (`#e23b4a`) +- Success: Teal (`#00a87e`) + +### Example Component Prompts +- "Create a hero: white background. Headline at 136px Aeonik Pro weight 500, line-height 1.00, letter-spacing -2.72px, #191c1f text. Dark pill CTA (#191c1f, 9999px, 14px 32px). Outlined pill secondary (transparent, 2px solid #191c1f)." +- "Build a pill button: #191c1f background, white text, 9999px radius, 14px 32px padding, 20px Aeonik Pro weight 500. Hover: opacity 0.85." + +### Iteration Guide +1. Aeonik Pro 500 for headings — never bold +2. All buttons are pills (9999px) with generous padding +3. Zero shadows — flat is the Revolut identity +4. Near-black + white for marketing, semantic colors for product diff --git a/app/static/futures_analysis.css b/app/static/futures_analysis.css index f256196..ebe543c 100644 --- a/app/static/futures_analysis.css +++ b/app/static/futures_analysis.css @@ -1,23 +1,30 @@ +/* ============================================ + 期货智析 - 科技感界面样式 + 风格:低调鲜明、赛博朋克科技感 + ============================================ */ + :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; + --bg-primary: #06080d; + --bg-secondary: #0c1017; + --bg-card: rgba(15, 20, 30, 0.7); + --bg-card-hover: rgba(20, 28, 42, 0.8); + --border-color: rgba(56, 189, 248, 0.1); + --border-glow: rgba(56, 189, 248, 0.3); + + --text-primary: #e2e8f0; + --text-secondary: #94a3b8; + --text-muted: #64748b; + + --cyan: #06b6d4; + --cyan-glow: rgba(6, 182, 212, 0.4); --purple: #8b5cf6; - --accent: #22c55e; + --purple-glow: rgba(139, 92, 246, 0.4); + --green: #10b981; + --green-glow: rgba(16, 185, 129, 0.4); + --red: #ef4444; + --red-glow: rgba(239, 68, 68, 0.4); + --amber: #f59e0b; + --amber-glow: rgba(245, 158, 11, 0.4); } * { @@ -27,25 +34,59 @@ } body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif; background: var(--bg-primary); color: var(--text-primary); line-height: 1.5; + overflow-x: hidden; +} + +/* 背景网格和光效 */ +.bg-grid { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-image: + linear-gradient(rgba(56, 189, 248, 0.03) 1px, transparent 1px), + linear-gradient(90deg, rgba(56, 189, 248, 0.03) 1px, transparent 1px); + background-size: 40px 40px; + pointer-events: none; + z-index: 0; +} + +.bg-glow { + position: fixed; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: radial-gradient(ellipse at 30% 20%, rgba(6, 182, 212, 0.08) 0%, transparent 50%), + radial-gradient(ellipse at 70% 80%, rgba(139, 92, 246, 0.06) 0%, transparent 50%); + pointer-events: none; + z-index: 0; } .app-container { + position: relative; + z-index: 1; 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); + padding: 0 24px; + height: 56px; + background: rgba(6, 8, 13, 0.8); + backdrop-filter: blur(12px); border-bottom: 1px solid var(--border-color); position: sticky; top: 0; @@ -64,16 +105,31 @@ body { } .logo-icon { + position: relative; 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; + background: linear-gradient(135deg, rgba(6, 182, 212, 0.2), rgba(139, 92, 246, 0.2)); + border: 1px solid var(--border-glow); + border-radius: 10px; + color: var(--cyan); + font-size: 16px; +} + +.logo-pulse { + position: absolute; + inset: -2px; + border-radius: 12px; + background: linear-gradient(135deg, var(--cyan), var(--purple)); + opacity: 0; + animation: pulse 3s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 0; transform: scale(1); } + 50% { opacity: 0.3; transform: scale(1.05); } } .logo-text { @@ -82,51 +138,51 @@ body { } .logo-title { - font-size: 16px; + font-size: 15px; font-weight: 600; - color: var(--text-primary); + background: linear-gradient(135deg, var(--text-primary), var(--cyan)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; } .logo-subtitle { - font-size: 11px; - color: var(--text-secondary); + font-size: 9px; + color: var(--text-muted); + letter-spacing: 1.5px; + text-transform: uppercase; } .nav-center { display: flex; - gap: 8px; + gap: 4px; } .nav-item { + display: flex; + align-items: center; + gap: 8px; padding: 8px 16px; color: var(--text-secondary); text-decoration: none; - font-size: 14px; + font-size: 13px; font-weight: 500; - border-radius: 6px; + border-radius: 8px; transition: all 0.2s; - position: relative; } .nav-item:hover { color: var(--text-primary); - background: var(--bg-card); + background: rgba(56, 189, 248, 0.08); } .nav-item.active { - color: var(--green); + color: var(--cyan); + background: rgba(6, 182, 212, 0.1); } -.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-icon { + font-size: 14px; } .nav-right { @@ -135,56 +191,66 @@ body { gap: 16px; } -.datetime { +.system-status { display: flex; align-items: center; gap: 6px; - color: var(--text-secondary); - font-size: 13px; + padding: 4px 10px; + background: rgba(16, 185, 129, 0.1); + border: 1px solid rgba(16, 185, 129, 0.2); + border-radius: 20px; } -.nav-icon-btn { - color: var(--text-secondary); - text-decoration: none; - font-size: 16px; - padding: 6px; - border-radius: 6px; - transition: all 0.2s; +.status-dot { + width: 6px; + height: 6px; + background: var(--green); + border-radius: 50%; + animation: blink 2s ease-in-out infinite; } -.nav-icon-btn:hover { - color: var(--text-primary); - background: var(--bg-card); +@keyframes blink { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.4; } } -.notification { - position: relative; - color: var(--text-secondary); - font-size: 16px; - cursor: pointer; - padding: 6px; +.status-text { + font-size: 10px; + font-weight: 600; + color: var(--green); + letter-spacing: 1px; } -.notification .badge { - position: absolute; - top: 0; - right: 0; - width: 16px; - height: 16px; - background: var(--red); - border-radius: 50%; - font-size: 10px; +.datetime { + font-size: 12px; + color: var(--text-muted); + font-variant-numeric: tabular-nums; +} + +.nav-btn { + width: 32px; + height: 32px; display: flex; align-items: center; justify-content: center; - color: white; + color: var(--text-secondary); + text-decoration: none; + border-radius: 8px; + transition: all 0.2s; } -/* 主内容区 */ +.nav-btn:hover { + color: var(--cyan); + background: rgba(6, 182, 212, 0.1); +} + +/* ============================================ + 主内容区 + ============================================ */ .main-content { flex: 1; padding: 20px 24px; - max-width: 1400px; + max-width: 1440px; margin: 0 auto; width: 100%; } @@ -197,24 +263,31 @@ body { display: block; } -/* 工具栏 */ -.toolbar { +/* ============================================ + 搜索栏 + ============================================ */ +.search-section { display: flex; - justify-content: space-between; - align-items: center; + gap: 12px; margin-bottom: 16px; } .search-box { flex: 1; - max-width: 600px; + max-width: 480px; display: flex; align-items: center; + gap: 10px; + padding: 10px 14px; background: var(--bg-card); border: 1px solid var(--border-color); - border-radius: 8px; - padding: 10px 14px; - gap: 10px; + border-radius: 10px; + transition: all 0.2s; +} + +.search-box:focus-within { + border-color: var(--cyan); + box-shadow: 0 0 0 3px rgba(6, 182, 212, 0.1); } .search-box i { @@ -234,155 +307,303 @@ body { color: var(--text-muted); } -.view-toggle { +.search-box kbd { + padding: 2px 6px; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 4px; + font-size: 11px; + color: var(--text-muted); +} + +.view-controls { display: flex; gap: 4px; } -.toggle-btn { - width: 36px; - height: 36px; +.view-btn { + width: auto; + height: auto; display: flex; align-items: center; justify-content: center; + gap: 6px; + padding: 8px 14px; background: var(--bg-card); border: 1px solid var(--border-color); - border-radius: 6px; - color: var(--text-secondary); + border-radius: 8px; + color: var(--text-muted); cursor: pointer; transition: all 0.2s; + font-size: 13px; } -.toggle-btn.active { - background: var(--green); - border-color: var(--green); - color: white; +.view-btn i { + font-size: 14px; +} + +.view-btn span { + font-size: 13px; + font-weight: 500; +} + +.view-btn:hover { + color: var(--text-secondary); +} + +.view-btn.active { + background: rgba(6, 182, 212, 0.15); + border-color: var(--cyan); + color: var(--cyan); } -/* 筛选栏 */ +/* ============================================ + 筛选栏 + ============================================ */ .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 { +.filter-tabs { display: flex; - align-items: center; - gap: 8px; -} - -.filter-label { - color: var(--text-secondary); - font-size: 13px; + gap: 6px; } -.filter-btn { - padding: 6px 12px; - background: var(--bg-secondary); +.filter-tab { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 14px; + background: var(--bg-card); border: 1px solid var(--border-color); - border-radius: 6px; + border-radius: 8px; color: var(--text-secondary); font-size: 13px; cursor: pointer; transition: all 0.2s; } -.filter-btn:hover { +.filter-tab:hover { border-color: var(--text-muted); color: var(--text-primary); } -.filter-btn.active { - background: var(--green); - border-color: var(--green); - color: white; +.filter-tab.active { + background: rgba(6, 182, 212, 0.12); + border-color: var(--cyan); + color: var(--cyan); +} + +.filter-tab i { + font-size: 12px; +} + +.filter-count { + padding: 1px 6px; + background: rgba(255, 255, 255, 0.1); + border-radius: 10px; + font-size: 11px; } .sort-select { - padding: 6px 12px; - background: var(--bg-secondary); + position: relative; +} + +.sort-select select { + padding: 8px 32px 8px 12px; + background: var(--bg-card); border: 1px solid var(--border-color); - border-radius: 6px; + border-radius: 8px; color: var(--text-primary); font-size: 13px; - cursor: pointer; + font-weight: 500; outline: none; + cursor: pointer; + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%2394a3b8' d='M6 8L1 3h10z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 10px center; + background-size: 12px; + transition: all 0.2s; } -/* 统计栏 */ -.stats-bar { - display: flex; - gap: 20px; - margin-bottom: 20px; - font-size: 14px; - color: var(--text-secondary); +.sort-select select:hover { + border-color: var(--text-muted); +} + +.sort-select select:focus { + border-color: var(--cyan); + box-shadow: 0 0 0 3px rgba(6, 182, 212, 0.1); } -.stats-bar strong { +.sort-select select option { + background: var(--bg-card); color: var(--text-primary); + padding: 8px; +} + +/* 下拉菜单展开样式 */ +.sort-select select:focus option { + background: var(--bg-card); +} + +.sort-select select option:hover, +.sort-select select option:checked { + background: rgba(6, 182, 212, 0.1) !important; +} + +/* ============================================ + 统计概览 + ============================================ */ +.stats-overview { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 12px; + margin-bottom: 20px; +} + +.stat-card { + display: flex; + align-items: center; + gap: 14px; + padding: 14px 18px; + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: 12px; + transition: all 0.2s; } -.stat-up { +.stat-card:hover { + border-color: var(--border-glow); + transform: translateY(-2px); +} + +.stat-icon { + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + background: rgba(56, 189, 248, 0.1); + border-radius: 10px; + color: var(--cyan); + font-size: 16px; +} + +.stat-card.up .stat-icon { + background: rgba(16, 185, 129, 0.1); color: var(--green); } -.stat-down { +.stat-card.down .stat-icon { + background: rgba(239, 68, 68, 0.1); color: var(--red); } -/* 品种卡片网格 */ +.stat-card.neutral .stat-icon { + background: rgba(245, 158, 11, 0.1); + color: var(--amber); +} + +.stat-info { + display: flex; + flex-direction: column; +} + +.stat-value { + font-size: 22px; + font-weight: 700; + font-variant-numeric: tabular-nums; +} + +.stat-label { + font-size: 12px; + color: var(--text-muted); +} + +/* ============================================ + 品种卡片网格 + ============================================ */ .futures-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); - gap: 16px; + grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); + gap: 14px; } .futures-card { background: var(--bg-card); border: 1px solid var(--border-color); - border-radius: 12px; + border-radius: 14px; padding: 18px; cursor: pointer; - transition: all 0.2s; + transition: all 0.25s; + position: relative; + overflow: hidden; +} + +.futures-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, transparent, var(--cyan), transparent); + opacity: 0; + transition: opacity 0.25s; } .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); + border-color: var(--border-glow); + transform: translateY(-3px); + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4); +} + +.futures-card:hover::before { + opacity: 1; } -.card-header { +.card-top { display: flex; justify-content: space-between; align-items: flex-start; - margin-bottom: 12px; + margin-bottom: 14px; } -.card-title { +.card-symbol { display: flex; align-items: center; - gap: 8px; + gap: 10px; +} + +.symbol-tag { + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, rgba(6, 182, 212, 0.15), rgba(139, 92, 246, 0.15)); + border: 1px solid var(--border-glow); + border-radius: 10px; + font-size: 11px; + font-weight: 600; + color: var(--cyan); } .card-name { - font-size: 16px; + font-size: 15px; font-weight: 600; } .card-code { - font-size: 12px; + font-size: 11px; color: var(--text-muted); - background: var(--bg-secondary); - padding: 2px 6px; - border-radius: 4px; } .card-price { @@ -391,178 +612,161 @@ body { .price-value { font-size: 20px; - font-weight: 600; + font-weight: 700; + font-variant-numeric: tabular-nums; } .price-change { - font-size: 13px; + font-size: 12px; + font-weight: 500; display: flex; align-items: center; - gap: 4px; + gap: 3px; justify-content: flex-end; } -.up { - color: var(--green); -} - -.down { - color: var(--red); -} +.up { color: var(--green); } +.down { color: var(--red); } +.neutral { color: var(--amber); } .suggestion-badge { display: inline-block; padding: 4px 10px; - border-radius: 4px; + border-radius: 6px; font-size: 12px; font-weight: 500; margin-bottom: 12px; } .suggestion-badge.up { - background: var(--green-bg); + background: rgba(16, 185, 129, 0.12); color: var(--green); - border: 1px solid var(--green-border); + border: 1px solid rgba(16, 185, 129, 0.25); } .suggestion-badge.down { - background: var(--red-bg); + background: rgba(239, 68, 68, 0.12); color: var(--red); - border: 1px solid var(--red-border); + border: 1px solid rgba(239, 68, 68, 0.25); } .suggestion-badge.neutral { - background: rgba(107, 114, 128, 0.15); - color: var(--text-muted); - border: 1px solid rgba(107, 114, 128, 0.3); + background: rgba(245, 158, 11, 0.1); + color: var(--amber); + border: 1px solid rgba(245, 158, 11, 0.2); } -.card-section { +.card-metrics { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; 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; +.metric-item { display: flex; - align-items: center; + flex-direction: column; 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); +.metric-label { + font-size: 11px; color: var(--text-muted); - border: 1px solid rgba(107, 114, 128, 0.2); } -.progress-bar { - height: 6px; - background: var(--bg-secondary); - border-radius: 3px; +.metric-bar { + height: 4px; + background: rgba(255, 255, 255, 0.06); + border-radius: 2px; overflow: hidden; - margin-bottom: 4px; } -.progress-fill { +.metric-fill { height: 100%; - border-radius: 3px; + border-radius: 2px; transition: width 0.3s; } -.progress-fill.up { - background: var(--green); -} - -.progress-fill.down { - background: var(--red); -} +.metric-fill.up { background: var(--green); } +.metric-fill.down { background: var(--red); } +.metric-fill.orange { background: var(--amber); } -.progress-fill.orange { - background: var(--orange); +.metric-value { + font-size: 13px; + font-weight: 600; + font-variant-numeric: tabular-nums; } -.progress-info { +.period-trends { display: flex; - justify-content: space-between; - font-size: 12px; -} - -.progress-label { - color: var(--text-secondary); + gap: 6px; + margin-bottom: 12px; } -.progress-value { - font-weight: 500; +.period-tag { + flex: 1; + padding: 5px 8px; + background: rgba(255, 255, 255, 0.04); + border: 1px solid var(--border-color); + border-radius: 6px; + font-size: 11px; + text-align: center; + transition: all 0.2s; } -.key-levels-row { - display: flex; - justify-content: space-between; - font-size: 12px; +.period-tag.up { + background: rgba(16, 185, 129, 0.1); + border-color: rgba(16, 185, 129, 0.25); + color: var(--green); } -.level-label { - color: var(--text-secondary); +.period-tag.down { + background: rgba(239, 68, 68, 0.1); + border-color: rgba(239, 68, 68, 0.25); + color: var(--red); } -.level-value { - font-weight: 500; +.period-tag.neutral { + background: rgba(245, 158, 11, 0.08); + border-color: rgba(245, 158, 11, 0.2); + color: var(--amber); } .card-footer { display: flex; - justify-content: flex-end; + justify-content: space-between; + align-items: center; padding-top: 12px; border-top: 1px solid var(--border-color); } -.detail-link { - color: var(--text-secondary); +.key-levels { + display: flex; + gap: 12px; font-size: 12px; - text-decoration: none; +} + +.key-levels span { display: flex; align-items: center; gap: 4px; - transition: color 0.2s; } -.detail-link:hover { - color: var(--green); +.key-levels .label { + color: var(--text-muted); } -/* 详情视图 */ -.detail-header { - margin-bottom: 20px; +.detail-link { + font-size: 12px; + color: var(--cyan); + display: flex; + align-items: center; + gap: 4px; } +/* ============================================ + 详情视图 + ============================================ */ .back-btn { display: inline-flex; align-items: center; @@ -572,7 +776,7 @@ body { border: 1px solid var(--border-color); border-radius: 8px; color: var(--text-secondary); - font-size: 14px; + font-size: 13px; cursor: pointer; margin-bottom: 16px; transition: all 0.2s; @@ -581,35 +785,64 @@ body { .back-btn:hover { background: var(--bg-card-hover); color: var(--text-primary); + border-color: var(--border-glow); } -.detail-title-bar { +.detail-header { display: flex; - align-items: center; justify-content: space-between; + align-items: center; + padding: 20px 24px; background: var(--bg-card); border: 1px solid var(--border-color); - border-radius: 12px; - padding: 16px 20px; + border-radius: 14px; + margin-bottom: 16px; } -.price-info { +.header-left { display: flex; - flex-direction: column; + align-items: center; + gap: 24px; +} + +.symbol-info { + display: flex; + align-items: baseline; + gap: 10px; +} + +.symbol-name { + font-size: 22px; + font-weight: 700; +} + +.symbol-code { + font-size: 13px; + color: var(--text-muted); + padding: 2px 8px; + background: rgba(255, 255, 255, 0.05); + border-radius: 4px; +} + +.price-main { + display: flex; + flex-direction: row; + align-items: baseline; + gap: 12px; } -.current-price { +.price-value { font-size: 28px; font-weight: 700; - color: var(--green); + font-variant-numeric: tabular-nums; } .price-change { font-size: 14px; - margin-top: 4px; + font-weight: 500; } -.quote-info { +.quote-grid { display: flex; gap: 24px; } @@ -618,44 +851,51 @@ body { display: flex; flex-direction: column; align-items: center; + gap: 4px; } .quote-label { - font-size: 12px; + font-size: 11px; color: var(--text-muted); - margin-bottom: 4px; + text-transform: uppercase; + letter-spacing: 0.5px; } .quote-value { font-size: 14px; - font-weight: 500; + font-weight: 600; + font-variant-numeric: tabular-nums; } /* 周期选择 */ -.period-selector { +.period-bar { display: flex; align-items: center; - gap: 8px; + gap: 12px; margin-bottom: 16px; } -.period-selector i { - color: var(--green); -} - .period-label { - color: var(--text-secondary); font-size: 13px; - margin-right: 8px; + color: var(--text-muted); + display: flex; + align-items: center; + gap: 6px; +} + +.period-btns { + display: flex; + gap: 4px; } .period-btn { - padding: 8px 16px; + padding: 6px 16px; background: var(--bg-card); border: 1px solid var(--border-color); border-radius: 8px; color: var(--text-secondary); font-size: 13px; + font-weight: 500; cursor: pointer; transition: all 0.2s; } @@ -665,122 +905,147 @@ body { } .period-btn.active { - background: var(--green); - border-color: var(--green); - color: white; + background: rgba(6, 182, 212, 0.15); + border-color: var(--cyan); + color: var(--cyan); } /* 详情主体 */ .detail-body { display: grid; - grid-template-columns: 1fr 340px; - gap: 20px; + grid-template-columns: 1fr 360px; + gap: 16px; } -.chart-section { +.chart-container { background: var(--bg-card); border: 1px solid var(--border-color); - border-radius: 12px; + border-radius: 14px; padding: 16px; } +.chart-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; +} + +.chart-title { + font-size: 14px; + font-weight: 600; + color: var(--text-primary); +} + +.chart-legend { + display: flex; + gap: 14px; +} + +.legend-item { + display: flex; + align-items: center; + gap: 6px; + font-size: 11px; + color: var(--text-muted); +} + +.legend-dot { + width: 10px; + height: 3px; + border-radius: 2px; +} + +.legend-dot.ma5 { background: #f59e0b; } +.legend-dot.ma10 { background: #3b82f6; } +.legend-dot.ma20 { background: #8b5cf6; } + .kline-chart { width: 100%; - height: 500px; + height: 480px; } -/* 分析面板 */ -.analysis-panel { +/* 分析侧边栏 */ +.analysis-sidebar { display: flex; flex-direction: column; - gap: 16px; + gap: 14px; } .panel-card { background: var(--bg-card); border: 1px solid var(--border-color); - border-radius: 12px; + border-radius: 14px; padding: 16px; + transition: all 0.2s; +} + +.panel-card:hover { + border-color: var(--border-glow); } -.panel-title { +.panel-header { display: flex; align-items: center; gap: 8px; - font-size: 14px; + font-size: 13px; font-weight: 600; margin-bottom: 14px; color: var(--text-primary); } -.panel-title i { - color: var(--green); +.panel-header i { + color: var(--cyan); + font-size: 14px; } -/* 交易建议 */ -.suggestion-box { - padding: 14px; - border-radius: 8px; - margin-bottom: 14px; +/* AI建议卡片 */ +.suggestion-card .suggestion-content { text-align: center; + padding: 16px; + background: rgba(6, 182, 212, 0.06); + border: 1px solid rgba(6, 182, 212, 0.15); + border-radius: 10px; + margin-bottom: 14px; } -.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 { +.suggestion-badge { font-size: 18px; - font-weight: 600; + font-weight: 700; margin-bottom: 6px; } -.suggestion-box.up .suggestion-action { - color: var(--green); -} - -.suggestion-box.down .suggestion-action { - color: var(--red); -} +.suggestion-badge.up { color: var(--green); } +.suggestion-badge.down { color: var(--red); } +.suggestion-badge.neutral { color: var(--amber); } .suggestion-reason { font-size: 12px; color: var(--text-secondary); } -.suggestion-details { +.trade-params { display: grid; grid-template-columns: 1fr 1fr; - gap: 10px; + gap: 8px; } -.detail-row { +.param-row { display: flex; justify-content: space-between; padding: 8px 10px; - background: var(--bg-secondary); + background: rgba(255, 255, 255, 0.03); border-radius: 6px; font-size: 12px; } -.detail-label { +.param-label { color: var(--text-muted); } -.detail-value { - font-weight: 500; +.param-value { + font-weight: 600; + font-variant-numeric: tabular-nums; } /* 技术指标 */ @@ -790,22 +1055,23 @@ body { gap: 10px; } -.indicator-item { +.indicator-cell { padding: 12px; - background: var(--bg-secondary); + background: rgba(255, 255, 255, 0.03); border-radius: 8px; + display: flex; + flex-direction: column; + gap: 4px; } -.indicator-name { - font-size: 12px; +.indicator-label { + font-size: 11px; color: var(--text-muted); - margin-bottom: 6px; } .indicator-value { font-size: 14px; font-weight: 600; - margin-bottom: 4px; } .indicator-detail { @@ -814,125 +1080,1236 @@ body { } /* 关键点位 */ -.levels-section { - margin-bottom: 12px; -} - -.levels-section:last-child { - margin-bottom: 0; +.levels-container { + display: flex; + flex-direction: column; + gap: 8px; } -.levels-header { - font-size: 12px; +.level-group-label { + font-size: 11px; font-weight: 600; - margin-bottom: 8px; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 6px; + display: block; } -.levels-header.resistance { - color: var(--red); +.level-group.resistance .level-group-label { color: var(--red); } +.level-group.support .level-group-label { color: var(--green); } + +.level-item { + display: flex; + justify-content: space-between; + padding: 6px 10px; + background: rgba(255, 255, 255, 0.03); + border-radius: 6px; + font-size: 12px; } -.levels-header.support { - color: var(--green); +.level-item span:first-child { + color: var(--text-muted); + font-weight: 500; } -.level-row { - display: flex; - justify-content: space-between; - padding: 8px 0; - border-bottom: 1px solid var(--border-color); - font-size: 13px; +.level-item span:last-child { + font-weight: 600; + font-variant-numeric: tabular-nums; } -.level-row:last-child { - border-bottom: none; +.level-divider { + height: 1px; + background: var(--border-color); + margin: 4px 0; } -.level-row span:first-child { - color: var(--text-secondary); +/* 多周期趋势 */ +.trends-container { + display: flex; + flex-direction: column; + gap: 6px; } -/* 多周期一致性 */ -.consistency-row { +.trend-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; + padding: 8px 12px; + background: rgba(255, 255, 255, 0.03); + border-radius: 6px; } -.period-name { - font-size: 13px; +.trend-period { + font-size: 12px; color: var(--text-secondary); } -.consistency-badge { - padding: 4px 10px; +.trend-badge { + padding: 3px 10px; border-radius: 4px; - font-size: 12px; - display: flex; - align-items: center; - gap: 4px; + font-size: 11px; + font-weight: 500; } -.consistency-badge.up { - background: var(--green-bg); +.trend-badge.up { + background: rgba(16, 185, 129, 0.12); color: var(--green); } -.consistency-badge.down { - background: var(--red-bg); +.trend-badge.down { + background: rgba(239, 68, 68, 0.12); color: var(--red); } -.consistency-badge.neutral { - background: rgba(107, 114, 128, 0.15); +.trend-badge.neutral { + background: rgba(245, 158, 11, 0.1); + color: var(--amber); +} + +/* 趋势评分 */ +.score-display { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; +} + +.score-ring { + position: relative; + width: 100px; + height: 100px; +} + +.score-ring svg { + transform: rotate(-90deg); +} + +.score-bg { + fill: none; + stroke: rgba(255, 255, 255, 0.06); + stroke-width: 8; +} + +.score-fill { + fill: none; + stroke: url(#scoreGradient); + stroke-width: 8; + stroke-linecap: round; + stroke-dasharray: 283; + stroke-dashoffset: 283; + transition: stroke-dashoffset 0.8s ease-out; +} + +.score-value { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 24px; + font-weight: 700; + font-variant-numeric: tabular-nums; +} + +.score-label { + font-size: 12px; color: var(--text-muted); } -/* 响应式 */ +/* ============================================ + 响应式 + ============================================ */ @media (max-width: 1200px) { .detail-body { grid-template-columns: 1fr; } - .analysis-panel { + .analysis-sidebar { 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; + .stats-overview { + grid-template-columns: repeat(2, 1fr); } .futures-grid { grid-template-columns: 1fr; } - .analysis-panel { + .analysis-sidebar { grid-template-columns: 1fr; } - .detail-title-bar { + .detail-header { flex-direction: column; gap: 16px; } - .quote-info { + .quote-grid { width: 100%; justify-content: space-between; } + + .filter-bar { + flex-direction: column; + gap: 12px; + } +} + +/* ============================================ + 简洁风格主题 - Revolut 设计系统 + ============================================ */ +body.theme-minimal { + --bg-primary: #ffffff; + --bg-secondary: #f4f4f4; + --bg-card: #ffffff; + --bg-card-hover: #f4f4f4; + --border-color: #c9c9cd; + --border-glow: #c9c9cd; + + --text-primary: #191c1f; + --text-secondary: #505a63; + --text-muted: #8d969e; + + --cyan: #494fdf; + --cyan-glow: rgba(73, 79, 223, 0.2); + --purple: #494fdf; + --purple-glow: rgba(73, 79, 223, 0.2); + --green: #00a87e; + --green-glow: rgba(0, 168, 126, 0.2); + --red: #e23b4a; + --red-glow: rgba(226, 59, 74, 0.2); + --amber: #ec7e00; + --amber-glow: rgba(236, 126, 0, 0.2); +} + +body.theme-minimal { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif; + letter-spacing: 0.16px; +} + +body.theme-minimal .bg-grid, +body.theme-minimal .bg-glow { + display: none; +} + +body.theme-minimal .top-nav { + background: var(--bg-primary); + border-bottom-color: var(--border-color); +} + +body.theme-minimal .logo-icon { + background: var(--bg-secondary); + border-color: var(--border-color); + border-radius: 12px; + color: var(--text-primary); +} + +body.theme-minimal .logo-pulse { + display: none; +} + +body.theme-minimal .logo-title { + background: none; + -webkit-text-fill-color: var(--text-primary); + font-family: 'Inter', sans-serif; + font-weight: 500; +} + +body.theme-minimal .logo-subtitle { + color: var(--text-muted); + letter-spacing: 0.24px; +} + +body.theme-minimal .nav-item { + font-family: 'Inter', sans-serif; + font-weight: 500; + letter-spacing: 0.16px; + color: var(--text-secondary); +} + +body.theme-minimal .nav-item:hover { + color: var(--text-primary); +} + +body.theme-minimal .nav-item.active { + background: var(--bg-card-hover); + border-radius: 9999px; + color: var(--text-primary); +} + +body.theme-minimal .system-status { + background: var(--bg-secondary); + border-color: var(--border-color); + border-radius: 9999px; +} + +body.theme-minimal .system-status .status-dot { + background: var(--green); +} + +body.theme-minimal .system-status .status-text { + color: var(--green); +} + +body.theme-minimal .search-box { + background: var(--bg-secondary); + border-radius: 9999px; + box-shadow: none; +} + +body.theme-minimal .search-box:focus-within { + box-shadow: 0 0 0 0.125rem rgba(73, 79, 223, 0.2); +} + +body.theme-minimal .search-box i { + color: var(--text-muted); +} + +body.theme-minimal .view-btn, +body.theme-minimal .filter-tab { + background: var(--bg-secondary); + border-radius: 9999px; + box-shadow: none; + padding: 10px 16px; + color: var(--text-secondary); + display: flex; + align-items: center; + gap: 6px; +} + +body.theme-minimal .view-btn:hover, +body.theme-minimal .filter-tab:hover { + color: var(--text-primary); +} + +body.theme-minimal .view-btn i { + font-size: 14px; +} + +body.theme-minimal .view-btn span { + font-size: 13px; + font-weight: 500; +} + +body.theme-minimal .stat-card { + background: var(--bg-secondary); + box-shadow: none; + border-radius: 20px; + padding: 20px 24px; +} + +body.theme-minimal .stat-card:hover { + box-shadow: none; + transform: none; + background: var(--bg-card-hover); +} + +body.theme-minimal .stat-icon { + background: var(--bg-card); + border-radius: 12px; + color: var(--cyan); +} + +body.theme-minimal .stat-card.up .stat-icon { + color: var(--green); +} + +body.theme-minimal .stat-card.down .stat-icon { + color: var(--red); +} + +body.theme-minimal .stat-card.neutral .stat-icon { + color: var(--amber); +} + +body.theme-minimal .futures-card { + background: var(--bg-secondary); + box-shadow: none; + border-radius: 20px; +} + +body.theme-minimal .futures-card::before { + display: none; +} + +body.theme-minimal .futures-card:hover { + box-shadow: none; + transform: none; + background: var(--bg-card-hover); +} + +body.theme-minimal .symbol-tag { + background: var(--bg-card); + border-color: var(--border-color); + border-radius: 12px; + color: var(--cyan); +} + +body.theme-minimal .period-tag { + background: var(--bg-card); + border-radius: 9999px; + padding: 6px 12px; +} + +body.theme-minimal .detail-link { + color: var(--cyan); +} + +body.theme-minimal .detail-header { + background: var(--bg-secondary); + box-shadow: none; + border-radius: 20px; +} + +body.theme-minimal .symbol-code { + background: var(--bg-card); + border-radius: 9999px; + padding: 4px 12px; +} + +body.theme-minimal .period-btn { + background: var(--bg-secondary); + border-radius: 9999px; + padding: 10px 20px; + color: var(--text-secondary); +} + +body.theme-minimal .period-btn:hover { + color: var(--text-primary); +} + +body.theme-minimal .chart-container { + background: var(--bg-secondary); + box-shadow: none; + border-radius: 20px; +} + +body.theme-minimal .panel-card { + background: var(--bg-secondary); + box-shadow: none; + border-radius: 20px; +} + +body.theme-minimal .panel-card:hover { + box-shadow: none; +} + +body.theme-minimal .panel-header i { + color: var(--text-primary); +} + +body.theme-minimal .suggestion-card .suggestion-content { + background: var(--bg-card); + border-color: var(--border-color); + border-radius: 12px; +} + +body.theme-minimal .indicator-cell { + background: var(--bg-card); + border-radius: 12px; +} + +body.theme-minimal .level-item { + background: var(--bg-card); + border-radius: 9999px; +} + +body.theme-minimal .trend-row { + background: var(--bg-card); + border-radius: 9999px; +} + +body.theme-minimal .param-row { + background: var(--bg-card); + border-radius: 9999px; +} + +body.theme-minimal .back-btn { + background: var(--bg-secondary); + box-shadow: none; + border-radius: 9999px; + padding: 14px 32px; + color: var(--text-secondary); +} + +body.theme-minimal .back-btn:hover { + color: var(--text-primary); +} + +body.theme-minimal .datetime { + color: var(--text-muted); +} + +body.theme-minimal .nav-btn { + border-radius: 9999px; + color: var(--text-secondary); +} + +body.theme-minimal .nav-btn:hover { + background: var(--bg-card-hover); + color: var(--text-primary); +} + +body.theme-minimal .sort-select select { + background: var(--bg-secondary); + border-color: var(--border-color); + border-radius: 9999px; + padding: 10px 36px 10px 16px; + font-family: 'Inter', sans-serif; + font-weight: 500; + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23505a63' d='M6 8L1 3h10z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 14px center; + background-size: 12px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} + +body.theme-minimal .sort-select select:hover { + border-color: var(--text-muted); +} + +body.theme-minimal .sort-select select:focus { + border-color: var(--cyan); + box-shadow: 0 0 0 0.125rem rgba(73, 79, 223, 0.2); +} + +body.theme-minimal .sort-select select option { + background: var(--bg-secondary); + color: var(--text-primary); +} + +body.theme-minimal .sort-select select option:hover, +body.theme-minimal .sort-select select option:checked { + background: rgba(73, 79, 223, 0.1) !important; +} + +body.theme-minimal .filter-tab.active { + background: var(--cyan); + color: white; + border-color: var(--cyan); +} + +body.theme-minimal .view-btn.active { + background: var(--cyan); + border-color: var(--cyan); + color: white; +} + +body.theme-minimal .period-btn.active { + background: var(--cyan); + border-color: var(--cyan); + color: white; +} + +body.theme-minimal .suggestion-badge { + border-radius: 9999px; + padding: 6px 16px; +} + +body.theme-minimal .btn, +body.theme-minimal button { + border-radius: 9999px; +} + +/* 主题切换按钮 */ +.theme-toggle { + position: relative; +} + +.theme-toggle i { + transition: transform 0.3s ease; +} + +body.theme-minimal .theme-toggle i::before { + content: '\f185'; +} + +/* ============================================ + 自选按钮 + ============================================ */ +.watch-btn { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + background: none; + border: 1px solid var(--border-color); + border-radius: 8px; + color: var(--text-muted); + cursor: pointer; + transition: all 0.2s; +} + +.watch-btn:hover { + border-color: var(--amber); + color: var(--amber); +} + +.watch-btn.active { + background: rgba(245, 158, 11, 0.1); + border-color: var(--amber); + color: var(--amber); +} + +body.theme-minimal .watch-btn { + border-radius: 9999px; + background: var(--bg-card); +} + +body.theme-minimal .watch-btn:hover { + background: rgba(236, 126, 0, 0.1); +} + +body.theme-minimal .watch-btn.active { + background: rgba(236, 126, 0, 0.15); +} + +/* ============================================ + 价格关键位标签 + ============================================ */ +.price-levels { + display: flex; + gap: 8px; + margin-top: 8px; +} + +.level-tag { + padding: 4px 10px; + border-radius: 6px; + font-size: 11px; + font-weight: 600; + font-variant-numeric: tabular-nums; +} + +.level-tag.resistance { + background: rgba(226, 59, 74, 0.1); + color: #e23b4a; +} + +.level-tag.support { + background: rgba(0, 168, 126, 0.1); + color: #00a87e; +} + +body.theme-minimal .level-tag { + border-radius: 9999px; +} + +/* ============================================ + 历史分析记录 + ============================================ */ +.chart-section { + display: flex; + flex-direction: column; + gap: 16px; +} + +.history-container { + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: 14px; + padding: 16px; +} + +body.theme-minimal .history-container { + background: var(--bg-secondary); + border-radius: 20px; + box-shadow: none; +} + +.history-header { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + font-weight: 600; + margin-bottom: 12px; + color: var(--text-primary); +} + +.history-header i { + color: var(--cyan); +} + +body.theme-minimal .history-header i { + color: var(--text-primary); +} + +.history-list { + display: flex; + flex-direction: column; + gap: 8px; + max-height: 320px; + overflow-y: auto; +} + +.empty-state { + text-align: center; + padding: 30px; + color: var(--text-muted); + font-size: 14px; +} + +.history-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 14px; + background: rgba(255, 255, 255, 0.03); + border: 1px solid var(--border-color); + border-radius: 8px; + cursor: pointer; + transition: all 0.2s; +} + +.history-item:hover { + background: rgba(255, 255, 255, 0.06); + border-color: var(--border-glow); +} + +body.theme-minimal .history-item { + background: var(--bg-card); + border-radius: 12px; +} + +body.theme-minimal .history-item:hover { + background: var(--bg-card-hover); +} + +.history-item-left { + display: flex; + align-items: center; + gap: 12px; +} + +.history-time { + font-size: 12px; + color: var(--text-muted); + min-width: 130px; +} + +.history-suggestion { + padding: 3px 10px; + border-radius: 4px; + font-size: 12px; + font-weight: 500; +} + +.history-suggestion.up { + background: rgba(16, 185, 129, 0.12); + color: var(--green); +} + +.history-suggestion.down { + background: rgba(239, 68, 68, 0.12); + color: var(--red); +} + +.history-suggestion.neutral { + background: rgba(245, 158, 11, 0.1); + color: var(--amber); +} + +body.theme-minimal .history-suggestion { + border-radius: 9999px; +} + +.history-score { + font-size: 13px; + font-weight: 600; +} + +.history-item-right { + display: flex; + align-items: center; + gap: 16px; +} + +.history-metric { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; +} + +.history-metric-label { + font-size: 10px; + color: var(--text-muted); +} + +.history-metric-value { + font-size: 12px; + font-weight: 600; +} + +.history-detail-btn { + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + background: none; + border: 1px solid var(--border-color); + border-radius: 6px; + color: var(--text-muted); + cursor: pointer; + transition: all 0.2s; +} + +.history-detail-btn:hover { + color: var(--cyan); + border-color: var(--cyan); +} + +body.theme-minimal .history-detail-btn { + border-radius: 9999px; +} + +/* ============================================ + 面板头部操作按钮 + ============================================ */ +.panel-header-action { + margin-left: auto; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + color: var(--text-muted); + cursor: pointer; + border-radius: 6px; + transition: all 0.2s; +} + +.panel-header-action:hover { + color: var(--text-primary); + background: rgba(255, 255, 255, 0.06); +} + +/* ============================================ + 对话框 + ============================================ */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(4px); + display: none; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.modal-overlay.active { + display: flex; +} + +.modal-content { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 20px; + width: 90%; + max-width: 600px; + max-height: 80vh; + overflow: hidden; + display: flex; + flex-direction: column; +} + +body.theme-minimal .modal-content { + background: #ffffff; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15); +} + +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 20px 24px; + border-bottom: 1px solid var(--border-color); +} + +.modal-header h3 { + display: flex; + align-items: center; + gap: 10px; + font-size: 16px; + font-weight: 600; +} + +.modal-header h3 i { + color: var(--cyan); +} + +body.theme-minimal .modal-header h3 i { + color: var(--text-primary); +} + +.modal-close { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + background: none; + border: none; + color: var(--text-muted); + cursor: pointer; + border-radius: 8px; + transition: all 0.2s; +} + +.modal-close:hover { + background: rgba(255, 255, 255, 0.1); + color: var(--text-primary); +} + +body.theme-minimal .modal-close:hover { + background: var(--bg-secondary); +} + +.modal-body { + padding: 24px; + overflow-y: auto; + flex: 1; +} + +/* 对话框内容样式 */ +.modal-suggestion-main { + text-align: center; + padding: 20px; + background: rgba(6, 182, 212, 0.06); + border: 1px solid rgba(6, 182, 212, 0.15); + border-radius: 12px; + margin-bottom: 20px; +} + +body.theme-minimal .modal-suggestion-main { + background: var(--bg-secondary); + border-color: var(--border-color); +} + +.modal-suggestion-main .suggestion-badge { + font-size: 24px; + font-weight: 700; + margin-bottom: 8px; +} + +.modal-suggestion-main .suggestion-reason { + font-size: 14px; + color: var(--text-secondary); +} + +.modal-params-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 12px; + margin-bottom: 20px; +} + +.modal-param-card { + padding: 14px; + background: rgba(255, 255, 255, 0.03); + border-radius: 10px; + text-align: center; +} + +body.theme-minimal .modal-param-card { + background: var(--bg-secondary); +} + +.modal-param-card .param-label { + display: block; + font-size: 12px; + color: var(--text-muted); + margin-bottom: 6px; +} + +.modal-param-card .param-value { + font-size: 18px; + font-weight: 700; + font-variant-numeric: tabular-nums; +} + +.modal-section { + margin-bottom: 20px; +} + +.modal-section:last-child { + margin-bottom: 0; +} + +.modal-section-title { + font-size: 14px; + font-weight: 600; + margin-bottom: 12px; + display: flex; + align-items: center; + gap: 8px; +} + +.modal-section-title i { + color: var(--cyan); +} + +body.theme-minimal .modal-section-title i { + color: var(--text-primary); +} + +.modal-indicators-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 10px; +} + +.modal-indicator-item { + padding: 12px; + background: rgba(255, 255, 255, 0.03); + border-radius: 8px; +} + +body.theme-minimal .modal-indicator-item { + background: var(--bg-secondary); +} + +.modal-indicator-item .indicator-label { + font-size: 11px; + color: var(--text-muted); + margin-bottom: 4px; +} + +.modal-indicator-item .indicator-value { + font-size: 14px; + font-weight: 600; +} + +.modal-indicator-item .indicator-detail { + font-size: 11px; + color: var(--text-secondary); +} + +.modal-levels-list { + display: flex; + flex-direction: column; + gap: 6px; +} + +.modal-level-row { + display: flex; + justify-content: space-between; + padding: 8px 12px; + background: rgba(255, 255, 255, 0.03); + border-radius: 6px; + font-size: 13px; +} + +body.theme-minimal .modal-level-row { + background: var(--bg-secondary); +} + +.modal-level-row .level-label { + color: var(--text-secondary); +} + +.modal-level-row .level-value { + font-weight: 600; + font-variant-numeric: tabular-nums; +} + +.modal-trends-list { + display: flex; + flex-direction: column; + gap: 6px; +} + +.modal-trend-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 12px; + background: rgba(255, 255, 255, 0.03); + border-radius: 6px; +} + +body.theme-minimal .modal-trend-row { + background: var(--bg-secondary); +} + +.modal-trend-period { + font-size: 13px; + color: var(--text-secondary); +} + +.modal-trend-badge { + padding: 3px 10px; + border-radius: 4px; + font-size: 11px; + font-weight: 500; +} + +.modal-trend-badge.up { + background: rgba(16, 185, 129, 0.12); + color: var(--green); +} + +.modal-trend-badge.down { + background: rgba(239, 68, 68, 0.12); + color: var(--red); +} + +.modal-trend-badge.neutral { + background: rgba(245, 158, 11, 0.1); + color: var(--amber); +} + +body.theme-minimal .modal-trend-badge { + border-radius: 9999px; +} + +body.theme-minimal .period-tag { + border-radius: 9999px; + padding: 6px 12px; +} + +body.theme-minimal .detail-link { + color: var(--cyan); +} + +body.theme-minimal .suggestion-card .suggestion-content { + background: var(--bg-card); + border-color: var(--border-color); + border-radius: 12px; +} + +body.theme-minimal .indicator-cell { + background: var(--bg-card); + border-radius: 12px; +} + +body.theme-minimal .level-item { + background: var(--bg-card); + border-radius: 9999px; +} + +body.theme-minimal .trend-row { + background: var(--bg-card); + border-radius: 9999px; +} + +body.theme-minimal .param-row { + background: var(--bg-card); + border-radius: 9999px; +} + +body.theme-minimal .back-btn { + background: var(--bg-secondary); + box-shadow: none; + border-radius: 9999px; + padding: 14px 32px; + color: var(--text-secondary); +} + +body.theme-minimal .back-btn:hover { + color: var(--text-primary); +} + +body.theme-minimal .datetime { + color: var(--text-muted); +} + +body.theme-minimal .nav-btn { + border-radius: 9999px; + color: var(--text-secondary); +} + +body.theme-minimal .nav-btn:hover { + background: var(--bg-card-hover); + color: var(--text-primary); +} + +body.theme-minimal .sort-select select { + background: var(--bg-secondary); + border-color: var(--border-color); + border-radius: 9999px; + padding: 10px 36px 10px 16px; + font-family: 'Inter', sans-serif; + font-weight: 500; + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23505a63' d='M6 8L1 3h10z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 14px center; + background-size: 12px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} + +body.theme-minimal .sort-select select:hover { + border-color: var(--text-muted); +} + +body.theme-minimal .sort-select select:focus { + border-color: var(--cyan); + box-shadow: 0 0 0 0.125rem rgba(73, 79, 223, 0.2); +} + +body.theme-minimal .sort-select select option { + background: var(--bg-secondary); + color: var(--text-primary); +} + +body.theme-minimal .sort-select select option:hover, +body.theme-minimal .sort-select select option:checked { + background: rgba(73, 79, 223, 0.1) !important; +} + +body.theme-minimal .filter-tab.active { + background: var(--cyan); + color: white; + border-color: var(--cyan); +} + +body.theme-minimal .view-btn.active { + background: var(--cyan); + border-color: var(--cyan); + color: white; +} + +body.theme-minimal .period-btn.active { + background: var(--cyan); + border-color: var(--cyan); + color: white; +} + +body.theme-minimal .suggestion-badge { + border-radius: 9999px; + padding: 6px 16px; +} + +body.theme-minimal .btn, +body.theme-minimal button { + border-radius: 9999px; +} + +/* 主题切换按钮 */ +.theme-toggle { + position: relative; +} + +.theme-toggle i { + transition: transform 0.3s ease; +} + +body.theme-minimal .theme-toggle i::before { + content: '\f185'; } diff --git a/app/static/futures_analysis.html b/app/static/futures_analysis.html index 9a1e700..d0d1628 100644 --- a/app/static/futures_analysis.html +++ b/app/static/futures_analysis.html @@ -7,39 +7,57 @@ - + +
+
+
@@ -48,43 +66,94 @@
- -
+ +
-
- - +
+ +
+
-
- 分类: - - - - - +
+ + + + + +
-
- 排序: - +
- -
- 8 个品种 - 7 - 1 + +
+
+
+
+ 0 + 监控品种 +
+
+
+
+
+ 0 + 上涨趋势 +
+
+
+
+
+ 0 + 下跌趋势 +
+
+
+
+
+ 0 + 震荡整理 +
+
@@ -95,180 +164,201 @@
- + + + +
- -
-
- ¥2,150 - - +196.00 (+10.06%) - +
+
+ -- + --
-
+
+ -- + -- +
+ R1: -- + S1: -- +
+
+
+
+
开盘 - 1,960 + --
最高 - 2,200 + --
最低 - 1,940 + --
- 持仓量 - 45,600 + 成交量 + --
-
- - 周期选择 - - - - +
+ 周期 +
+ + + + +
-
-
+ +
+
+ K线图 +
+ MA5 + MA10 + MA20 +
+
+
+
+ + +
+
+ + 历史分析记录 +
+
+ +
+
-
- -
-
- - 交易建议 +
+ +
+
+ + AI 交易建议 +
-
-
操作建议
-
逢低做多
-
涨停突破,地缘风险推升运价
+
+
--
+
--
-
-
- 建议入场 - 2,137.1 +
+
+ 入场 + --
-
- 目标价位 - 2,236 +
+ 目标 + --
-
- 止损价位 - 2,107 +
+ 止损 + --
-
- 风险等级 - +
+ 风险 + --
-
-
- +
+
+ 技术指标
-
-
MACD
-
金叉
-
DIF: -0.0147
+
+ MACD + -- + --
-
-
RSI
-
47
-
正常
+
+ RSI + -- + --
-
-
布林带
-
中轨
-
区间: 2086-2215
+
+ BOLL + -- + --
-
-
KDJ
-
中性
-
K: 71 D: 87
+
+ KDJ + -- + --
-
-
- +
+
+ 关键点位
-
-
压力位
-
- 压力 1 - 2,200 -
-
- 压力 2 - 2,300 +
+
+ 压力 +
R1--
+
R2--
+
R3--
-
- 压力 3 - 2,400 +
+
+ 支撑 +
S1--
+
S2--
+
S3--
-
-
支撑位
-
- 支撑 1 - 2,000 -
-
- 支撑 2 - 1,900 -
-
- 支撑 3 - 1,800 -
+
+ + + - -
-
- - 多周期一致性 + +
+
+ + 趋势评分
-
-
- 5分钟 - 上涨 -
-
- 15分钟 - 上涨 -
-
- 30分钟 - 上涨 -
-
- 60分钟 - 震荡 +
+
+ + + + + --
+
综合评分
@@ -277,6 +367,32 @@
+ + + + + + diff --git a/app/static/futures_analysis.js b/app/static/futures_analysis.js index acae11e..ce31b65 100644 --- a/app/static/futures_analysis.js +++ b/app/static/futures_analysis.js @@ -4,19 +4,54 @@ let klineChart = null; let currentSymbol = null; let currentPeriod = '15'; let allFuturesData = []; +let watchedSymbols = []; +let currentDetailData = null; document.addEventListener('DOMContentLoaded', function() { + addScoreGradient(); updateTime(); setInterval(updateTime, 1000); initEventListeners(); + loadWatchedSymbols(); loadFuturesList(); }); +function addScoreGradient() { + const svg = document.querySelector('.score-ring svg'); + if (svg && !svg.querySelector('defs')) { + const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs'); + const gradient = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient'); + gradient.setAttribute('id', 'scoreGradient'); + gradient.setAttribute('x1', '0%'); + gradient.setAttribute('y1', '0%'); + gradient.setAttribute('x2', '100%'); + gradient.setAttribute('y2', '0%'); + + const stop1 = document.createElementNS('http://www.w3.org/2000/svg', 'stop'); + stop1.setAttribute('offset', '0%'); + stop1.setAttribute('stop-color', '#ef4444'); + + const stop2 = document.createElementNS('http://www.w3.org/2000/svg', 'stop'); + stop2.setAttribute('offset', '50%'); + stop2.setAttribute('stop-color', '#f59e0b'); + + const stop3 = document.createElementNS('http://www.w3.org/2000/svg', 'stop'); + stop3.setAttribute('offset', '100%'); + stop3.setAttribute('stop-color', '#10b981'); + + gradient.appendChild(stop1); + gradient.appendChild(stop2); + gradient.appendChild(stop3); + defs.appendChild(gradient); + svg.insertBefore(defs, svg.firstChild); + } +} + function updateTime() { const now = new Date(); - const timeStr = now.getFullYear() + '/' + - String(now.getMonth() + 1).padStart(2, '0') + '/' + + 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') + ':' + @@ -27,6 +62,23 @@ function updateTime() { function initEventListeners() { document.getElementById('back-btn').addEventListener('click', showListView); + document.getElementById('theme-toggle').addEventListener('click', toggleTheme); + + document.getElementById('suggestion-card').addEventListener('click', function() { + if (currentDetailData) { + showSuggestionModal(currentDetailData); + } + }); + + const savedTheme = localStorage.getItem('futures-theme'); + if (savedTheme === 'dark') { + document.body.classList.remove('theme-minimal'); + updateThemeIcon(false); + } else { + document.body.classList.add('theme-minimal'); + updateThemeIcon(true); + } + document.querySelectorAll('.period-btn').forEach(btn => { btn.addEventListener('click', function() { document.querySelectorAll('.period-btn').forEach(b => b.classList.remove('active')); @@ -40,9 +92,9 @@ function initEventListeners() { filterFuturesList(this.value); }); - document.querySelectorAll('.filter-btn').forEach(btn => { - btn.addEventListener('click', function() { - document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active')); + document.querySelectorAll('.filter-tab').forEach(tab => { + tab.addEventListener('click', function() { + document.querySelectorAll('.filter-tab').forEach(t => t.classList.remove('active')); this.classList.add('active'); filterByCategory(this.dataset.category); }); @@ -51,6 +103,18 @@ function initEventListeners() { document.getElementById('sort-select').addEventListener('change', function() { sortFuturesList(this.value); }); + + document.querySelectorAll('.modal-overlay').forEach(modal => { + modal.addEventListener('click', function(e) { + if (e.target === this) { + this.classList.remove('active'); + } + }); + }); +} + +function closeModal(modalId) { + document.getElementById(modalId).classList.remove('active'); } function showListView() { @@ -68,6 +132,80 @@ function showDetailView(symbol) { document.getElementById('detail-view').classList.add('active'); loadFuturesDetail(symbol); loadKlineData(symbol, currentPeriod); + loadHistoryList(symbol); +} + +async function loadWatchedSymbols() { + try { + const response = await fetch(`${API_BASE}/watched`); + const data = await response.json(); + if (data.success) { + watchedSymbols = data.data.map(s => s.symbol); + document.getElementById('count-watched').textContent = watchedSymbols.length; + } + } catch (error) { + console.error('加载自选列表失败:', error); + watchedSymbols = []; + } +} + +async function toggleWatch(symbol, name, event) { + event.stopPropagation(); + const isWatched = watchedSymbols.includes(symbol); + + try { + if (isWatched) { + const response = await fetch(`${API_BASE}/watched/${symbol}`, { method: 'DELETE' }); + const data = await response.json(); + if (data.success) { + watchedSymbols = watchedSymbols.filter(s => s !== symbol); + } + } else { + const response = await fetch(`${API_BASE}/watched`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ symbol, name }) + }); + const data = await response.json(); + if (data.success) { + watchedSymbols.push(symbol); + } + } + document.getElementById('count-watched').textContent = watchedSymbols.length; + + const activeTab = document.querySelector('.filter-tab.active'); + if (activeTab && activeTab.dataset.category === 'watched') { + filterByCategory('watched'); + } else { + renderFuturesGrid(getCurrentFilteredData()); + } + } catch (error) { + console.error('切换自选失败:', error); + } +} + +function getCurrentFilteredData() { + const activeTab = document.querySelector('.filter-tab.active'); + const category = activeTab ? activeTab.dataset.category : 'all'; + return filterDataByCategory(allFuturesData, category); +} + +function filterDataByCategory(data, category) { + if (category === 'all') return data; + if (category === 'watched') { + return data.filter(item => watchedSymbols.includes(item.symbol)); + } + const categoryMap = { + 'energy': ['SC', 'FU', 'LU', 'BU', 'RU', 'NR'], + 'metal': ['AU', 'AG', 'CU', 'AL', 'ZN', 'NI', 'SN', 'PB', 'SS', 'RB', 'HC', 'I', 'J', 'JM', 'AO', 'SI', 'LC', 'PS'], + 'agriculture': ['M', 'RM', 'C', 'CS', 'A', 'B', 'Y', 'P', 'OI', 'CF', 'SR', 'AP', 'LH'], + 'finance': ['IF', 'IC', 'IH', 'IM', 'T', 'TF', 'TS', 'TL'] + }; + const symbols = categoryMap[category] || []; + return data.filter(item => { + const symbolBase = item.symbol.replace(/[0-9]/g, '').toUpperCase(); + return symbols.includes(symbolBase); + }); } async function loadFuturesList() { @@ -123,265 +261,125 @@ async function loadFuturesFromConfig() { 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 - } + { symbol: 'SC2606', 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: 'AU2606', 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: 'AG2606', 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: 'CU2606', 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: 'M2609', 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 => ` + if (data.length === 0) { + grid.innerHTML = '
暂无数据
'; + return; + } + + grid.innerHTML = data.map(item => { + const isWatched = watchedSymbols.includes(item.symbol); + return `
-
-
- ${item.name} - (${item.symbol}) +
+
+
${item.symbol.replace(/[0-9]/g, '').substring(0, 2)}
+
+
${item.name}
+
${item.symbol}
+
¥${formatNumber(item.price)}
- +${formatNumber(item.change)} (+${item.changePct.toFixed(2)}%) + ${item.change >= 0 ? '+' : ''}${formatNumber(item.change)} (${item.changePct >= 0 ? '+' : ''}${item.changePct.toFixed(2)}%)
${item.suggestion} -
- -
- 5分 - 15分 - 30分 - 60分 +
+
+ 成功率 +
+ ${item.successRate}%
-
-
- -
-
-
-
- - ${item.successRate}% -
-
-
- -
-
-
-
- - ${item.trendScore}/100 +
+ 趋势评分 +
+ ${item.trendScore}
-
- -
- 压力: ${formatNumber(item.resistance)} - 支撑: ${formatNumber(item.support)} -
+
- `).join(''); -} - -function getArrow(type) { - if (type === 'up') return 'up'; - if (type === 'down') return 'down'; - return 'right'; + `}).join(''); } function formatNumber(num) { + if (num === 0 || num === undefined || num === null) return '--'; return num.toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 2 }); } +function calcPriceChangePercent(current, target) { + if (!current || !target || current === 0) return '--'; + const pct = ((target - current) / current * 100).toFixed(2); + return (pct >= 0 ? '+' : '') + pct + '%'; +} + function updateStats(data) { - document.getElementById('total-count').textContent = data.length; - const upCount = data.filter(d => d.change >= 0).length; - const downCount = data.length - upCount; + const total = data.length; + const upCount = data.filter(d => d.change > 0).length; + const downCount = data.filter(d => d.change < 0).length; + const neutralCount = total - upCount - downCount; + + document.getElementById('total-count').textContent = total; document.getElementById('up-count').textContent = upCount; document.getElementById('down-count').textContent = downCount; + document.getElementById('neutral-count').textContent = neutralCount; + document.getElementById('count-all').textContent = total; + document.getElementById('count-watched').textContent = watchedSymbols.length; } function filterFuturesList(keyword) { keyword = keyword.toLowerCase(); - const filtered = allFuturesData.filter(item => - item.name.toLowerCase().includes(keyword) || - item.symbol.toLowerCase().includes(keyword) - ); + const activeTab = document.querySelector('.filter-tab.active'); + const category = activeTab ? activeTab.dataset.category : 'all'; + + let filtered = filterDataByCategory(allFuturesData, category); + + if (keyword) { + filtered = filtered.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); - } + let filtered = filterDataByCategory(allFuturesData, category); + renderFuturesGrid(filtered); } function sortFuturesList(sortBy) { - let sorted = [...allFuturesData]; + let sorted = [...getCurrentFilteredData()]; switch(sortBy) { case 'success_rate': sorted.sort((a, b) => b.successRate - a.successRate); @@ -404,59 +402,46 @@ async function loadFuturesDetail(symbol) { const response = await fetch(`${API_BASE}/detail/${symbol}`); const data = await response.json(); if (data.success) { + currentDetailData = data.data; 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'); + document.getElementById('detail-name').textContent = data.name || '--'; + document.getElementById('detail-symbol').textContent = data.symbol || '--'; + + const priceEl = document.getElementById('detail-price'); + priceEl.textContent = '¥' + formatNumber(data.price); + priceEl.className = 'price-value ' + (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)}%)`; + 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 || ''; + const r1 = data.resistances ? data.resistances[0] : data.resistance; + const s1 = data.supports ? data.supports[0] : data.support; + + document.getElementById('detail-r1').textContent = `R1: ${formatNumber(r1)} (${calcPriceChangePercent(data.price, r1)})`; + document.getElementById('detail-s1').textContent = `S1: ${formatNumber(s1)} (${calcPriceChangePercent(data.price, s1)})`; - 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 || '中'; + const badge = document.getElementById('suggestion-badge'); + badge.textContent = data.suggestion || '--'; + badge.className = 'suggestion-badge ' + (data.suggestionType || 'neutral'); + + document.getElementById('suggestion-reason').textContent = data.suggestionReason || '--'; + document.getElementById('entry-price').textContent = formatNumber(data.entryPrice); + document.getElementById('target-price').textContent = formatNumber(data.targetPrice); + document.getElementById('stop-loss').textContent = formatNumber(data.stopLoss); + document.getElementById('risk-level').textContent = data.riskLevel || '--'; if (data.macd) { document.getElementById('macd-signal').textContent = data.macd.signal; @@ -478,70 +463,214 @@ function updateDetailView(data) { 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 (el) { + el.querySelector('span:last-child').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 (el) { + el.querySelector('span:last-child').textContent = formatNumber(data.supports[i]); } } } if (data.periodConsistency) { - const container = document.getElementById('period-consistency'); + const container = document.getElementById('period-trends'); const periodNames = { '5': '5分钟', '15': '15分钟', '30': '30分钟', '60': '60分钟' }; container.innerHTML = Object.entries(data.periodConsistency).map(([period, trend]) => ` -
- ${periodNames[period]} - - +
+ ${periodNames[period]} + ${trend === 'up' ? '上涨' : trend === 'down' ? '下跌' : '震荡'}
`).join(''); } + + if (data.trendScore !== undefined) { + document.getElementById('trend-score').textContent = data.trendScore; + const circle = document.getElementById('score-fill'); + const circumference = 2 * Math.PI * 45; + const offset = circumference - (data.trendScore / 100) * circumference; + circle.style.strokeDasharray = circumference; + circle.style.strokeDashoffset = offset; + } } -async function loadKlineData(symbol, period) { +async function loadHistoryList(symbol) { try { - const response = await fetch(`${API_BASE}/kline/${symbol}?period=${period}`); + const response = await fetch(`${API_BASE}/analysis/history/${symbol}?limit=10`); const data = await response.json(); if (data.success) { - renderKlineChart(data.data); + renderHistoryList(data.data); } } catch (error) { - console.error('加载K线数据失败:', error); - const mockKline = generateMockKlineData(); - renderKlineChart(mockKline); + console.error('加载历史记录失败:', error); + document.getElementById('history-list').innerHTML = '
暂无历史记录
'; } } -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; +function renderHistoryList(records) { + const container = document.getElementById('history-list'); + if (!records || records.length === 0) { + container.innerHTML = '
暂无历史记录
'; + return; } - return data; + container.innerHTML = records.map(record => ` +
+
+ ${record.analysis_time ? record.analysis_time.replace('T', ' ').substring(0, 16) : '--'} + ${record.suggestion || '--'} + 评分: ${record.trend_score || '--'} +
+
+
+ MACD + ${record.macd_signal || '--'} +
+
+ RSI + ${record.rsi_value || '--'} +
+ +
+
+ `).join(''); +} + +function showSuggestionModal(data) { + const body = document.getElementById('suggestion-modal-body'); + body.innerHTML = ` + + + `; + document.getElementById('suggestion-modal').classList.add('active'); +} + +function showHistoryModal(record) { + const body = document.getElementById('history-modal-body'); + body.innerHTML = ` + + + + + + `; + document.getElementById('history-modal').classList.add('active'); +} + +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); + } } function renderKlineChart(data) { @@ -564,67 +693,36 @@ function renderKlineChart(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: { type: 'cross' }, + backgroundColor: 'rgba(10, 15, 25, 0.95)', + borderColor: 'rgba(56, 189, 248, 0.2)', + textStyle: { color: '#e2e8f0', fontSize: 12 } }, axisPointer: { link: [{ xAxisIndex: 'all' }], - label: { - backgroundColor: '#22c55e' - } + label: { backgroundColor: '#06b6d4' } }, 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%' } + { left: 70, right: 20, top: 10, height: '50%' }, + { left: 70, right: 20, top: '56%', height: '16%' }, + { left: 70, right: 20, top: '76%', height: '16%' } ], xAxis: [ { type: 'category', data: dates, boundaryGap: true, - axisLine: { lineStyle: { color: '#2a2d3a' } }, - axisLabel: { color: '#9aa0ab', fontSize: 10 }, + axisLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }, + axisLabel: { color: '#64748b', fontSize: 10 }, splitLine: { show: false } }, { type: 'category', gridIndex: 1, data: dates, - boundaryGap: true, - axisLine: { lineStyle: { color: '#2a2d3a' } }, + axisLine: { show: false }, axisLabel: { show: false }, splitLine: { show: false } }, @@ -632,53 +730,46 @@ function renderKlineChart(data) { type: 'category', gridIndex: 2, data: dates, - boundaryGap: true, - axisLine: { lineStyle: { color: '#2a2d3a' } }, - axisLabel: { color: '#9aa0ab', fontSize: 10 }, + axisLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }, + axisLabel: { color: '#64748b', fontSize: 10 }, splitLine: { show: false } } ], yAxis: [ { scale: true, - axisLine: { lineStyle: { color: '#2a2d3a' } }, - axisLabel: { color: '#9aa0ab' }, - splitLine: { lineStyle: { color: '#2a2d3a', type: 'dashed' } } + axisLine: { show: false }, + axisLabel: { color: '#64748b' }, + splitLine: { lineStyle: { color: 'rgba(255,255,255,0.05)', 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' } } + axisLine: { show: false }, + axisLabel: { color: '#64748b', fontSize: 10 }, + splitLine: { lineStyle: { color: 'rgba(255,255,255,0.05)', type: 'dashed' } } } ], dataZoom: [ - { - type: 'inside', - xAxisIndex: [0, 1, 2], - start: 50, - end: 100 - }, + { type: 'inside', xAxisIndex: [0, 1, 2], start: 50, end: 100 }, { show: true, xAxisIndex: [0, 1, 2], type: 'slider', bottom: 5, - height: 18, + height: 16, borderColor: 'transparent', - backgroundColor: '#1a1d28', - fillerColor: 'rgba(34, 197, 94, 0.15)', - handleStyle: { color: '#22c55e' }, - textStyle: { color: '#9aa0ab' } + backgroundColor: 'rgba(15, 20, 30, 0.5)', + fillerColor: 'rgba(6, 182, 212, 0.15)', + handleStyle: { color: '#06b6d4' }, + textStyle: { color: '#64748b' } } ], series: [ @@ -687,9 +778,9 @@ function renderKlineChart(data) { type: 'candlestick', data: values, itemStyle: { - color: '#22c55e', + color: '#10b981', color0: '#ef4444', - borderColor: '#22c55e', + borderColor: '#10b981', borderColor0: '#ef4444' } }, @@ -721,10 +812,7 @@ function renderKlineChart(data) { yAxisIndex: 1, data: volumes.map(v => ({ value: v[0], - itemStyle: { - color: v[1] >= 0 ? '#22c55e' : '#ef4444', - opacity: 0.6 - } + itemStyle: { color: v[1] >= 0 ? 'rgba(16,185,129,0.5)' : 'rgba(239,68,68,0.5)' } })) }, { @@ -750,22 +838,16 @@ function renderKlineChart(data) { type: 'bar', xAxisIndex: 2, yAxisIndex: 2, - data: macdData.macd.map((val, idx) => ({ + data: macdData.macd.map(val => ({ value: val, - itemStyle: { - color: val >= 0 ? '#22c55e' : '#ef4444', - opacity: 0.7 - } + itemStyle: { color: val >= 0 ? 'rgba(16,185,129,0.6)' : 'rgba(239,68,68,0.6)' } })) } ] }; klineChart.setOption(option); - - window.addEventListener('resize', () => { - klineChart && klineChart.resize(); - }); + window.addEventListener('resize', () => klineChart && klineChart.resize()); } function calculateMA(data, dayCount) { @@ -784,6 +866,19 @@ function calculateMA(data, dayCount) { return result; } +function toggleTheme() { + const isMinimal = document.body.classList.toggle('theme-minimal'); + localStorage.setItem('futures-theme', isMinimal ? 'minimal' : 'dark'); + updateThemeIcon(isMinimal); +} + +function updateThemeIcon(isMinimal) { + const icon = document.querySelector('#theme-toggle i'); + if (icon) { + icon.className = isMinimal ? 'fas fa-sun' : 'fas fa-moon'; + } +} + function calculateMACD(data) { const closes = data.map(d => parseFloat(d[2])); const ema12 = calcEMA(closes, 12); @@ -799,7 +894,6 @@ function calculateMACD(data) { } const dea = calcEMA(dif, 9); - const macd = dif.map((d, i) => 2 * (d - (dea[i] || 0))); return { dif, dea, macd }; @@ -812,9 +906,7 @@ function calcEMA(data, period) { if (data.length < period) return result; let sum = 0; - for (let i = 0; i < period; i++) { - sum += data[i]; - } + for (let i = 0; i < period; i++) sum += data[i]; result[period - 1] = sum / period; for (let i = period; i < data.length; i++) { From 82b8f859d889664a2ca39ea86ca7209dbda8e48d Mon Sep 17 00:00:00 2001 From: Lxy Date: Thu, 21 May 2026 01:05:32 +0800 Subject: [PATCH 03/16] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9=E5=8E=8B?= =?UTF-8?q?=E5=8A=9B=E6=94=AF=E6=92=91=E8=AE=A1=E7=AE=97=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../futures_analysis.cpython-311.pyc | Bin 39472 -> 39467 bytes app/api/futures_analysis.py | 25 +++++++++--------- app/static/futures_analysis.css | 23 ++++++++++++++++ app/static/futures_analysis.html | 7 +++-- app/static/futures_analysis.js | 14 ++++++++-- 5 files changed, 53 insertions(+), 16 deletions(-) diff --git a/app/api/__pycache__/futures_analysis.cpython-311.pyc b/app/api/__pycache__/futures_analysis.cpython-311.pyc index 8c155c097be6c794dc0e01f2d8c164ccb42604a2..0050bd32a7866133d8a0bc0db1414915fe12f69a 100644 GIT binary patch delta 3918 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 F>wmETBa{FD delta 4015 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 diff --git a/app/api/futures_analysis.py b/app/api/futures_analysis.py index 6e9653a..1de767e 100644 --- a/app/api/futures_analysis.py +++ b/app/api/futures_analysis.py @@ -67,8 +67,8 @@ def get_futures_list(db: Session = Depends(get_db)): "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), + "resistance": round(2 * ((high_price + low_price + close_price) / 3) - low_price, 2), + "support": round(2 * ((high_price + low_price + close_price) / 3) - high_price, 2), "open": open_price, "high": high_price, "low": low_price, @@ -120,12 +120,12 @@ def get_futures_detail(symbol: str, db: Session = Depends(get_db)): 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) + # Pivot Point 公式计算关键点位 + pp = (high_price + low_price + close_price) / 3 + r1 = round(2 * pp - low_price, 2) + r2 = round(pp + (high_price - low_price), 2) + s1 = round(2 * pp - high_price, 2) + s2 = round(pp - (high_price - low_price), 2) suggestion = _get_suggestion(close_price, open_price, change_pct) suggestion_type = "up" if change >= 0 else "down" @@ -145,15 +145,16 @@ def get_futures_detail(symbol: str, db: Session = Depends(get_db)): "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, + "targetPrice": r1 if change >= 0 else s1, + "stopLoss": s1 if change >= 0 else r1, "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], + "resistances": [r1, r2], + "supports": [s1, s2], + "pivotPoint": round(pp, 2), "periodConsistency": _get_period_trends(all_candles) } diff --git a/app/static/futures_analysis.css b/app/static/futures_analysis.css index ebe543c..b07bfe5 100644 --- a/app/static/futures_analysis.css +++ b/app/static/futures_analysis.css @@ -1117,6 +1117,24 @@ body { font-variant-numeric: tabular-nums; } +.level-item.pivot-point { + background: rgba(139, 92, 246, 0.1); + border: 1px solid rgba(139, 92, 246, 0.2); + border-radius: 8px; + padding: 8px 12px; + margin: 4px 0; +} + +.level-item.pivot-point span:first-child { + color: var(--purple); + font-weight: 600; +} + +.level-item.pivot-point span:last-child { + color: var(--purple); + font-size: 14px; +} + .level-divider { height: 1px; background: var(--border-color); @@ -1517,6 +1535,11 @@ body.theme-minimal .level-item { border-radius: 9999px; } +body.theme-minimal .level-item.pivot-point { + background: rgba(124, 58, 237, 0.08); + border-color: rgba(124, 58, 237, 0.2); +} + body.theme-minimal .trend-row { background: var(--bg-card); border-radius: 9999px; diff --git a/app/static/futures_analysis.html b/app/static/futures_analysis.html index d0d1628..4a33e53 100644 --- a/app/static/futures_analysis.html +++ b/app/static/futures_analysis.html @@ -321,14 +321,17 @@ 压力
R1--
R2--
-
R3--
+
+
+
+ 中枢 (PP) + --
支撑
S1--
S2--
-
S3--
diff --git a/app/static/futures_analysis.js b/app/static/futures_analysis.js index ce31b65..877740b 100644 --- a/app/static/futures_analysis.js +++ b/app/static/futures_analysis.js @@ -461,7 +461,7 @@ function updateDetailView(data) { } if (data.resistances) { - for (let i = 0; i < 3; i++) { + for (let i = 0; i < 2; i++) { const el = document.getElementById(`resistance-${i + 1}`); if (el) { el.querySelector('span:last-child').textContent = formatNumber(data.resistances[i]); @@ -469,7 +469,7 @@ function updateDetailView(data) { } } if (data.supports) { - for (let i = 0; i < 3; i++) { + for (let i = 0; i < 2; i++) { const el = document.getElementById(`support-${i + 1}`); if (el) { el.querySelector('span:last-child').textContent = formatNumber(data.supports[i]); @@ -477,6 +477,10 @@ function updateDetailView(data) { } } + if (data.pivotPoint) { + document.getElementById('pivot-point').querySelector('span:last-child').textContent = formatNumber(data.pivotPoint); + } + if (data.periodConsistency) { const container = document.getElementById('period-trends'); const periodNames = { '5': '5分钟', '15': '15分钟', '30': '30分钟', '60': '60分钟' }; @@ -630,6 +634,12 @@ function showHistoryModal(record) { ${formatNumber(v)}
`).join('')} + ${record.pivot_point ? ` + + ` : ''} ${(record.support_levels || []).map((v, i) => ` + + +