feat : 增加智能分析模块

master^2
Lxy 2 weeks ago
parent 705bd802ca
commit 168f06ecf5

@ -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}

@ -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}"}

@ -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")

@ -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;
}
}

@ -0,0 +1,189 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI模型配置 - 期货智析</title>
<link rel="stylesheet" href="/static/ai_config.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body>
<div class="app-container">
<header class="top-nav">
<div class="nav-left">
<a href="/static/futures_analysis.html" class="back-link">
<i class="fas fa-arrow-left"></i> 返回期货智析
</a>
<div class="page-title">
<i class="fas fa-robot"></i>
<span>AI模型配置</span>
</div>
</div>
<div class="nav-right">
<a href="/" class="nav-icon-btn" title="返回首页">
<i class="fas fa-home"></i>
</a>
</div>
</header>
<main class="main-content">
<div class="config-container">
<!-- AI提供商选择 -->
<div class="config-card">
<div class="card-header">
<h3><i class="fas fa-cloud"></i> AI提供商</h3>
</div>
<div class="provider-grid" id="provider-grid">
<!-- 动态生成 -->
</div>
</div>
<!-- API配置 -->
<div class="config-card">
<div class="card-header">
<h3><i class="fas fa-key"></i> API配置</h3>
</div>
<div class="form-grid">
<div class="form-group">
<label>提供商</label>
<select id="api-provider" class="form-control">
<option value="openai">OpenAI</option>
<option value="anthropic">Anthropic Claude</option>
<option value="google">Google Gemini</option>
<option value="aliyun">阿里云通义千问</option>
<option value="baidu">百度文心一言</option>
<option value="zhipu">智谱清言</option>
<option value="custom">自定义</option>
</select>
</div>
<div class="form-group">
<label>API Key</label>
<div class="input-with-toggle">
<input type="password" id="api-key" class="form-control" placeholder="sk-...">
<button type="button" class="toggle-visibility" onclick="toggleApiKeyVisibility()">
<i class="fas fa-eye"></i>
</button>
</div>
</div>
<div class="form-group">
<label>API Base URL</label>
<input type="text" id="api-base" class="form-control" placeholder="https://api.openai.com/v1">
</div>
<div class="form-group">
<label>模型ID</label>
<select id="model-id" class="form-control">
<option value="gpt-4o">gpt-4o</option>
<option value="gpt-4-turbo">gpt-4-turbo</option>
<option value="gpt-3.5-turbo">gpt-3.5-turbo</option>
</select>
</div>
</div>
<div class="form-actions">
<button class="btn btn-secondary" onclick="testConnection()">
<i class="fas fa-plug"></i> 测试连接
</button>
<span id="test-result" class="test-result"></span>
</div>
</div>
<!-- 模型参数 -->
<div class="config-card">
<div class="card-header">
<h3><i class="fas fa-sliders-h"></i> 模型参数</h3>
</div>
<div class="form-grid">
<div class="form-group">
<label>Temperature (创造力): <span id="temp-value">0.7</span></label>
<input type="range" id="temperature" class="form-range" min="0" max="2" step="0.1" value="0.7">
<div class="range-labels">
<span>精确</span>
<span>创造</span>
</div>
</div>
<div class="form-group">
<label>Max Tokens (最大输出)</label>
<input type="number" id="max-tokens" class="form-control" value="2000" min="100" max="8000">
</div>
</div>
</div>
<!-- 分析设置 -->
<div class="config-card">
<div class="card-header">
<h3><i class="fas fa-cogs"></i> 分析设置</h3>
</div>
<div class="settings-list">
<div class="setting-item">
<div class="setting-info">
<span class="setting-name">技术分析</span>
<span class="setting-desc">基于K线和技术指标进行分析</span>
</div>
<label class="switch">
<input type="checkbox" id="enable-technical" checked>
<span class="slider"></span>
</label>
</div>
<div class="setting-item">
<div class="setting-info">
<span class="setting-name">基本面分析</span>
<span class="setting-desc">结合宏观经济和行业数据</span>
</div>
<label class="switch">
<input type="checkbox" id="enable-fundamental">
<span class="slider"></span>
</label>
</div>
<div class="setting-item">
<div class="setting-info">
<span class="setting-name">情绪分析</span>
<span class="setting-desc">分析市场情绪和新闻舆情</span>
</div>
<label class="switch">
<input type="checkbox" id="enable-sentiment">
<span class="slider"></span>
</label>
</div>
<div class="form-group">
<label>风险偏好</label>
<select id="risk-tolerance" class="form-control">
<option value="conservative">保守型</option>
<option value="medium" selected>平衡型</option>
<option value="aggressive">激进型</option>
</select>
</div>
<div class="form-group">
<label>最大仓位比例 (%)</label>
<input type="number" id="max-position" class="form-control" value="10" min="1" max="100">
</div>
</div>
</div>
<!-- 已保存的模型 -->
<div class="config-card">
<div class="card-header">
<h3><i class="fas fa-database"></i> 已保存的模型</h3>
<button class="btn btn-primary btn-sm" onclick="addNewModel()">
<i class="fas fa-plus"></i> 添加模型
</button>
</div>
<div class="models-list" id="models-list">
<!-- 动态生成 -->
</div>
</div>
<!-- 保存按钮 -->
<div class="save-actions">
<button class="btn btn-primary btn-lg" onclick="saveConfig()">
<i class="fas fa-save"></i> 保存配置
</button>
<button class="btn btn-secondary btn-lg" onclick="loadConfig()">
<i class="fas fa-sync"></i> 重新加载
</button>
</div>
</div>
</main>
</div>
<script src="/static/ai_config.js"></script>
</body>
</html>

