Compare commits

..

18 Commits

29
.gitignore vendored

@ -0,0 +1,29 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
*.egg-info/
dist/
build/
# Virtual environments
venv/
.venv/
# Logs
*.log
data/collector.log
# IDE
.vscode/
.idea/
# OS
.DS_Store
Thumbs.db
# Cache
cache/*.pkl

@ -0,0 +1,185 @@
# Design System Inspired by Revolut
## 1. Visual Theme & Atmosphere
Revolut's website is fintech confidence distilled into pixels — a design system that communicates "your money is in capable hands" through massive typography, generous whitespace, and a disciplined neutral palette. The visual language is built on Aeonik Pro, a geometric grotesque that creates billboard-scale headlines at 136px with weight 500 and aggressive negative tracking (-2.72px). This isn't subtle branding; it's fintech at stadium scale.
The color system is built on a comprehensive `--rui-*` (Revolut UI) token architecture with semantic naming for every state: danger (`#e23b4a`), warning (`#ec7e00`), teal (`#00a87e`), blue (`#494fdf`), deep-pink (`#e61e49`), and more. But the marketing surface itself is remarkably restrained — near-black (`#191c1f`) and pure white (`#ffffff`) dominate, with the colorful semantic tokens reserved for the product interface, not the marketing page.
What distinguishes Revolut is its pill-everything button system. Every button uses 9999px radius — primary dark (`#191c1f`), secondary light (`#f4f4f4`), outlined (`transparent + 2px solid`), and ghost on dark (`rgba(244,244,244,0.1) + 2px solid`). The padding is generous (14px 32px34px), creating large, confident touch targets. Combined with Inter for body text at various weights and positive letter-spacing (0.16px0.24px), the result is a design that feels both premium and accessible — banking for the modern era.
**Key Characteristics:**
- Aeonik Pro display at 136px weight 500 — billboard-scale fintech headlines
- Near-black (`#191c1f`) + white binary with comprehensive `--rui-*` semantic tokens
- Universal pill buttons (9999px radius) with generous padding (14px 32px)
- Inter for body text with positive letter-spacing (0.16px0.24px)
- Rich semantic color system: blue, teal, pink, yellow, green, brown, danger, warning
- Zero shadows detected — depth through color contrast only
- Tight display line-heights (1.00) with relaxed body (1.501.56)
## 2. Color Palette & Roles
### Primary
- **Revolut Dark** (`#191c1f`): Primary dark surface, button background, near-black text
- **Pure White** (`#ffffff`): `--rui-color-action-label`, primary light surface
- **Light Surface** (`#f4f4f4`): Secondary button background, subtle surface
### Brand / Interactive
- **Revolut Blue** (`#494fdf`): `--rui-color-blue`, primary brand blue
- **Action Blue** (`#4f55f1`): `--rui-color-action-photo-header-text`, header accent
- **Blue Text** (`#376cd5`): `--website-color-blue-text`, link blue
### Semantic
- **Danger Red** (`#e23b4a`): `--rui-color-danger`, error/destructive
- **Deep Pink** (`#e61e49`): `--rui-color-deep-pink`, critical accent
- **Warning Orange** (`#ec7e00`): `--rui-color-warning`, warning states
- **Yellow** (`#b09000`): `--rui-color-yellow`, attention
- **Teal** (`#00a87e`): `--rui-color-teal`, success/positive
- **Light Green** (`#428619`): `--rui-color-light-green`, secondary success
- **Green Text** (`#006400`): `--website-color-green-text`, green text
- **Light Blue** (`#007bc2`): `--rui-color-light-blue`, informational
- **Brown** (`#936d62`): `--rui-color-brown`, warm neutral accent
- **Red Text** (`#8b0000`): `--website-color-red-text`, dark red text
### Neutral Scale
- **Mid Slate** (`#505a63`): Secondary text
- **Cool Gray** (`#8d969e`): Muted text, tertiary
- **Gray Tone** (`#c9c9cd`): `--rui-color-grey-tone-20`, borders/dividers
## 3. Typography Rules
### Font Families
- **Display**: `Aeonik Pro` — geometric grotesque, no detected fallbacks
- **Body / UI**: `Inter` — standard system sans
- **Fallback**: `Arial` for specific button contexts
### Hierarchy
| Role | Font | Size | Weight | Line Height | Letter Spacing | Notes |
|------|------|------|--------|-------------|----------------|-------|
| Display Mega | Aeonik Pro | 136px (8.50rem) | 500 | 1.00 (tight) | -2.72px | Stadium-scale hero |
| Display Hero | Aeonik Pro | 80px (5.00rem) | 500 | 1.00 (tight) | -0.8px | Primary hero |
| Section Heading | Aeonik Pro | 48px (3.00rem) | 500 | 1.21 (tight) | -0.48px | Feature sections |
| Sub-heading | Aeonik Pro | 40px (2.50rem) | 500 | 1.20 (tight) | -0.4px | Sub-sections |
| Card Title | Aeonik Pro | 32px (2.00rem) | 500 | 1.19 (tight) | -0.32px | Card headings |
| Feature Title | Aeonik Pro | 24px (1.50rem) | 400 | 1.33 | normal | Light headings |
| Nav / UI | Aeonik Pro | 20px (1.25rem) | 500 | 1.40 | normal | Navigation, buttons |
| Body Large | Inter | 18px (1.13rem) | 400 | 1.56 | -0.09px | Introductions |
| Body | Inter | 16px (1.00rem) | 400 | 1.50 | 0.24px | Standard reading |
| Body Semibold | Inter | 16px (1.00rem) | 600 | 1.50 | 0.16px | Emphasized body |
| Body Bold Link | Inter | 16px (1.00rem) | 700 | 1.50 | 0.24px | Bold links |
### Principles
- **Weight 500 as display default**: Aeonik Pro uses medium (500) for ALL headings — no bold. This creates authority through size and tracking, not weight.
- **Billboard tracking**: -2.72px at 136px is extremely compressed — text designed to be read at a glance, like airport signage.
- **Positive tracking on body**: Inter uses +0.16px to +0.24px, creating airy, well-spaced reading text that contrasts with the compressed headings.
## 4. Component Stylings
### Buttons
**Primary Dark Pill**
- Background: `#191c1f`
- Text: `#ffffff`
- Padding: 14px 32px
- Radius: 9999px (full pill)
- Hover: opacity 0.85
- Focus: `0 0 0 0.125rem` ring
**Secondary Light Pill**
- Background: `#f4f4f4`
- Text: `#000000`
- Padding: 14px 34px
- Radius: 9999px
- Hover: opacity 0.85
**Outlined Pill**
- Background: transparent
- Text: `#191c1f`
- Border: `2px solid #191c1f`
- Padding: 14px 32px
- Radius: 9999px
**Ghost on Dark**
- Background: `rgba(244, 244, 244, 0.1)`
- Text: `#f4f4f4`
- Border: `2px solid #f4f4f4`
- Padding: 14px 32px
- Radius: 9999px
### Cards & Containers
- Radius: 12px (small), 20px (cards)
- No shadows — flat surfaces with color contrast
- Dark and light section alternation
### Navigation
- Aeonik Pro 20px weight 500
- Clean header, hamburger toggle at 12px radius
- Pill CTAs right-aligned
## 5. Layout Principles
### Spacing System
- Base unit: 8px
- Scale: 4px, 6px, 8px, 14px, 16px, 20px, 24px, 32px, 40px, 48px, 80px, 88px, 120px
- Large section spacing: 80px120px
### Border Radius Scale
- Standard (12px): Navigation, small buttons
- Card (20px): Feature cards
- Pill (9999px): All buttons
## 6. Depth & Elevation
| Level | Treatment | Use |
|-------|-----------|-----|
| Flat (Level 0) | No shadow | Everything — Revolut uses zero shadows |
| Focus | `0 0 0 0.125rem` ring | Accessibility focus |
**Shadow Philosophy**: Revolut uses ZERO shadows. Depth comes entirely from the dark/light section contrast and the generous whitespace between elements.
## 7. Do's and Don'ts
### Do
- Use Aeonik Pro weight 500 for all display headings
- Apply 9999px radius to all buttons — pill shape is universal
- Use generous button padding (14px 32px)
- Keep the palette to near-black + white for marketing surfaces
- Apply positive letter-spacing on Inter body text
### Don't
- Don't use shadows — Revolut is flat by design
- Don't use bold (700) for Aeonik Pro headings — 500 is the weight
- Don't use small buttons — the generous padding is intentional
- Don't apply semantic colors to marketing surfaces — they're for the product
## 8. Responsive Behavior
### Breakpoints
| Name | Width | Key Changes |
|------|-------|-------------|
| Mobile Small | <400px | Compact, single column |
| Mobile | 400720px | Standard mobile |
| Tablet | 7201024px | 2-column layouts |
| Desktop | 10241280px | Standard desktop |
| Large | 12801920px | Full layout |
## 9. Agent Prompt Guide
### Quick Color Reference
- Dark: Revolut Dark (`#191c1f`)
- Light: White (`#ffffff`)
- Surface: Light (`#f4f4f4`)
- Blue: Revolut Blue (`#494fdf`)
- Danger: Red (`#e23b4a`)
- Success: Teal (`#00a87e`)
### Example Component Prompts
- "Create a hero: white background. Headline at 136px Aeonik Pro weight 500, line-height 1.00, letter-spacing -2.72px, #191c1f text. Dark pill CTA (#191c1f, 9999px, 14px 32px). Outlined pill secondary (transparent, 2px solid #191c1f)."
- "Build a pill button: #191c1f background, white text, 9999px radius, 14px 32px padding, 20px Aeonik Pro weight 500. Hover: opacity 0.85."
### Iteration Guide
1. Aeonik Pro 500 for headings — never bold
2. All buttons are pills (9999px) with generous padding
3. Zero shadows — flat is the Revolut identity
4. Near-black + white for marketing, semantic colors for product

@ -0,0 +1,40 @@
"""
期货智析数据库 - 独立存储
"""
from pathlib import Path
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base
from datetime import datetime
# 数据目录
DATA_DIR = Path(__file__).resolve().parent.parent / "data"
DATA_DIR.mkdir(parents=True, exist_ok=True)
ANALYSIS_DB_PATH = DATA_DIR / "futures_analysis.db"
analysis_engine = create_engine(
f"sqlite:///{ANALYSIS_DB_PATH}",
connect_args={"check_same_thread": False},
pool_pre_ping=True,
)
AnalysisSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=analysis_engine)
AnalysisBase = declarative_base()
def get_analysis_db():
"""获取期货智析数据库会话"""
db = AnalysisSessionLocal()
try:
yield db
finally:
db.close()
def init_analysis_db():
"""初始化期货智析数据库表"""
# 确保导入所有模型类,使其注册到 AnalysisBase
from app import analysis_models
# 直接导入 analysis_models 模块中的所有类
from app.analysis_models import FuturesAnalysis, WatchedSymbol, AIModelConfig, AnalysisSettings, AIAnalysisCache
AnalysisBase.metadata.create_all(bind=analysis_engine)

@ -0,0 +1,102 @@
"""
期货智析数据模型
"""
from datetime import datetime
from sqlalchemy import Column, String, Integer, Float, Text, DateTime, Boolean, Index, UniqueConstraint, JSON
from app.analysis_db import AnalysisBase
class FuturesAnalysis(AnalysisBase):
"""期货分析报告表"""
__tablename__ = "futures_analysis"
id = Column(Integer, primary_key=True, autoincrement=True)
symbol = Column(String(32), nullable=False, index=True, comment="品种合约代码")
analysis_time = Column(DateTime, nullable=False, default=datetime.now, index=True, comment="分析时间")
period = Column(String(16), nullable=False, default="15min", comment="分析周期")
# 分析结果
suggestion = Column(String(32), nullable=True, comment="交易建议: 逢低做多/逢高做空/观望等待")
suggestion_type = Column(String(16), nullable=True, comment="建议类型: up/down/neutral")
entry_price = Column(Float, nullable=True, comment="建议入场价")
target_price = Column(Float, nullable=True, comment="目标价位")
stop_loss = Column(Float, nullable=True, comment="止损价位")
risk_level = Column(String(16), nullable=True, comment="风险等级: 低/中/高")
# 技术指标
macd_signal = Column(String(16), nullable=True, comment="MACD信号")
rsi_value = Column(Float, nullable=True, comment="RSI值")
boll_signal = Column(String(16), nullable=True, comment="布林带信号")
kdj_signal = Column(String(16), nullable=True, comment="KDJ信号")
# 趋势评分
trend_score = Column(Integer, nullable=True, comment="趋势评分 0-100")
success_rate = Column(Float, nullable=True, comment="交易成功率")
# 关键点位
resistance_levels = Column(JSON, nullable=True, comment="压力位列表")
support_levels = Column(JSON, nullable=True, comment="支撑位列表")
# 多周期趋势
period_trends = Column(JSON, nullable=True, comment="各周期趋势")
def __repr__(self):
return f"<FuturesAnalysis {self.symbol} {self.analysis_time}>"
class WatchedSymbol(AnalysisBase):
"""用户关注品种表"""
__tablename__ = "watched_symbols"
id = Column(Integer, primary_key=True, autoincrement=True)
symbol = Column(String(32), nullable=False, unique=True, comment="品种合约代码")
name = Column(String(64), nullable=True, comment="品种名称")
note = Column(Text, nullable=True, comment="备注")
created_at = Column(DateTime, nullable=False, default=datetime.now)
updated_at = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now)
def __repr__(self):
return f"<WatchedSymbol {self.symbol}>"
class AIModelConfig(AnalysisBase):
"""AI模型配置表"""
__tablename__ = "ai_model_configs"
id = Column(Integer, primary_key=True, autoincrement=True)
provider = Column(String(32), nullable=False, comment="AI提供商: openai/anthropic/google等")
model_name = Column(String(64), nullable=False, comment="模型名称")
api_key = Column(String(256), nullable=False, comment="API密钥")
api_base = Column(String(256), nullable=True, comment="API基础URL")
model_id = Column(String(64), nullable=True, comment="模型ID")
temperature = Column(Float, nullable=True, default=0.7, comment="温度参数")
max_tokens = Column(Integer, nullable=True, default=2000, comment="最大输出token")
enabled = Column(Boolean, nullable=False, default=True, comment="是否启用")
is_active = Column(Boolean, nullable=False, default=False, comment="是否为当前活跃模型")
created_at = Column(DateTime, nullable=False, default=datetime.now)
updated_at = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now)
def __repr__(self):
return f"<AIModelConfig {self.provider} {self.model_name}>"
class AnalysisSettings(AnalysisBase):
"""分析设置表(单例配置)"""
__tablename__ = "analysis_settings"
id = Column(Integer, primary_key=True, autoincrement=True)
key = Column(String(64), nullable=False, unique=True, comment="配置键")
value = Column(JSON, nullable=False, comment="配置值")
updated_at = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now)
def __repr__(self):
return f"<AnalysisSettings {self.key}>"
class AIAnalysisCache(AnalysisBase):
"""AI分析缓存表"""
__tablename__ = "ai_analysis_cache"
id = Column(Integer, primary_key=True, autoincrement=True)
symbol = Column(String(32), nullable=False, index=True, comment="品种合约代码")
analysis_data = Column(JSON, nullable=False, comment="AI分析结果数据")
kline_timestamp = Column(DateTime, nullable=True, comment="分析时K线数据的时间戳")
created_at = Column(DateTime, nullable=False, default=datetime.now, index=True, comment="分析时间")
def __repr__(self):
return f"<AIAnalysisCache {self.symbol} {self.created_at}>"

@ -0,0 +1,189 @@
"""
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": "aliyun_coding",
"name": "阿里云通义灵码",
"api_base": "https://dashscope.aliyuncs.com/compatible-mode/v1",
"models": ["qwen-coder-plus", "qwen-coder-turbo"]
},
{
"id": "bailian",
"name": "阿里百炼",
"api_base": "https://coding.dashscope.aliyuncs.com/v1",
"models": ["qwen3.6-plus", "qwen3.5-plus", "qwen3-max", "qwen3-coder-plus", "MiniMax-M2.5", "glm-4.7", "kimi-k2.5"]
},
{
"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,956 @@
"""
期货智析接口 - 提供期货分析数据
"""
import json
import logging
from pathlib import Path
from typing import Optional
import threading
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
from sqlalchemy.orm import Session
from app.database import get_db
from app.analysis_db import get_analysis_db
from app.analysis_models import FuturesAnalysis, WatchedSymbol, AIModelConfig, AnalysisSettings, AIAnalysisCache
from app.services.cache import get_cached_data, get_latest_cached, save_market_data
from app.services.collector import fetch_symbol_data
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(2 * ((high_price + low_price + close_price) / 3) - low_price, 2),
"support": round(2 * ((high_price + low_price + close_price) / 3) - high_price, 2),
"open": open_price,
"high": high_price,
"low": low_price,
"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
# Pivot Point 公式计算关键点位
pp = (high_price + low_price + close_price) / 3
r1 = round(2 * pp - low_price, 2)
r2 = round(pp + (high_price - low_price), 2)
s1 = round(2 * pp - high_price, 2)
s2 = round(pp - (high_price - low_price), 2)
suggestion = _get_suggestion(close_price, open_price, change_pct)
suggestion_type = "up" if change >= 0 else "down"
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": r1 if change >= 0 else s1,
"stopLoss": s1 if change >= 0 else r1,
"riskLevel": "" if trend_score >= 80 else "" if trend_score >= 60 else "",
"macd": _calc_macd(all_candles),
"rsi": _calc_rsi(all_candles),
"boll": _calc_boll(all_candles),
"kdj": _calc_kdj(all_candles),
"resistances": [r1, r2],
"supports": [s1, s2],
"pivotPoint": round(pp, 2),
"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}"}
# ==================== 期货智析数据管理接口 ====================
@router.get("/analysis/history/{symbol}")
def get_analysis_history(symbol: str, limit: int = 10, adb: Session = Depends(get_analysis_db)):
"""获取品种历史分析记录"""
records = adb.query(FuturesAnalysis).filter(
FuturesAnalysis.symbol == symbol
).order_by(
FuturesAnalysis.analysis_time.desc()
).limit(limit).all()
return {
"success": True,
"data": [{
"id": r.id,
"symbol": r.symbol,
"analysis_time": r.analysis_time.isoformat(),
"suggestion": r.suggestion,
"suggestion_type": r.suggestion_type,
"trend_score": r.trend_score,
"entry_price": r.entry_price,
"target_price": r.target_price,
"stop_loss": r.stop_loss,
"risk_level": r.risk_level
} for r in records]
}
@router.post("/analysis/save")
def save_analysis_record(data: dict, adb: Session = Depends(get_analysis_db)):
"""保存分析记录到数据库"""
try:
record = FuturesAnalysis(
symbol=data.get("symbol"),
suggestion=data.get("suggestion"),
suggestion_type=data.get("suggestion_type"),
entry_price=data.get("entry_price"),
target_price=data.get("target_price"),
stop_loss=data.get("stop_loss"),
risk_level=data.get("risk_level"),
macd_signal=data.get("macd", {}).get("signal") if data.get("macd") else None,
rsi_value=data.get("rsi", {}).get("value") if data.get("rsi") else None,
boll_signal=data.get("boll", {}).get("signal") if data.get("boll") else None,
kdj_signal=data.get("kdj", {}).get("signal") if data.get("kdj") else None,
trend_score=data.get("trend_score"),
success_rate=data.get("success_rate"),
resistance_levels=data.get("resistances"),
support_levels=data.get("supports"),
period_trends=data.get("periodConsistency")
)
adb.add(record)
adb.commit()
return {"success": True, "message": "分析记录已保存", "id": record.id}
except Exception as e:
adb.rollback()
logger.error(f"保存分析记录失败: {e}")
return {"success": False, "message": str(e)}
# ==================== 关注品种管理 ====================
@router.get("/watched")
def get_watched_symbols(adb: Session = Depends(get_analysis_db)):
"""获取关注的品种列表"""
symbols = adb.query(WatchedSymbol).order_by(WatchedSymbol.created_at.desc()).all()
return {
"success": True,
"data": [{
"id": s.id,
"symbol": s.symbol,
"name": s.name,
"note": s.note,
"created_at": s.created_at.isoformat()
} for s in symbols]
}
@router.post("/watched")
def add_watched_symbol(data: dict, adb: Session = Depends(get_analysis_db)):
"""添加关注品种"""
try:
symbol = data.get("symbol")
existing = adb.query(WatchedSymbol).filter(WatchedSymbol.symbol == symbol).first()
if existing:
return {"success": False, "message": "该品种已关注"}
new_symbol = WatchedSymbol(
symbol=symbol,
name=data.get("name"),
note=data.get("note")
)
adb.add(new_symbol)
adb.commit()
return {"success": True, "message": "已添加关注", "id": new_symbol.id}
except Exception as e:
adb.rollback()
return {"success": False, "message": str(e)}
@router.delete("/watched/{symbol}")
def remove_watched_symbol(symbol: str, adb: Session = Depends(get_analysis_db)):
"""取消关注品种"""
try:
record = adb.query(WatchedSymbol).filter(WatchedSymbol.symbol == symbol).first()
if not record:
return {"success": False, "message": "未找到该品种"}
adb.delete(record)
adb.commit()
return {"success": True, "message": "已取消关注"}
except Exception as e:
adb.rollback()
return {"success": False, "message": str(e)}
# ==================== AI模型配置管理 ====================
@router.get("/ai-models")
def get_ai_models(adb: Session = Depends(get_analysis_db)):
"""获取AI模型配置列表"""
models = adb.query(AIModelConfig).order_by(AIModelConfig.created_at.desc()).all()
settings = adb.query(AnalysisSettings).filter(
AnalysisSettings.key == "analysis_settings"
).first()
return {
"success": True,
"data": {
"models": [{
"id": m.id,
"provider": m.provider,
"model_name": m.model_name,
"api_base": m.api_base,
"model_id": m.model_id,
"temperature": m.temperature,
"max_tokens": m.max_tokens,
"enabled": m.enabled,
"is_active": m.is_active,
"created_at": m.created_at.isoformat()
} for m in models],
"analysis_settings": settings.value if settings else {
"enable_technical_analysis": True,
"enable_fundamental_analysis": False,
"enable_sentiment_analysis": False,
"risk_tolerance": "medium",
"max_position_pct": 10
}
}
}
@router.post("/ai-models")
def save_ai_model(data: dict, adb: Session = Depends(get_analysis_db)):
"""保存AI模型配置"""
try:
if data.get("action") == "save_settings":
settings = adb.query(AnalysisSettings).filter(
AnalysisSettings.key == "analysis_settings"
).first()
if settings:
settings.value = data.get("settings", {})
else:
settings = AnalysisSettings(
key="analysis_settings",
value=data.get("settings", {})
)
adb.add(settings)
adb.commit()
return {"success": True, "message": "分析设置已保存"}
model_data = data.get("model", {})
model = AIModelConfig(
provider=model_data.get("provider", "custom"),
model_name=model_data.get("model_name", ""),
api_key=model_data.get("api_key", ""),
api_base=model_data.get("api_base"),
model_id=model_data.get("model_id"),
temperature=model_data.get("temperature", 0.7),
max_tokens=model_data.get("max_tokens", 2000),
enabled=model_data.get("enabled", True),
is_active=model_data.get("is_active", False)
)
if model.is_active:
adb.query(AIModelConfig).update({"is_active": False})
adb.add(model)
adb.commit()
return {"success": True, "message": "AI模型已保存", "id": model.id}
except Exception as e:
adb.rollback()
return {"success": False, "message": str(e)}
@router.put("/ai-models/{model_id}")
def update_ai_model(model_id: int, data: dict, adb: Session = Depends(get_analysis_db)):
"""更新AI模型配置"""
try:
model = adb.query(AIModelConfig).filter(AIModelConfig.id == model_id).first()
if not model:
return {"success": False, "message": "模型不存在"}
if "is_active" in data and data["is_active"]:
adb.query(AIModelConfig).update({"is_active": False})
model.is_active = True
else:
for key, value in data.items():
if hasattr(model, key):
setattr(model, key, value)
adb.commit()
return {"success": True, "message": "模型已更新"}
except Exception as e:
adb.rollback()
return {"success": False, "message": str(e)}
@router.delete("/ai-models/{model_id}")
def delete_ai_model(model_id: int, adb: Session = Depends(get_analysis_db)):
"""删除AI模型配置"""
try:
model = adb.query(AIModelConfig).filter(AIModelConfig.id == model_id).first()
if not model:
return {"success": False, "message": "模型不存在"}
adb.delete(model)
adb.commit()
return {"success": True, "message": "模型已删除"}
except Exception as e:
adb.rollback()
return {"success": False, "message": str(e)}
# ==================== 数据刷新接口 ====================
from app.services.cache import needs_refresh, get_symbol_timestamp
from app.services.ai_analysis import AIFuturesAnalyzer
refresh_lock = threading.Lock()
refresh_status = {"running": False, "progress": 0, "total": 0, "message": ""}
REFRESH_THRESHOLD = 300 # 5分钟阈值
def _refresh_single_symbol_sync(db: Session, symbol: str) -> dict:
"""同步刷新单个品种数据(会等待采集完成)"""
try:
# 先检查是否需要刷新
if not needs_refresh(db, symbol, "futures", REFRESH_THRESHOLD):
last_refresh = get_symbol_timestamp(db, symbol, "futures")
return {
"success": True,
"message": f"{symbol} 数据仍然新鲜,无需刷新",
"last_refresh": last_refresh.isoformat() if last_refresh else None,
"refreshed": False
}
# 需要刷新,执行采集
logger.info(f"开始刷新 {symbol} 数据...")
result = fetch_symbol_data(symbol, "futures")
if result.get("timeframes"):
save_market_data(db, symbol, result)
logger.info(f"{symbol} 数据刷新完成")
return {
"success": True,
"message": f"{symbol} 数据已更新",
"refreshed": True
}
return {"success": False, "message": f"{symbol} 未获取到数据", "refreshed": False}
except Exception as e:
logger.error(f"刷新 {symbol} 失败: {e}")
return {"success": False, "message": f"{symbol} 刷新失败: {str(e)}", "refreshed": False}
@router.post("/refresh/{symbol}")
def refresh_single_symbol_api(symbol: str, db: Session = Depends(get_db)):
"""刷新单个品种合约数据(同步执行,检查时间戳)"""
if refresh_lock.locked():
return {"success": False, "message": "数据刷新中,请稍后再试"}
try:
refresh_lock.acquire()
result = _refresh_single_symbol_sync(db, symbol)
return result
finally:
refresh_lock.release()
@router.post("/refresh-all")
def refresh_all_symbols_api(background_tasks: BackgroundTasks):
"""刷新所有品种合约数据(异步执行)"""
global refresh_status
if refresh_lock.locked():
return {"success": False, "message": "数据刷新中,请稍后再试"}
# 从配置加载所有品种
config = _load_symbols_config()
futures_config = config.get("futures", {})
symbols = list(futures_config.values())
def refresh_all_task():
global refresh_status
# 在后台任务内部创建新的数据库会话
local_db = next(get_db())
try:
with refresh_lock:
refresh_status = {"running": True, "progress": 0, "total": len(symbols), "message": "开始刷新..."}
for i, symbol in enumerate(symbols):
refresh_status["message"] = f"正在刷新 {symbol} ({i + 1}/{len(symbols)})"
refresh_status["progress"] = i + 1
_refresh_single_symbol_sync(local_db, symbol)
with refresh_lock:
refresh_status = {"running": False, "progress": len(symbols), "total": len(symbols), "message": "全部刷新完成"}
finally:
local_db.close()
background_tasks.add_task(refresh_all_task)
return {"success": True, "message": "开始刷新所有品种数据...", "count": len(symbols)}
@router.get("/refresh-status")
def get_refresh_status():
"""获取刷新状态"""
return {"success": True, "data": refresh_status}
# ==================== AI智能分析接口 ====================
@router.post("/ai-analysis/{symbol}")
def run_ai_analysis(symbol: str, db: Session = Depends(get_db), analysis_db: Session = Depends(get_analysis_db)):
"""执行AI智能分析"""
try:
analyzer = AIFuturesAnalyzer(db, analysis_db)
result = analyzer.analyze(symbol)
if result.get("success"):
return {
"success": True,
"data": result["data"]
}
else:
return {
"success": False,
"error": result.get("error", "AI分析失败")
}
except Exception as e:
logger.error(f"AI分析失败: {e}")
return {
"success": False,
"error": f"AI分析失败: {str(e)}"
}
@router.get("/ai-analysis/{symbol}")
def get_ai_analysis(symbol: str, force_refresh: bool = False, db: Session = Depends(get_db), analysis_db: Session = Depends(get_analysis_db)):
"""获取AI分析结果智能判断是否需要重新分析"""
try:
analyzer = AIFuturesAnalyzer(db, analysis_db)
if force_refresh:
logger.info(f"强制刷新: {symbol}")
result = analyzer.analyze(symbol)
if result.get("success"):
return {
"success": True,
"data": result["data"],
"is_cached": False
}
else:
return {
"success": False,
"error": result.get("error", "AI分析失败")
}
# 获取最新缓存
cache = analyzer.get_latest_cache(symbol)
if cache:
# 智能判断是否需要重新分析
if analyzer.should_reanalyze(symbol, cache):
logger.info(f"检测到数据变化或超时,自动重新分析: {symbol}")
result = analyzer.analyze(symbol)
if result.get("success"):
return {
"success": True,
"data": result["data"],
"is_cached": False
}
else:
# 如果重新分析失败,返回旧缓存
logger.warning(f"重新分析失败,返回旧缓存: {symbol}")
return {
"success": True,
"data": {
"id": cache.id,
"symbol": cache.symbol,
"analysis_time": cache.created_at.isoformat(),
"result": cache.analysis_data
},
"is_cached": True,
"warning": "分析数据可能不是最新的"
}
# 返回缓存数据
return {
"success": True,
"data": {
"id": cache.id,
"symbol": cache.symbol,
"analysis_time": cache.created_at.isoformat(),
"result": cache.analysis_data
},
"is_cached": True
}
# 没有缓存,执行分析
result = analyzer.analyze(symbol)
if result.get("success"):
return {
"success": True,
"data": result["data"],
"is_cached": False
}
else:
return {
"success": False,
"error": result.get("error", "未找到分析结果")
}
except Exception as e:
logger.error(f"获取AI分析结果失败: {e}")
return {
"success": False,
"error": f"获取AI分析失败: {str(e)}"
}
@router.get("/ai-analysis/{symbol}/history")
def get_ai_analysis_history(symbol: str, limit: int = 20, analysis_db: Session = Depends(get_analysis_db)):
"""获取AI分析历史记录"""
try:
records = analysis_db.query(AIAnalysisCache).filter(
AIAnalysisCache.symbol == symbol
).order_by(
AIAnalysisCache.created_at.desc()
).limit(limit).all()
return {
"success": True,
"data": [{
"id": r.id,
"symbol": r.symbol,
"analysis_time": r.created_at.isoformat(),
"analysis_data": r.analysis_data
} for r in records]
}
except Exception as e:
logger.error(f"获取AI分析历史失败: {e}")
return {
"success": False,
"error": f"获取历史记录失败: {str(e)}"
}
@router.get("/ai-analysis/history/{record_id}")
def get_ai_analysis_detail(record_id: int, analysis_db: Session = Depends(get_analysis_db)):
"""获取单条AI分析记录详情"""
try:
record = analysis_db.query(AIAnalysisCache).filter(
AIAnalysisCache.id == record_id
).first()
if not record:
return {
"success": False,
"error": "记录不存在"
}
return {
"success": True,
"data": {
"id": record.id,
"symbol": record.symbol,
"analysis_time": record.created_at.isoformat(),
"analysis_data": record.analysis_data
}
}
except Exception as e:
logger.error(f"获取AI分析详情失败: {e}")
return {
"success": False,
"error": f"获取记录详情失败: {str(e)}"
}

@ -12,7 +12,7 @@ from fastapi.responses import FileResponse
from app.database import engine, Base from app.database import engine, Base
from app.config import HOST, PORT, LOG_LEVEL 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 from app.services.scheduler import start_scheduler, stop_scheduler
# 配置日志 # 配置日志
@ -29,6 +29,9 @@ async def lifespan(app: FastAPI):
# 启动时:建表 + 启动调度器 # 启动时:建表 + 启动调度器
logger.info("创建数据库表...") logger.info("创建数据库表...")
Base.metadata.create_all(bind=engine) Base.metadata.create_all(bind=engine)
from app.analysis_db import init_analysis_db
init_analysis_db()
logger.info("期货智析数据库初始化完成")
logger.info("启动定时调度器...") logger.info("启动定时调度器...")
start_scheduler() start_scheduler()
@ -87,6 +90,20 @@ def ui_page():
app.include_router(data.router, prefix="/api/v1") app.include_router(data.router, prefix="/api/v1")
app.include_router(tasks.router, prefix="/api/v1") app.include_router(tasks.router, prefix="/api/v1")
app.include_router(config.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") @app.get("/api/v1/health")

@ -28,6 +28,20 @@ class MarketData(Base):
return f"<MarketData {self.symbol} {self.period} candles={self.candle_count}>" return f"<MarketData {self.symbol} {self.period} candles={self.candle_count}>"
class SymbolTimestamp(Base):
"""合约数据时间戳表 - 记录每个合约的最新数据时间"""
__tablename__ = "symbol_timestamps"
id = Column(Integer, primary_key=True, autoincrement=True)
symbol = Column(String(32), nullable=False, unique=True, index=True, comment="品种合约代码")
data_type = Column(String(16), nullable=False, default="futures", comment="数据类型")
last_refresh_at = Column(DateTime, nullable=False, default=datetime.now, comment="最后刷新时间")
refresh_count = Column(Integer, default=0, comment="刷新次数")
def __repr__(self):
return f"<SymbolTimestamp {self.symbol} last_refresh={self.last_refresh_at}>"
class ScheduledTask(Base): class ScheduledTask(Base):
"""定时任务配置表""" """定时任务配置表"""
__tablename__ = "scheduled_tasks" __tablename__ = "scheduled_tasks"

@ -0,0 +1,517 @@
"""
AI分析服务 - 期货四维联合分析
"""
import json
import re
import logging
from datetime import datetime
from typing import Dict, List, Optional
from sqlalchemy.orm import Session
from app.analysis_models import AIAnalysisCache
from app.services.cache import get_cached_data, get_latest_cached
from pathlib import Path
CONFIG_DIR = Path(__file__).resolve().parent.parent.parent / "config"
AI_CONFIG_FILE = CONFIG_DIR / "ai_config.json"
logger = logging.getLogger(__name__)
class AIAnalysisPrompt:
"""AI分析提示词管理器"""
SYSTEM_PROMPT = """你是一位拥有20年实战经验的资深金融交易分析师精通A股市场与商品期货的技术分析。
你的核心使命是基于提供的K线数据执行四维联合判断分析法(4D-XV)并提供包含风控红线审查的客观交易策略
你的分析必须遵循以下原则
1. 数据驱动所有结论必须基于输入的JSON数据严禁凭空捏造
2. 四维共振任何交易建议必须经过MACD趋势成交量资金KDJ时机多周期方向的交叉验证
3. 红线否决如果数据触发17条交易红线必须直接给出禁止交易止损的建议
4. 客观中立不使用绝对化表述提供情景预案概率估算
5. 动态切换遇到关键位放量突破时立即切换右侧思维不逆势死扛"""
ANALYSIS_TEMPLATE = """请严格按照以下JSON格式输出分析结果不要输出任何其他内容
{
"summary": "一句话总结当前市场状态",
"four_dimensional": {
"60min": {
"macd": {"trend": "up/down/neutral", "histogram": "放大/缩小/背离", "position": "零轴上/下"},
"volume": {"status": "放量上涨/缩量回调/趋势量能/拐点量能", "ratio": 1.5},
"kdj": {"k": 85, "d": 80, "j": 95, "status": "超买/超卖/中性", "signal": "金叉/死叉/钝化"},
"conclusion": "定大势结论"
},
"30min": {
"macd": {"trend": "up/down/neutral", "histogram": "放大/缩小/背离", "position": "零轴上/下"},
"volume": {"status": "放量/缩量/正常", "ratio": 1.5},
"kdj": {"k": 50, "d": 45, "j": 60, "status": "中性", "signal": "金叉/死叉"},
"conclusion": "找拐点结论"
},
"15min": {
"macd": {"trend": "up/down/neutral", "histogram": "放大/缩小/背离", "position": "零轴上/下"},
"volume": {"status": "放量/缩量/正常", "ratio": 2.0},
"kdj": {"k": 30, "d": 25, "j": 40, "status": "超卖", "signal": "金叉/死叉"},
"conclusion": "择入场结论"
},
"5min": {
"macd": {"trend": "up/down/neutral", "histogram": "放大/缩小/背离", "position": "零轴上/下"},
"volume": {"status": "放量/缩量/正常", "ratio": 1.8},
"kdj": {"k": 50, "d": 45, "j": 60, "status": "超买/超卖/中性", "signal": "金叉/死叉"},
"conclusion": "精确定位入场点"
}
},
"kdj_diagnosis": {
"current_status": "超买/超卖/中性区域",
"divergence": "是否存在顶/底背离",
"paralysis": "是否钝化(持续>6根K线",
"recommendation": "KDJ使用建议"
},
"pivot_points": {
"r2": 7500,
"r1": 7350,
"pp": 7200,
"s1": 7050,
"s2": 6900,
"validation": {
"test_count": 3,
"volume_confirmed": true,
"multi_period_resonance": true,
"breakback_confirmed": false
}
},
"red_lines_check": {
"passed": true,
"violated": [],
"warnings": ["暂无红线警告"]
},
"discipline_score": {
"total": 9,
"max": 11,
"details": {
"trend": true,
"position": true,
"signal": true,
"risk": true,
"mindset": true
}
},
"trading_suggestion": {
"direction": "做多/做空/观望",
"confidence": 75,
"entry_range": {"min": 7050, "max": 7100},
"stop_loss": 6950,
"take_profit": [{"price": 7200, "ratio": 50}, {"price": 7350, "ratio": 30}, {"price": 7500, "ratio": 20}],
"position_size": "轻仓/半仓/重仓",
"reason": "做多理由"
},
"scenario_plans": {
"breakthrough": {"probability": 35, "action": "放量突破关键位,跟随右侧思维"},
"consolidation": {"probability": 40, "action": "R1-S1区间内高抛低吸"},
"reversal": {"probability": 15, "action": "MACD顶/底背离+量能不足,立即止损反手"},
"news_impact": {"probability": 10, "action": "减仓50%规避不确定性"}
},
"risk_warnings": [
"技术指标具有滞后性,历史表现不代表未来",
"需结合基本面和市场情绪综合判断"
],
"experience_lessons": [
"警惕缩量创新高,可能是诱多信号",
"KDJ超买钝化中不宜逆势做空"
]
}"""
@classmethod
def build_prompt(cls, symbol: str, data: Dict) -> str:
"""构建完整的AI分析提示词"""
prompt = f"""{cls.SYSTEM_PROMPT}
现在请分析以下期货品种的K线数据
## 品种信息
- 合约代码{symbol}
- 当前价格{data.get('current_price', 'N/A')}
## 多周期K线数据
```json
{json.dumps(data, ensure_ascii=False, indent=2)}
```
{cls.ANALYSIS_TEMPLATE}"""
return prompt
class AIFuturesAnalyzer:
"""AI期货分析器"""
def __init__(self, db: Session, analysis_db: Session = None):
self.db = db
self.analysis_db = analysis_db or db
def get_active_model(self) -> Optional[Dict]:
"""获取当前激活的AI模型配置"""
try:
if not AI_CONFIG_FILE.exists():
return None
with open(AI_CONFIG_FILE, "r", encoding="utf-8") as f:
config = json.load(f)
models = config.get("models", [])
active_model_name = config.get("active_model")
if active_model_name:
for model in models:
if model.get("model_name") == active_model_name and model.get("enabled", True):
return model
for model in models:
if model.get("enabled", True):
logger.warning(f"未找到匹配的激活模型,使用第一个启用的模型: {model.get('model_name')}")
return model
return None
except Exception as e:
logger.error(f"加载AI配置失败: {e}")
return None
def prepare_multi_period_data(self, symbol: str) -> Optional[Dict]:
"""准备多周期数据用于AI分析"""
cached_data = get_cached_data(
self.db,
symbol,
"futures",
["5min", "15min", "30min", "60min", "daily"]
)
if not cached_data or not cached_data.get("timeframes"):
return None
timeframes = cached_data.get("timeframes", {})
current_price = cached_data.get("current_price")
result = {
"symbol": symbol,
"current_price": current_price,
"timeframes": {}
}
for period_name, db_period in [("5min", "5min"), ("15min", "15min"),
("30min", "30min"), ("60min", "60min"),
("daily", "daily")]:
if period_name in timeframes and timeframes[period_name]:
candles = timeframes[period_name]
if len(candles) >= 20:
result["timeframes"][period_name] = self._analyze_timeframe(candles, period_name)
return result
def _analyze_timeframe(self, candles: List[Dict], period: str) -> Dict:
"""分析单个周期的技术指标"""
if not candles or len(candles) < 20:
return {}
closes = [float(c.get("close", 0)) for c in candles]
highs = [float(c.get("high", 0)) for c in candles]
lows = [float(c.get("low", 0)) for c in candles]
volumes = [float(c.get("volume", 0)) for c in candles]
ma10 = sum(closes[-10:]) / 10 if len(closes) >= 10 else None
ma20 = sum(closes[-20:]) / 20 if len(closes) >= 20 else None
macd_data = self._calc_macd(closes)
kdj_data = self._calc_kdj(highs, lows, closes)
avg_volume = sum(volumes[-20:]) / 20 if len(volumes) >= 20 else 0
current_volume = volumes[-1] if volumes else 0
return {
"trend": "up" if closes[-1] > closes[0] else "down",
"ma10": round(ma10, 2) if ma10 else None,
"ma20": round(ma20, 2) if ma20 else None,
"macd_dif": round(macd_data["dif"], 4),
"macd_dea": round(macd_data["dea"], 4),
"macd_histogram": round(macd_data["histogram"], 4),
"kdj_k": kdj_data["k"],
"kdj_d": kdj_data["d"],
"kdj_j": kdj_data["j"],
"volume_avg": round(avg_volume, 2),
"volume_current": round(current_volume, 2),
"volume_ratio": round(current_volume / avg_volume, 2) if avg_volume > 0 else 1,
"candles": candles[-10:] if len(candles) > 10 else candles
}
def _calc_macd(self, closes: List[float]) -> Dict:
"""计算MACD指标"""
if len(closes) < 26:
return {"dif": 0, "dea": 0, "histogram": 0}
ema12 = self._calc_ema(closes, 12)
ema26 = self._calc_ema(closes, 26)
dif = ema12 - ema26
dea = self._calc_ema([dif] * len(closes), 9)
histogram = 2 * (dif - dea)
return {"dif": dif, "dea": dea, "histogram": histogram}
def _calc_ema(self, data: List[float], period: int) -> float:
"""计算EMA"""
if len(data) < period:
return sum(data) / len(data) if data else 0
multiplier = 2 / (period + 1)
ema = sum(data[:period]) / period
for i in range(period, len(data)):
ema = (data[i] - ema) * multiplier + ema
return ema
def _calc_kdj(self, highs: List[float], lows: List[float], closes: List[float]) -> Dict:
"""计算KDJ指标"""
if len(closes) < 9:
return {"k": 50, "d": 50, "j": 50}
period = 9
recent_highs = highs[-period:]
recent_lows = lows[-period:]
recent_closes = closes[-period:]
highest = max(recent_highs)
lowest = min(recent_lows)
current = recent_closes[-1]
if highest == lowest:
rsv = 50
else:
rsv = (current - lowest) / (highest - lowest) * 100
k = rsv * 2 / 3 + 50 / 3
d = k * 2 / 3 + 50 / 3
j = 3 * k - 2 * d
return {"k": round(k, 2), "d": round(d, 2), "j": round(j, 2)}
def call_ai_model(self, prompt: str, model: Dict) -> Optional[str]:
"""调用AI模型"""
try:
import requests
api_base = model.get("api_base", "https://api.openai.com/v1")
api_key = model.get("api_key", "")
model_id = model.get("model_id") or model.get("model_name", "")
logger.info(f"========== AI模型调用开始 ==========")
logger.info(f"API Base: {api_base}")
logger.info(f"API Key: {'已配置' if api_key else '未配置'} ({api_key[:10]}...)" if api_key else "API Key: 未配置")
logger.info(f"Model ID: {model_id}")
logger.info(f"Temperature: {model.get('temperature', 0.7)}")
logger.info(f"Max Tokens: {model.get('max_tokens', 2000)}")
if not api_key:
logger.error("API Key 未配置无法调用AI模型")
return None
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}"
}
payload = {
"model": model_id,
"messages": [
{"role": "user", "content": prompt}
],
"temperature": model.get("temperature", 0.7),
"max_tokens": model.get("max_tokens", 2000)
}
url = f"{api_base}/chat/completions"
logger.info(f"请求 URL: {url}")
logger.info(f"请求 Payload 大小: {len(str(payload))} 字符")
response = requests.post(
url,
headers=headers,
json=payload,
timeout=180 # 增加到180秒3分钟超时
)
logger.info(f"响应状态码: {response.status_code}")
if response.status_code == 200:
result = response.json()
content = result["choices"][0]["message"]["content"]
logger.info(f"AI响应成功内容长度: {len(content)} 字符")
logger.info(f"========== AI模型调用成功 ==========")
return content
else:
logger.error(f"AI模型调用失败:")
logger.error(f" 状态码: {response.status_code}")
logger.error(f" 响应内容: {response.text}")
logger.error(f"========== AI模型调用失败 ==========")
return None
except requests.exceptions.Timeout:
logger.error("AI模型调用超时超过60秒")
logger.error(f"========== AI模型调用失败 ==========")
return None
except requests.exceptions.ConnectionError as e:
logger.error(f"AI模型连接错误: {e}")
logger.error(f"========== AI模型调用失败 ==========")
return None
except requests.exceptions.RequestException as e:
logger.error(f"AI模型请求异常: {e}")
logger.error(f"========== AI模型调用失败 ==========")
return None
except Exception as e:
logger.error(f"调用AI模型未知异常: {e}", exc_info=True)
logger.error(f"========== AI模型调用失败 ==========")
return None
def parse_ai_response(self, response: str) -> Optional[Dict]:
"""解析AI返回的JSON响应"""
try:
json_match = re.search(r'\{[\s\S]*\}', response)
if json_match:
return json.loads(json_match.group(0))
return None
except Exception as e:
logger.error(f"解析AI响应失败: {e}")
return None
def save_analysis_cache(self, symbol: str, analysis_data: Dict, kline_timestamp: datetime) -> AIAnalysisCache:
"""保存AI分析结果到缓存"""
cache = AIAnalysisCache(
symbol=symbol,
analysis_data=analysis_data,
kline_timestamp=kline_timestamp,
created_at=datetime.now()
)
self.analysis_db.add(cache)
self.analysis_db.commit()
self.analysis_db.refresh(cache)
return cache
def get_latest_cache(self, symbol: str) -> Optional[AIAnalysisCache]:
"""获取最新的AI分析缓存"""
return self.analysis_db.query(AIAnalysisCache).filter(
AIAnalysisCache.symbol == symbol
).order_by(AIAnalysisCache.created_at.desc()).first()
def get_latest_kline_timestamp(self, symbol: str) -> Optional[datetime]:
"""获取当前K线数据的最新时间戳"""
cached_data = get_cached_data(
self.db,
symbol,
"futures",
["5min", "15min", "30min", "60min", "daily"]
)
if not cached_data or not cached_data.get("timeframes"):
return None
latest_time = None
for period, candles in cached_data.get("timeframes", {}).items():
if candles and len(candles) > 0:
last_candle = candles[-1]
time_str = last_candle.get("datetime", last_candle.get("time", ""))
if time_str:
try:
candle_time = datetime.fromisoformat(time_str.replace("Z", "+00:00"))
if latest_time is None or candle_time > latest_time:
latest_time = candle_time
except:
pass
return latest_time
def should_reanalyze(self, symbol: str, cache: AIAnalysisCache) -> bool:
"""判断是否需要重新分析"""
if not cache:
return True
# 1. 检查时间是否超过15分钟
time_since_analysis = (datetime.now() - cache.created_at).total_seconds()
if time_since_analysis > 900: # 15分钟 = 900秒
logger.info(f"分析时间已超过15分钟需要重新分析")
return True
# 2. 检查K线数据是否有变化
current_kline_time = self.get_latest_kline_timestamp(symbol)
if current_kline_time and cache.kline_timestamp:
if current_kline_time > cache.kline_timestamp:
logger.info(f"K线数据已更新当前: {current_kline_time}, 缓存: {cache.kline_timestamp}),需要重新分析")
return True
return False
def analyze(self, symbol: str) -> Dict:
"""执行完整的AI分析流程"""
logger.info(f"===== 开始AI分析: {symbol} =====")
# 获取当前K线数据的时间戳
kline_timestamp = self.get_latest_kline_timestamp(symbol)
logger.info(f"当前K线数据时间戳: {kline_timestamp}")
model = self.get_active_model()
if not model:
logger.error("未找到激活的AI模型配置")
return {
"success": False,
"error": "未配置AI模型或模型未激活"
}
logger.info(f"使用AI模型: {model.get('model_name')}")
data = self.prepare_multi_period_data(symbol)
if not data:
logger.error(f"未找到 {symbol} 的市场数据")
return {
"success": False,
"error": f"未找到 {symbol} 的市场数据"
}
logger.info(f"市场数据准备成功,包含周期: {list(data.get('timeframes', {}).keys())}")
prompt = AIAnalysisPrompt.build_prompt(symbol, data)
logger.info(f"AI提示词生成完成长度: {len(prompt)} 字符")
response = self.call_ai_model(prompt, model)
if not response:
logger.error("AI模型返回空响应")
return {
"success": False,
"error": "AI模型调用失败"
}
logger.info(f"AI模型响应接收成功长度: {len(response)} 字符")
analysis_result = self.parse_ai_response(response)
if not analysis_result:
logger.error(f"AI响应解析失败原始响应前100字符: {response[:100]}")
return {
"success": False,
"error": "AI响应解析失败"
}
logger.info(f"AI响应解析成功")
cache = self.save_analysis_cache(symbol, analysis_result, kline_timestamp)
logger.info(f"分析结果已保存到缓存ID: {cache.id}, K线时间戳: {kline_timestamp}")
logger.info(f"===== AI分析完成: {symbol} =====")
return {
"success": True,
"data": {
"id": cache.id,
"symbol": symbol,
"analysis_time": cache.created_at.isoformat(),
"result": analysis_result
}
}

@ -8,7 +8,7 @@ from typing import Dict, List, Optional
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.models import MarketData, ScheduledTask from app.models import MarketData, ScheduledTask, SymbolTimestamp
from app.config import CACHE_TTL_SECONDS from app.config import CACHE_TTL_SECONDS
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -67,7 +67,7 @@ def check_cache_status(
def save_market_data(db: Session, symbol: str, data: Dict) -> MarketData: def save_market_data(db: Session, symbol: str, data: Dict) -> MarketData:
""" """
保存采集结果到缓存 保存采集结果到缓存并同步更新合约时间戳
Args: Args:
symbol: 品种代码 symbol: 品种代码
@ -105,6 +105,9 @@ def save_market_data(db: Session, symbol: str, data: Dict) -> MarketData:
) )
db.add(record) db.add(record)
# 更新合约时间戳
update_symbol_timestamp(db, symbol, data.get("type", "futures"), now)
db.commit() db.commit()
logger.info(f"缓存已更新: {symbol}, {len(data.get('timeframes', {}))} 个周期") logger.info(f"缓存已更新: {symbol}, {len(data.get('timeframes', {}))} 个周期")
@ -115,6 +118,58 @@ def save_market_data(db: Session, symbol: str, data: Dict) -> MarketData:
).order_by(MarketData.fetched_at.desc()).first() ).order_by(MarketData.fetched_at.desc()).first()
def update_symbol_timestamp(db: Session, symbol: str, data_type: str, refresh_time: datetime) -> None:
"""更新或创建合约时间戳记录"""
timestamp_record = db.query(SymbolTimestamp).filter_by(
symbol=symbol,
data_type=data_type
).first()
if timestamp_record:
timestamp_record.last_refresh_at = refresh_time
timestamp_record.refresh_count += 1
else:
timestamp_record = SymbolTimestamp(
symbol=symbol,
data_type=data_type,
last_refresh_at=refresh_time,
refresh_count=1
)
db.add(timestamp_record)
db.commit()
def get_symbol_timestamp(db: Session, symbol: str, data_type: str = "futures") -> Optional[datetime]:
"""获取合约最后刷新时间"""
record = db.query(SymbolTimestamp).filter_by(
symbol=symbol,
data_type=data_type
).first()
return record.last_refresh_at if record else None
def needs_refresh(db: Session, symbol: str, data_type: str = "futures", threshold_seconds: int = 300) -> bool:
"""
检查合约是否需要刷新数据是否超过阈值时间
Args:
db: 数据库会话
symbol: 品种代码
data_type: 数据类型
threshold_seconds: 阈值时间默认300秒5分钟
Returns:
True 表示需要刷新False 表示数据仍然新鲜
"""
last_refresh = get_symbol_timestamp(db, symbol, data_type)
if last_refresh is None:
return True # 从未刷新过,需要刷新
age = (datetime.now() - last_refresh).total_seconds()
return age > threshold_seconds
def get_latest_cached( def get_latest_cached(
db: Session, db: Session,
symbol: str, symbol: str,

@ -0,0 +1,568 @@
: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);
}
/* 当前启用的AI卡片 */
.active-ai-card {
border-color: var(--green-border);
background: var(--green-bg);
}
.active-ai-card .card-header h3 {
color: var(--green);
}
.active-ai-card .card-header i {
color: var(--green);
}
.active-ai-display {
display: flex;
align-items: center;
gap: 16px;
padding: 12px;
}
.active-ai-icon {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(34, 197, 94, 0.15);
border: 1px solid var(--green-border);
border-radius: 12px;
color: var(--green);
font-size: 24px;
}
.active-ai-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.active-ai-name {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
}
.active-ai-model {
font-size: 13px;
color: var(--text-secondary);
}
/* 设置列表 */
.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,211 @@
<!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 active-ai-card" id="active-ai-card" style="display:none;">
<div class="card-header">
<h3><i class="fas fa-check-circle"></i> 当前启用</h3>
</div>
<div class="active-ai-display">
<div class="active-ai-icon" id="active-ai-icon">
<i class="fas fa-robot"></i>
</div>
<div class="active-ai-info">
<div class="active-ai-name" id="active-ai-name">--</div>
<div class="active-ai-model" id="active-ai-model">--</div>
</div>
</div>
</div>
<!-- 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="aliyun_coding">阿里云通义灵码</option>
<option value="bailian">阿里百炼</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 class="form-group">
<label>自定义模型</label>
<input type="text" id="custom-model" class="form-control" placeholder="输入自定义模型名称(留空使用上方选择)">
</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,369 @@
const API_BASE = '/api/v1/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: 'aliyun_coding', name: '通义灵码', icon: 'fas fa-code' },
{ id: 'bailian', name: '阿里百炼', icon: 'fas fa-flask' },
{ 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',
'aliyun_coding': 'fas fa-code',
'bailian': 'fas fa-flask',
'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'],
'aliyun_coding': ['qwen-coder-plus', 'qwen-coder-turbo'],
'bailian': ['qwen3.6-plus', 'qwen3.5-plus', 'qwen3-max', 'qwen3-coder-plus', 'MiniMax-M2.5', 'glm-4.7', 'kimi-k2.5', 'custom'],
'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',
'aliyun_coding': 'https://dashscope.aliyuncs.com/compatible-mode/v1',
'bailian': 'https://coding.dashscope.aliyuncs.com/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('custom-model').value = '';
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;
// 选中当前启用的提供商卡片
const providerCard = document.querySelector(`.provider-card[data-provider="${activeModel.provider}"]`);
if (providerCard) {
document.querySelectorAll('.provider-card').forEach(c => c.classList.remove('active'));
providerCard.classList.add('active');
}
// 更新模型下拉框
updateProviderModels();
}
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');
const activeCard = document.getElementById('active-ai-card');
if (!models || models.length === 0) {
list.innerHTML = '<div class="empty-state">暂无已保存的模型</div>';
activeCard.style.display = 'none';
return;
}
const activeModel = models.find(m => m.enabled);
if (activeModel) {
activeCard.style.display = 'block';
document.getElementById('active-ai-name').textContent = getProviderName(activeModel.provider || activeModel.api_base);
document.getElementById('active-ai-model').textContent = activeModel.model_name || activeModel.model_id || '--';
const iconMap = {
'openai': 'fab fa-openai',
'anthropic': 'fas fa-robot',
'google': 'fab fa-google',
'aliyun': 'fas fa-cloud',
'aliyun_coding': 'fas fa-code',
'bailian': 'fas fa-flask',
'baidu': 'fas fa-comments',
'zhipu': 'fas fa-lightbulb'
};
const icon = document.getElementById('active-ai-icon').querySelector('i');
icon.className = iconMap[activeModel.provider] || 'fas fa-robot';
} else {
activeCard.style.display = 'none';
}
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': '阿里云',
'aliyun_coding': '阿里云通义灵码',
'bailian': '阿里百炼',
'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 customModel = document.getElementById('custom-model').value.trim();
const selectedModel = document.getElementById('model-id').value;
const modelId = customModel || (selectedModel === 'custom' ? '' : selectedModel);
if (!modelId) {
resultEl.textContent = '✗ 请输入模型ID';
resultEl.className = 'test-result error';
return;
}
const config = {
model_name: modelId,
api_key: document.getElementById('api-key').value,
api_base: document.getElementById('api-base').value,
model_id: modelId,
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)
});
if (!response.ok) {
resultEl.textContent = `✗ 请求失败: ${response.status}`;
resultEl.className = 'test-result error';
return;
}
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 customModel = document.getElementById('custom-model').value.trim();
const selectedModel = document.getElementById('model-id').value;
const modelId = customModel || (selectedModel === 'custom' ? '' : selectedModel);
const newModel = {
model_name: modelId,
provider: selectedProvider,
api_key: document.getElementById('api-key').value,
api_base: document.getElementById('api-base').value,
model_id: modelId,
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);
}
models.forEach(m => {
m.enabled = (m.provider === selectedProvider && m.model_id === modelId);
});
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();
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,416 @@
<!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 class="theme-minimal">
<div class="bg-grid"></div>
<div class="bg-glow"></div>
<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 class="logo-pulse"></div>
</div>
<div class="logo-text">
<span class="logo-title">期货智析</span>
<span class="logo-subtitle">FUTURES INTELLIGENCE</span>
</div>
</div>
</div>
<nav class="nav-center">
<a href="#" class="nav-item active" data-page="analysis">
<span class="nav-icon"><i class="fas fa-microchip"></i></span>
<span>品种分析</span>
</a>
<a href="#" class="nav-item" data-page="watched">
<span class="nav-icon"><i class="fas fa-star"></i></span>
<span>自选</span>
</a>
<a href="#" class="nav-item" data-page="market">
<span class="nav-icon"><i class="fas fa-globe"></i></span>
<span>市场概览</span>
</a>
<a href="#" class="nav-item" data-page="risk">
<span class="nav-icon"><i class="fas fa-shield-alt"></i></span>
<span>风险预警</span>
</a>
</nav>
<div class="nav-right">
<div class="system-status">
<span class="status-dot"></span>
<span class="status-text">LIVE</span>
</div>
<div class="datetime" id="current-time">--</div>
<a href="/ai-config" class="nav-btn" title="AI配置">
<i class="fas fa-brain"></i>
</a>
<button class="nav-btn theme-toggle" id="theme-toggle" title="切换主题">
<i class="fas fa-moon"></i>
</button>
<a href="/" class="nav-btn" title="返回首页">
<i class="fas fa-th-large"></i>
</a>
</div>
</header>
<!-- Toast 提示容器 -->
<div class="toast-container" id="toast-container"></div>
<!-- 主内容区 -->
<main class="main-content">
<!-- 品种列表视图 -->
<div id="list-view" class="view active">
<!-- 搜索栏 -->
<div class="search-section">
<div class="search-box">
<i class="fas fa-search"></i>
<input type="text" id="search-input" placeholder="搜索品种名称或代码...">
<kbd>⌘K</kbd>
</div>
<div class="view-controls">
<button class="view-btn active" data-view="grid" title="网格视图">
<i class="fas fa-grid-2"></i>
<span>网格</span>
</button>
<button class="view-btn" data-view="list" title="列表视图">
<i class="fas fa-list"></i>
<span>列表</span>
</button>
</div>
</div>
<!-- 筛选栏 -->
<div class="filter-bar">
<div class="filter-tabs">
<button class="filter-tab active" data-category="all">
<span>全部</span>
<span class="filter-count" id="count-all">0</span>
</button>
<button class="filter-tab" data-category="watched">
<i class="fas fa-star"></i>
<span>自选</span>
<span class="filter-count" id="count-watched">0</span>
</button>
<button class="filter-tab" data-category="energy">
<i class="fas fa-fire"></i>
<span>能源</span>
</button>
<button class="filter-tab" data-category="metal">
<i class="fas fa-cube"></i>
<span>金属</span>
</button>
<button class="filter-tab" data-category="agriculture">
<i class="fas fa-seedling"></i>
<span>农产品</span>
</button>
<button class="filter-tab" data-category="finance">
<i class="fas fa-chart-pie"></i>
<span>金融</span>
</button>
</div>
<div class="filter-actions">
<button class="refresh-all-btn" id="refresh-all-btn" title="刷新全部品种">
<i class="fas fa-sync-alt"></i>
<span>刷新全部</span>
</button>
<button class="ai-analyze-all-btn" id="ai-analyze-all-btn" title="AI分析全部品种">
<i class="fas fa-brain"></i>
<span>全部分析</span>
</button>
<div class="sort-select">
<select id="sort-select">
<option value="trend_score">趋势评分</option>
<option value="success_rate">成功率</option>
<option value="change_pct">涨跌幅</option>
<option value="name">名称</option>
</select>
</div>
</div>
</div>
<!-- 统计概览 -->
<div class="stats-overview">
<div class="stat-card" onclick="filterByTrend('all')" style="cursor: pointer;" title="显示全部">
<div class="stat-icon"><i class="fas fa-layer-group"></i></div>
<div class="stat-info">
<span class="stat-value" id="total-count">0</span>
<span class="stat-label">监控品种</span>
</div>
</div>
<div class="stat-card up" onclick="filterByTrend('up')" style="cursor: pointer;" title="筛选上涨趋势">
<div class="stat-icon"><i class="fas fa-arrow-trend-up"></i></div>
<div class="stat-info">
<span class="stat-value" id="up-count">0</span>
<span class="stat-label">上涨趋势</span>
</div>
</div>
<div class="stat-card down" onclick="filterByTrend('down')" style="cursor: pointer;" title="筛选下跌趋势">
<div class="stat-icon"><i class="fas fa-arrow-trend-down"></i></div>
<div class="stat-info">
<span class="stat-value" id="down-count">0</span>
<span class="stat-label">下跌趋势</span>
</div>
</div>
<div class="stat-card neutral" onclick="filterByTrend('neutral')" style="cursor: pointer;" title="筛选震荡整理">
<div class="stat-icon"><i class="fas fa-arrows-left-right"></i></div>
<div class="stat-info">
<span class="stat-value" id="neutral-count">0</span>
<span class="stat-label">震荡整理</span>
</div>
</div>
</div>
<!-- 品种卡片网格 -->
<div id="futures-grid" class="futures-grid">
<!-- 动态生成 -->
</div>
</div>
<!-- 详情分析视图 -->
<div id="detail-view" class="view">
<!-- 返回按钮 -->
<div class="detail-actions">
<button class="back-btn" id="back-btn">
<i class="fas fa-arrow-left"></i>
<span>返回</span>
</button>
<button class="refresh-btn" id="refresh-symbol-btn" title="刷新合约数据">
<i class="fas fa-sync-alt"></i>
<span>刷新数据</span>
</button>
</div>
<!-- 品种标题区 -->
<div class="detail-header">
<div class="header-left">
<div class="symbol-info">
<span class="symbol-name" id="detail-name">--</span>
<span class="symbol-code" id="detail-symbol">--</span>
</div>
<div class="price-main">
<span class="price-value" id="detail-price">--</span>
<span class="price-change" id="detail-change">--</span>
<div class="price-levels">
<span class="level-tag resistance" id="detail-r1">R1: --</span>
<span class="level-tag support" id="detail-s1">S1: --</span>
</div>
</div>
</div>
<div class="header-right">
<div class="quote-grid">
<div class="quote-item">
<span class="quote-label">开盘</span>
<span class="quote-value" id="detail-open">--</span>
</div>
<div class="quote-item">
<span class="quote-label">最高</span>
<span class="quote-value up" id="detail-high">--</span>
</div>
<div class="quote-item">
<span class="quote-label">最低</span>
<span class="quote-value down" id="detail-low">--</span>
</div>
<div class="quote-item">
<span class="quote-label">成交量</span>
<span class="quote-value" id="detail-volume">--</span>
</div>
</div>
</div>
</div>
<!-- 周期选择 -->
<div class="period-bar">
<span class="period-label"><i class="fas fa-clock"></i> 周期</span>
<div class="period-btns">
<button class="period-btn" data-period="5">5M</button>
<button class="period-btn active" data-period="15">15M</button>
<button class="period-btn" data-period="30">30M</button>
<button class="period-btn" data-period="60">1H</button>
</div>
</div>
<!-- 图表和侧边栏 -->
<div class="detail-body">
<div class="chart-section">
<!-- K线图表区 -->
<div class="chart-container">
<div class="chart-header">
<span class="chart-title">K线图</span>
<div class="chart-legend">
<span class="legend-item"><span class="legend-dot ma5"></span>MA5</span>
<span class="legend-item"><span class="legend-dot ma10"></span>MA10</span>
<span class="legend-item"><span class="legend-dot ma20"></span>MA20</span>
</div>
</div>
<div id="kline-chart" class="kline-chart"></div>
</div>
<!-- 历史分析记录 -->
<div class="history-container">
<div class="history-header">
<i class="fas fa-clock-rotate-left"></i>
<span>历史分析记录</span>
</div>
<div class="history-list" id="history-list">
<!-- 动态生成 -->
</div>
</div>
</div>
<!-- 右侧分析面板 -->
<div class="analysis-sidebar">
<!-- AI智能分析 -->
<div class="panel-card ai-analysis-card" id="ai-analysis-panel">
<div class="panel-header">
<i class="fas fa-brain"></i>
<span>AI 思维分析</span>
<div class="panel-header-actions">
<button class="ai-analyze-btn" id="ai-analyze-btn" onclick="runAIAnalysis()" title="执行AI分析">
<i class="fas fa-play"></i>
<span>智能分析</span>
</button>
</div>
</div>
<div class="ai-analysis-content" id="ai-analysis-content">
<div class="ai-analysis-placeholder">
<i class="fas fa-brain"></i>
<p>点击"智能分析"按钮获取AI分析结果</p>
</div>
</div>
</div>
<!-- 技术指标 -->
<div class="panel-card indicators-card">
<div class="panel-header">
<i class="fas fa-wave-pulse"></i>
<span>技术指标</span>
</div>
<div class="indicators-grid">
<div class="indicator-cell">
<span class="indicator-label">MACD</span>
<span class="indicator-value" id="macd-signal">--</span>
<span class="indicator-detail" id="macd-detail">--</span>
</div>
<div class="indicator-cell">
<span class="indicator-label">RSI</span>
<span class="indicator-value" id="rsi-value">--</span>
<span class="indicator-detail" id="rsi-status">--</span>
</div>
<div class="indicator-cell">
<span class="indicator-label">BOLL</span>
<span class="indicator-value" id="boll-signal">--</span>
<span class="indicator-detail" id="boll-detail">--</span>
</div>
<div class="indicator-cell">
<span class="indicator-label">KDJ</span>
<span class="indicator-value" id="kdj-signal">--</span>
<span class="indicator-detail" id="kdj-detail">--</span>
</div>
</div>
</div>
<!-- 关键点位 -->
<div class="panel-card levels-card">
<div class="panel-header">
<i class="fas fa-crosshairs"></i>
<span>关键点位</span>
</div>
<div class="levels-container">
<div class="level-group resistance">
<span class="level-group-label">压力</span>
<div class="level-item" id="resistance-1"><span>R1</span><span>--</span></div>
<div class="level-item" id="resistance-2"><span>R2</span><span>--</span></div>
</div>
<div class="level-divider"></div>
<div class="level-item pivot-point" id="pivot-point">
<span>中枢 (PP)</span>
<span>--</span>
</div>
<div class="level-divider"></div>
<div class="level-group support">
<span class="level-group-label">支撑</span>
<div class="level-item" id="support-1"><span>S1</span><span>--</span></div>
<div class="level-item" id="support-2"><span>S2</span><span>--</span></div>
</div>
</div>
</div>
<!-- 多周期趋势 -->
<div class="panel-card trends-card">
<div class="panel-header">
<i class="fas fa-timeline"></i>
<span>多周期趋势</span>
</div>
<div class="trends-container" id="period-trends">
<!-- 动态生成 -->
</div>
</div>
<!-- 情景预案 -->
<div class="panel-card scenario-card" id="scenario-panel" style="display:none;">
<div class="panel-header">
<i class="fas fa-chess"></i>
<span>情景预案</span>
</div>
<div class="scenario-container" id="scenario-plans">
<!-- 动态生成 -->
</div>
</div>
</div>
</div>
</div>
</main>
</div>
<!-- AI分析详情对话框 -->
<div class="modal-overlay" id="ai-analysis-modal">
<div class="modal-content modal-large">
<div class="modal-header">
<h3><i class="fas fa-brain"></i> AI 四维联合分析报告</h3>
<button class="modal-close" onclick="closeModal('ai-analysis-modal')"><i class="fas fa-xmark"></i></button>
</div>
<div class="modal-body" id="ai-analysis-modal-body">
<!-- 动态生成 -->
</div>
</div>
</div>
<!-- AI建议详情对话框 -->
<div class="modal-overlay" id="suggestion-modal">
<div class="modal-content">
<div class="modal-header">
<h3><i class="fas fa-robot"></i> AI 交易建议详情</h3>
<button class="modal-close" onclick="closeModal('suggestion-modal')"><i class="fas fa-xmark"></i></button>
</div>
<div class="modal-body" id="suggestion-modal-body">
<!-- 动态生成 -->
</div>
</div>
</div>
<!-- 历史记录详情对话框 -->
<div class="modal-overlay" id="history-modal">
<div class="modal-content">
<div class="modal-header">
<h3><i class="fas fa-clock-rotate-left"></i> 分析记录详情</h3>
<button class="modal-close" onclick="closeModal('history-modal')"><i class="fas fa-xmark"></i></button>
</div>
<div class="modal-body" id="history-modal-body">
<!-- 动态生成 -->
</div>
</div>
</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>

