diff --git a/app/__pycache__/main.cpython-311.pyc b/app/__pycache__/main.cpython-311.pyc
index 13e81c5..c52b950 100644
Binary files a/app/__pycache__/main.cpython-311.pyc and b/app/__pycache__/main.cpython-311.pyc differ
diff --git a/app/api/__pycache__/ai_config.cpython-311.pyc b/app/api/__pycache__/ai_config.cpython-311.pyc
new file mode 100644
index 0000000..9982b26
Binary files /dev/null and b/app/api/__pycache__/ai_config.cpython-311.pyc differ
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 0000000..dbb1093
Binary files /dev/null and b/app/api/__pycache__/futures_analysis.cpython-311.pyc differ
diff --git a/app/api/ai_config.py b/app/api/ai_config.py
new file mode 100644
index 0000000..a506da7
--- /dev/null
+++ b/app/api/ai_config.py
@@ -0,0 +1,177 @@
+"""
+AI模型配置接口 - 管理AI分析模型的配置
+"""
+import json
+import logging
+from pathlib import Path
+from typing import Optional
+
+from fastapi import APIRouter, Depends, HTTPException
+from pydantic import BaseModel
+
+logger = logging.getLogger(__name__)
+router = APIRouter(prefix="/ai-config", tags=["AI模型配置"])
+
+
+class AIModelConfig(BaseModel):
+ """AI模型配置"""
+ model_name: str
+ api_key: str
+ api_base: str = "https://api.openai.com/v1"
+ model_id: str = "gpt-4"
+ temperature: float = 0.7
+ max_tokens: int = 2000
+ enabled: bool = True
+
+
+class AIConfigResponse(BaseModel):
+ """AI配置响应"""
+ success: bool
+ data: Optional[dict] = None
+ message: str = ""
+
+
+class SaveAIConfigRequest(BaseModel):
+ """保存AI配置请求"""
+ models: list = []
+ active_model: Optional[str] = None
+ analysis_settings: Optional[dict] = None
+
+
+CONFIG_DIR = Path(__file__).resolve().parent.parent.parent / "config"
+AI_CONFIG_FILE = CONFIG_DIR / "ai_config.json"
+
+
+def _ensure_config_dir():
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
+
+
+def _load_ai_config() -> 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模型配置 - 期货智析
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 技术分析
+ 基于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 => `
+
+ `).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,137.1
+
+
+ 目标价位
+ 2,236
+
+
+ 止损价位
+ 2,107
+
+
+ 风险等级
+ 低
+
+
+
+
+
+
+
+
+ 技术指标
+
+
+
+
MACD
+
金叉
+
DIF: -0.0147
+
+
+
+
布林带
+
中轨
+
区间: 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.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配置
+