@ -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 => `
<div class="provider-card ${p.id === selectedProvider ? 'active' : ''}" data-provider="${p.id}">
<i class="${iconMap[p.id] || 'fas fa-cog'}"></i>
<div class="provider-name">${p.name}</div>
</div>
`).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 => `<option value="${m}">${m}</option>`).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 = '<div class="empty-state">暂无已保存的模型</div>';
return;
}
list.innerHTML = models.map((model, index) => `
<div class="model-item">
<div class="model-info">
<div class="model-status ${model.enabled ? 'active' : 'inactive'}"></div>
<div>
<div class="model-name">${model.model_name || model.model_id}</div>
<div class="model-provider">${getProviderName(model.provider || model.api_base)}</div>
</div>
</div>
<div class="model-actions">
${!model.enabled ? `<button class="btn-set-active" onclick="setActiveModel(${index})">设为默认</button>` : '<span class="active-badge">默认</span>'}
<button class="btn-delete" onclick="deleteModel(${index})"><i class="fas fa-trash"></i></button>
</div>
</div>
`).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();
}

@ -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;
}
}

@ -0,0 +1,283 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>期货智析 - 智能期货期权分析系统</title>
<link rel="stylesheet" href="/static/futures_analysis.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body>
<div class="app-container">
<!-- 顶部导航 -->
<header class="top-nav">
<div class="nav-left">
<div class="logo">
<div class="logo-icon"><i class="fas fa-chart-line"></i></div>
<div class="logo-text">
<span class="logo-title">期货智析</span>
<span class="logo-subtitle">智能期货期权分析系统</span>
</div>
</div>
</div>
<nav class="nav-center">
<a href="#" class="nav-item" data-page="market">市场概览</a>
<a href="#" class="nav-item" data-page="events">热点事件</a>
<a href="#" class="nav-item active" data-page="analysis">品种分析</a>
<a href="#" class="nav-item" data-page="risk">风险提醒</a>
</nav>
<div class="nav-right">
<div class="datetime">
<i class="far fa-clock"></i>
<span id="current-time">2026/03/14 16:07:52</span>
</div>
<a href="/static/ai_config.html" class="nav-icon-btn" title="AI配置">
<i class="fas fa-robot"></i>
</a>
<div class="notification">
<i class="far fa-bell"></i>
<span class="badge">3</span>
</div>
<a href="/" class="nav-icon-btn" title="返回首页">
<i class="fas fa-home"></i>
</a>
</div>
</header>
<!-- 主内容区 -->
<main class="main-content">
<!-- 品种列表视图 -->
<div id="list-view" class="view active">
<!-- 搜索和筛选栏 -->
<div class="toolbar">
<div class="search-box">
<i class="fas fa-search"></i>
<input type="text" id="search-input" placeholder="搜索品种名称或代码...">
</div>
<div class="view-toggle">
<button class="toggle-btn active" data-view="grid"><i class="fas fa-th"></i></button>
<button class="toggle-btn" data-view="list"><i class="fas fa-list"></i></button>
</div>
</div>
<div class="filter-bar">
<div class="filter-group">
<span class="filter-label">分类:</span>
<button class="filter-btn active" data-category="all">全部</button>
<button class="filter-btn" data-category="energy">能源</button>
<button class="filter-btn" data-category="metal">金属</button>
<button class="filter-btn" data-category="agriculture">农产品</button>
<button class="filter-btn" data-category="finance">金融</button>
</div>
<div class="sort-group">
<span class="filter-label">排序:</span>
<select id="sort-select" class="sort-select">
<option value="success_rate">成功率</option>
<option value="trend_score">趋势评分</option>
<option value="change_pct">涨跌幅</option>
<option value="name">名称</option>
</select>
</div>
</div>
<!-- 统计信息 -->
<div class="stats-bar">
<span><strong id="total-count">8</strong> 个品种</span>
<span class="stat-up"><i class="fas fa-arrow-up"></i> <strong id="up-count">7</strong></span>
<span class="stat-down"><i class="fas fa-arrow-down"></i> <strong id="down-count">1</strong></span>
</div>
<!-- 品种卡片网格 -->
<div id="futures-grid" class="futures-grid">
<!-- 动态生成 -->
</div>
</div>
<!-- 详情分析视图 -->
<div id="detail-view" class="view">
<!-- 返回按钮和品种标题 -->
<div class="detail-header">
<button class="back-btn" id="back-btn">
<i class="fas fa-arrow-left"></i> 返回列表
</button>
<div class="detail-title-bar">
<div class="price-info">
<span class="current-price" id="detail-price">¥2,150</span>
<span class="price-change up" id="detail-change">
<i class="fas fa-arrow-up"></i> +196.00 (+10.06%)
</span>
</div>
<div class="quote-info">
<div class="quote-item">
<span class="quote-label">开盘</span>
<span class="quote-value" id="detail-open">1,960</span>
</div>
<div class="quote-item">
<span class="quote-label">最高</span>
<span class="quote-value up" id="detail-high">2,200</span>
</div>
<div class="quote-item">
<span class="quote-label">最低</span>
<span class="quote-value down" id="detail-low">1,940</span>
</div>
<div class="quote-item">
<span class="quote-label">持仓量</span>
<span class="quote-value" id="detail-volume">45,600</span>
</div>
</div>
</div>
</div>
<!-- 周期选择 -->
<div class="period-selector">
<i class="far fa-clock"></i>
<span class="period-label">周期选择</span>
<button class="period-btn" data-period="5">5分钟</button>
<button class="period-btn active" data-period="15">15分钟</button>
<button class="period-btn" data-period="30">30分钟</button>
<button class="period-btn" data-period="60">60分钟</button>
</div>
<!-- 图表和侧边栏 -->
<div class="detail-body">
<!-- K线图表区 -->
<div class="chart-section">
<div id="kline-chart" class="kline-chart"></div>
</div>
<!-- 右侧分析面板 -->
<div class="analysis-panel">
<!-- 交易建议 -->
<div class="panel-card trade-suggestion">
<div class="panel-title">
<i class="fas fa-bullseye"></i>
<span>交易建议</span>
</div>
<div class="suggestion-box up" id="suggestion-box">
<div class="suggestion-label">操作建议</div>
<div class="suggestion-action" id="suggestion-action">逢低做多</div>
<div class="suggestion-reason" id="suggestion-reason">涨停突破,地缘风险推升运价</div>
</div>
<div class="suggestion-details">
<div class="detail-row">
<span class="detail-label">建议入场</span>
<span class="detail-value" id="entry-price">2,137.1</span>
</div>
<div class="detail-row">
<span class="detail-label">目标价位</span>
<span class="detail-value up" id="target-price">2,236</span>
</div>
<div class="detail-row">
<span class="detail-label">止损价位</span>
<span class="detail-value down" id="stop-loss">2,107</span>
</div>
<div class="detail-row">
<span class="detail-label">风险等级</span>
<span class="detail-value" id="risk-level"></span>
</div>
</div>
</div>
<!-- 技术指标 -->
<div class="panel-card tech-indicators">
<div class="panel-title">
<i class="fas fa-wave-square"></i>
<span>技术指标</span>
</div>
<div class="indicators-grid">
<div class="indicator-item">
<div class="indicator-name">MACD</div>
<div class="indicator-value up" id="macd-signal">金叉</div>
<div class="indicator-detail" id="macd-detail">DIF: -0.0147</div>
</div>
<div class="indicator-item">
<div class="indicator-name">RSI</div>
<div class="indicator-value" id="rsi-value">47</div>
<div class="indicator-detail" id="rsi-status">正常</div>
</div>
<div class="indicator-item">
<div class="indicator-name">布林带</div>
<div class="indicator-value" id="boll-signal">中轨</div>
<div class="indicator-detail" id="boll-detail">区间: 2086-2215</div>
</div>
<div class="indicator-item">
<div class="indicator-name">KDJ</div>
<div class="indicator-value" id="kdj-signal">中性</div>
<div class="indicator-detail" id="kdj-detail">K: 71 D: 87</div>
</div>
</div>
</div>
<!-- 关键点位 -->
<div class="panel-card key-levels">
<div class="panel-title">
<i class="fas fa-chart-bar"></i>
<span>关键点位</span>
</div>
<div class="levels-section">
<div class="levels-header resistance">压力位</div>
<div class="level-row" id="resistance-1">
<span>压力 1</span>
<span class="level-value down">2,200</span>
</div>
<div class="level-row" id="resistance-2">
<span>压力 2</span>
<span class="level-value down">2,300</span>
</div>
<div class="level-row" id="resistance-3">
<span>压力 3</span>
<span class="level-value down">2,400</span>
</div>
</div>
<div class="levels-section">
<div class="levels-header support">支撑位</div>
<div class="level-row" id="support-1">
<span>支撑 1</span>
<span class="level-value up">2,000</span>
</div>
<div class="level-row" id="support-2">
<span>支撑 2</span>
<span class="level-value up">1,900</span>
</div>
<div class="level-row" id="support-3">
<span>支撑 3</span>
<span class="level-value up">1,800</span>
</div>
</div>
</div>
<!-- 多周期一致性 -->
<div class="panel-card multi-period">
<div class="panel-title">
<i class="fas fa-bolt"></i>
<span>多周期一致性</span>
</div>
<div class="period-consistency" id="period-consistency">
<div class="consistency-row">
<span class="period-name">5分钟</span>
<span class="consistency-badge up"><i class="fas fa-arrow-up"></i> 上涨</span>
</div>
<div class="consistency-row">
<span class="period-name">15分钟</span>
<span class="consistency-badge up"><i class="fas fa-arrow-up"></i> 上涨</span>
</div>
<div class="consistency-row">
<span class="period-name">30分钟</span>
<span class="consistency-badge up"><i class="fas fa-arrow-up"></i> 上涨</span>
</div>
<div class="consistency-row">
<span class="period-name">60分钟</span>
<span class="consistency-badge neutral"><i class="fas fa-minus"></i> 震荡</span>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<script src="/static/futures_analysis.js"></script>
</body>
</html>