File diff suppressed because it is too large Load Diff

@ -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> <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> <span>运行日志</span>
</a> </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> </nav>
</aside> </aside>

@ -0,0 +1,22 @@
import sqlite3
db_path = 'data/futures_analysis.db'
conn = sqlite3.connect(db_path)
print('=== AI模型配置 ===')
cursor = conn.execute("SELECT * FROM ai_model_configs")
cols = [desc[0] for desc in cursor.description]
print(f'字段: {cols}')
rows = cursor.fetchall()
print(f'记录数: {len(rows)}')
for row in rows:
print(dict(zip(cols, row)))
print('\n=== 分析设置 ===')
cursor = conn.execute("SELECT * FROM analysis_settings")
rows = cursor.fetchall()
for row in rows:
print(row)
conn.close()

@ -0,0 +1,12 @@
import sqlite3
conn = sqlite3.connect('data/futures_analysis.db')
cursor = conn.execute('SELECT id, symbol, created_at FROM ai_analysis_cache ORDER BY created_at DESC LIMIT 10')
print('AI分析缓存记录:')
print('ID | 合约 | 创建时间')
print('-' * 60)
for row in cursor:
print(f'{row[0]} | {row[1]} | {row[2]}')
conn.close()

@ -0,0 +1,32 @@
import sqlite3
import os
db_path = 'data/futures_analysis.db'
print(f'数据库文件路径: {os.path.abspath(db_path)}')
print(f'数据库存在: {os.path.exists(db_path)}')
if os.path.exists(db_path):
print(f'数据库大小: {os.path.getsize(db_path)} 字节')
conn = sqlite3.connect(db_path)
cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='table'")
tables = [row[0] for row in cursor.fetchall()]
print(f'数据库表列表: {tables}')
if 'ai_analysis_cache' in tables:
cursor = conn.execute("SELECT COUNT(*) FROM ai_analysis_cache")
count = cursor.fetchone()[0]
print(f'AI分析记录数: {count}')
cursor = conn.execute("SELECT id, symbol, created_at FROM ai_analysis_cache ORDER BY created_at DESC LIMIT 5")
records = cursor.fetchall()
print(f'最近5条记录:')
for r in records:
print(f' ID: {r[0]}, 合约: {r[1]}, 时间: {r[2]}')
else:
print('❌ ai_analysis_cache 表不存在!')
conn.close()
else:
print('❌ 数据库文件不存在!')

@ -0,0 +1,22 @@
{
"models": [
{
"model_name": "qwen3.6-plus",
"provider": "bailian",
"api_key": "sk-sp-51d0695ab1114470b913146d21baf68f",
"api_base": "https://coding.dashscope.aliyuncs.com/v1",
"model_id": "qwen3.6-plus",
"temperature": 0.7,
"max_tokens": 2000,
"enabled": true
}
],
"active_model": "bailian",
"analysis_settings": {
"enable_technical_analysis": true,
"enable_fundamental_analysis": false,
"enable_sentiment_analysis": false,
"risk_tolerance": "medium",
"max_position_pct": 10
}
}

@ -0,0 +1,201 @@
# 📋 AI 期货/股票四维联合分析系统 (Prompt 模板)
> **版本**: v2.0 (全量实战版)
> **核心架构**: 四维联合判断 (4D-XV) + 17 条交易红线 + 多周期共振 + 动态策略切换
> **适用场景**: 期货/股票技术分析、日内/短线交易决策、交易纪律审查
---
## 🧠 角色与任务设定 (System Prompt)
```text
你是一位拥有 20 年实战经验的资深金融交易分析师,精通 A 股市场与商品期货的技术分析。
你的核心使命是基于提供的 K 线数据,执行**「四维联合判断分析法 (4D-XV)」**,并提供包含风控红线审查的客观交易策略。
你的分析必须遵循以下原则:
1. **数据驱动**:所有结论必须基于输入的 JSON 数据,严禁凭空捏造。
2. **四维共振**:任何交易建议必须经过 MACD趋势、成交量资金、KDJ时机、多周期方向的交叉验证。
3. **红线否决**如果数据触发「17 条交易红线」,必须直接给出「禁止交易」或「止损」的建议。
4. **客观中立**:不使用绝对化表述,提供情景预案(概率估算)。
5. **动态切换**:遇到关键位放量突破时,立即切换右侧思维,不逆势死扛。
```
---
## 📥 输入数据规范 (JSON)
AI 模型接收的输入数据必须包含以下结构:
```json
{
"symbol": "AG2606",
"current_price": 7200,
"timeframes": {
"60min": {
"trend": "down",
"ma10": 7250, "ma20": 7300,
"macd_dif": -5.2, "macd_dea": -3.1, "macd_histogram": -2.1,
"kdj_k": 85, "kdj_d": 80, "kdj_j": 95,
"volume_avg": 150000, "volume_current": 200000,
"candles": [{"time": "...", "open": 0, "high": 0, "low": 0, "close": 0, "volume": 0}]
},
"30min": { "... 结构同上 ..." },
"15min": { "... 结构同上 ..." },
"5min": { "... 结构同上 ..." },
"daily": { "... 结构同上 ..." }
},
"fundamentals": "近期库存持续下降,宏观情绪偏暖"
}
```
---
## 🛠️ 核心分析引擎 (执行步骤)
请在代码中要求 AI 严格按以下步骤执行分析:
### 第一步:四维联合信号扫描 (4D-XV)
**60m/30m/15m** 周期进行四维交叉验证,填写诊断表:
| 周期 | MACD(趋势锚点) | 成交量(资金验证) | KDJ(时机狙击) | 四维综合结论 |
|------|----------------|------------------|---------------|--------------|
| **60m** | 零轴上/下,柱状图放大/缩小/背离 | 趋势量能(放量上涨/缩量回调等) | K 值位置,金死叉状态,是否钝化 | 定大势(只做多/只做空/震荡) |
| **30m** | 零轴上/下,柱状图放大/缩小 | 拐点量能(突破/跌破量比≥1.5 | K 值位置,金死叉状态 | 找拐点(验证短期转折) |
| **15m** | 零轴上/下,柱状图放大/缩小 | 入场量能即时量比≥1.5/2.0 | K 值位置,金死叉状态 | 择入场(精确触发点) |
**判定标准**
* ✅ **完美共振**3 维度以上同向 + 量能确认 → 强信号胜率≥80%
* ⚠️ **弱势/矛盾**:指标矛盾或缺少量能验证 → 震荡/观望/轻仓
* ❌ **逆势/背离**量价背离、MACD 与 KDJ 冲突 → **禁止入场**
### 第二步KDJ 深度诊断
* **区域判断**:超买 (>80)、超卖 (<20)、中性区。
* **信号确认**:金叉/死叉有效性(需等待第二次确认,防假信号)。
* **钝化检查**:是否持续超买/超卖 >6 根 K 线?若是,**放弃 KDJ 逆势信号,改用 MACD 趋势跟踪**。
* **背离识别**KDJ 顶/底背离 + MACD 同步背离 = 极高胜率反转信号。
* **参数建议**:日线 (9,3,3)、60m (9,3,3)、30m (6,3,3)、15m (5,3,3)。
### 第三步:关键位计算与验证 (Pivot Point)
使用最近一根完整 K 线的 H、L、C 计算:
`PP = (H+L+C)/3`, `R1 = 2*PP-L`, `S1 = 2*PP-H`, `R2 = PP+(H-L)`, `S2 = PP-(H-L)`
**关键位五维度验证**
1. 测试次数 ≥3 次?
2. 量价配合良好(无量支撑为虚位)?
3. 多周期共振(大周期关键位权重更高)?
4. 基本面匹配(成本线、政策线)?
5. 突破后回踩确认?
### 第四步:交易红线审查 (17 条)
**检查数据是否触碰以下红线(触碰即停)**
* [ ] 逆势重仓
* [ ] 移动止损/加仓摊平
* [ ] 情绪化交易/报复交易
* [ ] 放量突破死扛逆势单(最高红线)
* [ ] 22:00 后开仓/14:30 后隔夜决策
* [ ] 无衰竭信号入场/追涨杀跌
* [ ] 五维度评分 < 9 < 5
### 第五步:动态策略切换 (Plan B)
* **场景**:价格逼近关键阻力位,原判断受阻回落,但出现**放量突破**。
* **验证**15m/30m 成交量 > 近 5 日均量 200%,且伴随实体饱满大阳线。
* **动作**:若持有逆势单(如空单),价格站稳 15 分钟 → **无条件市价止损**,绝不等回调。
* **思维**:立即切换为右侧跟随思维,回踩阻力位(转支撑)时尝试做多。
---
## 📝 输出模板 (Markdown)
要求 AI 严格按照以下格式输出:
```markdown
## 📊 {symbol} 四维联合分析报告
**分析时间**{当前时间} | **当前价**{current_price}
### 一、四维联合信号表 (4D-XV)
| 周期 | MACD(趋势) | 成交量(资金) | KDJ(时机) | 结论 |
|------|------------|-------------|-----------|------|
| 60m | ... | ... | ... | ... |
| 30m | ... | ... | ... | ... |
| 15m | ... | ... | ... | ... |
### 二、KDJ 深度诊断
- **当前状态**...
- **钝化/背离**...
- **参数建议**...
### 三、关键位 (Pivot Point)
- 🔴 **R2**: ... | **R1**: ...
- ⚪ **PP**: ...
- 🟢 **S1**: ... | **S2**: ...
- **有效性验证**(测试次数、量能、周期共振情况)
### 四、11 项纪律检查与红线审查
- **红线检查**:✅ 未触碰 / ❌ 触碰 [具体红线]
- **纪律得分**__ / 11 (≥9 可入场)
- **诊断表**
- A. 趋势 (60m 大势/30m 结构/15m 协同): [✅/❌]
- B. 位置 (关键位/共振): [✅/❌]
- C. 信号 (K 线/量能/背离): [✅/❌]
- D. 风险 (盈亏比/仓位): [✅/❌]
- E. 心态: [✅/❌]
### 五、交易建议
- **方向**:做多 / 做空 / 观望
- **入场区间**...
- **止损位**... (依据:前低/高/关键位)
- **止盈目标**... (盈亏比 ≥ 1:2)
- **建议仓位**... (基于信号强度)
### 六、动态策略预案 (Plan B)
1. **顺势突破 (35-45%)**:放量突破关键位,跟随右侧思维,移动止损。
2. **震荡拉锯 (30-40%)**R1-S1 区间内高抛低吸,突破前观望。
3. **假突破反转 (15-25%)**MACD 顶/底背离 + 量能不足,立即止损反手。
4. **突发消息 (5-10%)**:减仓 50% 规避不确定性。
### ⚠️ 风险提示与经验教训
- **技术局限**:技术指标具有滞后性,历史表现不代表未来。
- **避坑提醒**(根据当前盘面自动提示,如"警惕缩量创新高"、"KDJ 超买钝化中不宜逆势做空"等)
- **红线警告**(如触发红线,在此处高亮警告)
- **免责声明**:本分析仅供参考,不构成投资建议,交易需自负盈亏。
```
---
## 💡 经验教训总结 (供 AI 内部参考)
在分析过程中AI 应时刻检索以下**历史经验教训库**,若数据特征匹配,必须在报告中提示:
| 陷阱名称 | 特征识别 | 应对经验教训 |
|----------|----------|--------------|
| **唯 MACD 论** | MACD 金叉但 KDJ 超买 | 往往是诱多,**禁止单独使用 MACD 入场**。 |
| **唯 KDJ 论** | KDJ 金叉但 MACD 零轴下死叉 | 通常是下跌中继(假反弹),**必须等待 MACD 确认**。 |
| **无量突破** | KDJ+MACD 共振但无量 | 假突破概率 > 80%**放弃或极轻仓试错**。 |
| **大周期否决** | 日线/60m 明确空头 | 小周期任何做多信号均为短线反弹,**仓位必须减半**。 |
| **钝化区逆势** | KDJ 超买但价格继续涨 | 不逆势等钝化解除KDJ 离开超买区 + 价格反向突破)。 |
| **假金叉/死叉** | 金叉后快速死叉 | 等待**第二次确认**,或结合 30m 级别过滤。 |
| **背离假信号** | 多次背离后价格继续原方向 | 背离不是入场理由,**等待价格突破关键位确认**。 |
| **放量突破死扛** | 价格放量突破阻力,仍持有逆势空单 | **最高红线!立即市价止损,绝不等回调。** |
---
## 📦 附录:精简版 Prompt (上下文窗口有限时使用)
```text
你是一位专业交易分析师。请基于以下 JSON 数据,执行【四维联合判断 (4D-XV)】分析。
分析逻辑:
1. MACD 定趋势KDJ 择时机,成交量验证,多周期 (60m→30m→15m) 共振。
2. 严格审查「17 条交易红线」,触碰即禁止交易。
3. 若出现放量突破关键位,执行 Plan B右侧跟随不逆势死扛
输出内容:
1. 四维信号表 (MACD/量/KDJ/结论)。
2. Pivot Point 关键位及有效性验证。
3. 11 项纪律检查评分。
4. 交易建议 (方向/入场/止损/止盈/仓位)。
5. 情景预案 (5 种情景 + 概率)。
6. 风险提示与避坑提醒 (结合经验教训库)。
数据:{{JSON_DATA}}
```