@ -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 => `
<div class="futures-card" onclick="showDetailView('${item.symbol}')">
<div class="card-header">
<div class="card-title">
<span class="card-name">${item.name}</span>
<span class="card-code">(${item.symbol})</span>
</div>
<div class="card-price">
<div class="price-value ${item.change >= 0 ? 'up' : 'down'}">¥${formatNumber(item.price)}</div>
<div class="price-change ${item.change >= 0 ? 'up' : 'down'}">
<i class="fas fa-arrow-${item.change >= 0 ? 'up' : 'down'}"></i>
+${formatNumber(item.change)} (+${item.changePct.toFixed(2)}%)
</div>
</div>
</div>
<span class="suggestion-badge ${item.suggestionType}">${item.suggestion}</span>
<div class="card-section">
<div class="section-label"><i class="far fa-clock"></i> </div>
<div class="period-tags">
<span class="period-tag ${item.periods['5']}"><i class="fas fa-arrow-${getArrow(item.periods['5'])}"></i> 5</span>
<span class="period-tag ${item.periods['15']}"><i class="fas fa-arrow-${getArrow(item.periods['15'])}"></i> 15</span>
<span class="period-tag ${item.periods['30']}"><i class="fas fa-arrow-${getArrow(item.periods['30'])}"></i> 30</span>
<span class="period-tag ${item.periods['60']}"><i class="fas fa-arrow-${getArrow(item.periods['60'])}"></i> 60</span>
</div>
</div>
<div class="card-section">
<div class="section-label"><i class="fas fa-chart-bar"></i> </div>
<div class="progress-bar">
<div class="progress-fill ${item.successRate >= 70 ? 'up' : item.successRate >= 60 ? 'orange' : 'down'}" style="width: ${item.successRate}%"></div>
</div>
<div class="progress-info">
<span class="progress-label"></span>
<span class="progress-value ${item.successRate >= 70 ? 'up' : item.successRate >= 60 ? '' : 'down'}">${item.successRate}%</span>
</div>
</div>
<div class="card-section">
<div class="section-label">趋势评分</div>
<div class="progress-bar">
<div class="progress-fill ${item.trendScore >= 70 ? 'up' : item.trendScore >= 50 ? 'orange' : 'down'}" style="width: ${item.trendScore}%"></div>
</div>
<div class="progress-info">
<span class="progress-label"></span>
<span class="progress-value ${item.trendScore >= 70 ? 'up' : item.trendScore >= 50 ? '' : 'down'}">${item.trendScore}/100</span>
</div>
</div>
<div class="card-section">
<div class="section-label"><i class="fas fa-crosshairs"></i> </div>
<div class="key-levels-row">
<span class="level-label">压力: <span class="level-value down">${formatNumber(item.resistance)}</span></span>
<span class="level-label">支撑: <span class="level-value up">${formatNumber(item.support)}</span></span>
</div>
</div>
<div class="card-footer">
<span class="detail-link">查看详情 <i class="fas fa-chevron-right"></i></span>
</div>
</div>
`).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 = `<i class="fas fa-arrow-${changeIcon}"></i> ${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]) => `
<div class="consistency-row">
<span class="period-name">${periodNames[period]}</span>
<span class="consistency-badge ${trend}">
<i class="fas fa-arrow-${trend === 'up' ? 'up' : trend === 'down' ? 'down' : 'right'}"></i>
${trend === 'up' ? '上涨' : trend === 'down' ? '下跌' : '震荡'}
</span>
</div>
`).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 = `<div style="font-weight:600;margin-bottom:6px">${params[0].axisValue}</div>`;
params.forEach(p => {
if (p.seriesName === 'K线' && p.data) {
const [o, c, l, h] = p.data;
result += `开: ${o} 收: ${c}<br/>低: ${l} 高: ${h}`;
} else if (p.seriesName === '成交量') {
result += `<br/>成交量: ${p.data}`;
} else if (p.seriesName === 'DIF' || p.seriesName === 'DEA') {
result += `<br/>${p.seriesName}: ${p.data}`;
} else if (p.seriesName === 'MACD') {
result += `<br/>MACD: ${p.data}`;
} else {
result += `<br/>${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;
}

@ -678,6 +678,15 @@
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
<span>运行日志</span>
</a>
<div class="nav-divider"></div>
<a class="nav-item" href="/futures-analysis" target="_blank">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3v18h18"/><path d="M18 17V9"/><path d="M13 17V5"/><path d="M8 17v-3"/></svg>
<span>期货智析</span>
</a>
<a class="nav-item" href="/ai-config" target="_blank">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
<span>AI配置</span>
</a>
</nav>
</aside>

Loading…
Cancel
Save