Binary file not shown.

@ -16,7 +16,7 @@ services:
- BUFFER_LOG_LEVEL=INFO - BUFFER_LOG_LEVEL=INFO
restart: unless-stopped restart: unless-stopped
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8600/api/v1/health"] test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8600/api/v1/health')"]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3

@ -0,0 +1,24 @@
"""
手动初始化期货智析数据库
用于在应用外部创建数据库表
"""
import sys
from pathlib import Path
# 添加项目根目录到 Python 路径
project_root = Path(__file__).parent
sys.path.insert(0, str(project_root))
from app.analysis_db import init_analysis_db
if __name__ == "__main__":
print("开始初始化期货智析数据库...")
init_analysis_db()
print("✅ 数据库初始化完成!")
# 验证数据库文件是否创建
from app.analysis_db import ANALYSIS_DB_PATH
if ANALYSIS_DB_PATH.exists():
print(f"✅ 数据库文件已创建: {ANALYSIS_DB_PATH}")
else:
print(f"❌ 数据库文件未创建: {ANALYSIS_DB_PATH}")

@ -0,0 +1,31 @@
"""
更新AI分析缓存表结构
添加 kline_timestamp 字段
"""
import sqlite3
from pathlib import Path
project_root = Path(__file__).parent
ANALYSIS_DB_PATH = project_root / "data" / "futures_analysis.db"
def add_kline_timestamp_column():
"""添加 kline_timestamp 列"""
conn = sqlite3.connect(str(ANALYSIS_DB_PATH))
cursor = conn.cursor()
# 检查列是否已存在
cursor.execute("PRAGMA table_info(ai_analysis_cache)")
columns = [col[1] for col in cursor.fetchall()]
if 'kline_timestamp' in columns:
print("✅ kline_timestamp 列已存在")
else:
print("添加 kline_timestamp 列...")
cursor.execute("ALTER TABLE ai_analysis_cache ADD COLUMN kline_timestamp DATETIME")
conn.commit()
print("✅ kline_timestamp 列添加成功")
conn.close()
if __name__ == "__main__":
add_kline_timestamp_column()

@ -0,0 +1,27 @@
"""
验证数据库表是否正确创建
"""
import sqlite3
db_path = "data/futures_analysis.db"
conn = sqlite3.connect(db_path)
cursor = conn.execute('SELECT name FROM sqlite_master WHERE type="table"')
print("✅ 数据库表列表:")
for row in cursor:
print(f" - {row[0]}")
# 检查 ai_analysis_cache 表是否存在
cursor.execute('SELECT name FROM sqlite_master WHERE type="table" AND name="ai_analysis_cache"')
if cursor.fetchone():
print("\n✅ ai_analysis_cache 表已存在!")
# 显示表结构
cursor.execute("PRAGMA table_info(ai_analysis_cache)")
print("\n表结构:")
for col in cursor:
print(f" {col[1]} ({col[2]})")
else:
print("\n❌ ai_analysis_cache 表不存在!")
conn.close()
Loading…
Cancel
Save