feat: 增加交易记录导入及分析

master^2
Lxy 4 weeks ago
parent 8a1918fc8d
commit 1498532e05

24
.env

@ -0,0 +1,24 @@
# 期货智析缓冲平台 - 环境配置
# 数据库配置
BUFFER_DB_PATH=/app/data/buffer.db
# 服务配置
BUFFER_HOST=0.0.0.0
BUFFER_PORT=8600
# 缓存配置(秒)
CACHE_TTL=300
# 日志级别 (DEBUG/INFO/WARNING/ERROR/CRITICAL)
BUFFER_LOG_LEVEL=INFO
# 并发采集数
MAX_WORKERS=2
# Docker端口映射
HOST_PORT=9600
CONTAINER_PORT=8600
# 数据目录
DATA_DIR=E:\docker_workspace\futures_datas

@ -40,6 +40,7 @@ def init_analysis_db():
FuturesAnalysis, WatchedSymbol, AIModelConfig, AnalysisSettings, FuturesAnalysis, WatchedSymbol, AIModelConfig, AnalysisSettings,
AIAnalysisCache, ReviewDate, SymbolRanking, TradingPlan, AIAnalysisCache, ReviewDate, SymbolRanking, TradingPlan,
SymbolScoreV2, TradingPlanV2, SectorHeat, ReviewPlanV2, SymbolScoreV2, TradingPlanV2, SectorHeat, ReviewPlanV2,
TradeRecord, TradeImportBatch,
) )
AnalysisBase.metadata.create_all(bind=analysis_engine) AnalysisBase.metadata.create_all(bind=analysis_engine)

@ -275,3 +275,51 @@ class ReviewPlanV2(AnalysisBase):
def __repr__(self): def __repr__(self):
return f"<ReviewPlanV2 {self.review_date}>" return f"<ReviewPlanV2 {self.review_date}>"
# ==================== 交易复盘模型 ====================
class TradeRecord(AnalysisBase):
"""交易记录表 - 从期货结算单导入的逐笔交易明细"""
__tablename__ = "trade_records"
id = Column(Integer, primary_key=True, autoincrement=True)
trade_type = Column(String(8), nullable=False, comment="类型: 期货/期权")
symbol = Column(String(32), nullable=False, index=True, comment="合约代码 AG2606")
variety = Column(String(16), nullable=False, index=True, comment="品种代码 AG")
symbol_name = Column(String(64), nullable=True, comment="品种名称 沪银")
direction = Column(String(8), nullable=False, comment="买卖方向: 买/卖")
offset = Column(String(8), nullable=True, comment="开平标志: 开/平")
price = Column(Float, nullable=True, comment="成交价/权利金单价")
volume = Column(Float, nullable=True, comment="手数/成交量")
amount = Column(Float, nullable=True, comment="成交额/权利金")
close_pnl = Column(Float, nullable=True, default=0.0, comment="平仓盈亏")
commission = Column(Float, nullable=True, default=0.0, comment="手续费")
trade_date = Column(String(16), nullable=True, index=True, comment="成交日期 YYYY-MM-DD")
trade_time = Column(String(32), nullable=True, comment="成交时间 HH:MM:SS")
import_batch = Column(String(64), nullable=False, index=True, comment="导入批次号 UUID")
source_file = Column(String(128), nullable=True, comment="来源文件名")
created_at = Column(DateTime, nullable=False, default=datetime.now)
__table_args__ = (
Index('ix_trade_records_date_variety', 'trade_date', 'variety'),
)
def __repr__(self):
return f"<TradeRecord {self.symbol} {self.direction} {self.offset} {self.trade_date}>"
class TradeImportBatch(AnalysisBase):
"""交易导入批次表 - 记录每次导入的元信息"""
__tablename__ = "trade_import_batches"
id = Column(Integer, primary_key=True, autoincrement=True)
batch_id = Column(String(64), nullable=False, unique=True, index=True, comment="批次号 UUID")
source_file = Column(String(128), nullable=False, comment="来源文件名")
futures_count = Column(Integer, nullable=False, default=0, comment="期货交易记录数")
options_count = Column(Integer, nullable=False, default=0, comment="期权交易记录数")
trade_dates = Column(String(256), nullable=True, comment="涉及交易日期范围")
created_at = Column(DateTime, nullable=False, default=datetime.now)
def __repr__(self):
return f"<TradeImportBatch {self.batch_id} {self.source_file}>"

@ -0,0 +1,424 @@
"""
交易复盘接口 - 提供交易记录导入查询汇总统计等功能
"""
import logging
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query
from pydantic import BaseModel
from sqlalchemy.orm import Session
from app.analysis_db import get_analysis_db
from app.analysis_models import TradeRecord, TradeImportBatch
from app.services.trade_parser import (
parse_settlement_file,
save_to_db,
calc_daily_summary,
calc_variety_summary,
calc_overall_statistics,
get_trade_pairs,
)
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/trade-review", tags=["交易复盘"])
# ==================== 文件导入 ====================
@router.post("/import")
async def import_settlement(
file: UploadFile = File(...),
db: Session = Depends(get_analysis_db),
):
"""上传并解析期货结算单文件,导入交易记录"""
if not file.filename.endswith(('.xls', '.xlsx')):
raise HTTPException(status_code=400, detail="仅支持 .xls/.xlsx 格式的结算单文件")
try:
content = await file.read()
df_futures, df_options = parse_settlement_file(content, file.filename)
if df_futures.empty and df_options.empty:
return {"success": False, "message": "文件中未找到有效的交易记录(请确认是期货公司导出的结算单)"}
result = save_to_db(db, df_futures, df_options, file.filename)
return {
"success": True,
"message": f"导入成功:期货 {result['futures_count']} 条,期权 {result['options_count']}",
"data": result,
}
except Exception as e:
logger.exception("导入结算单失败")
raise HTTPException(status_code=500, detail=f"导入失败: {str(e)}")
# ==================== 交易记录查询 ====================
@router.get("/records")
def get_records(
start_date: Optional[str] = Query(None, description="开始日期 YYYY-MM-DD"),
end_date: Optional[str] = Query(None, description="结束日期 YYYY-MM-DD"),
symbol: Optional[str] = Query(None, description="合约代码"),
variety: Optional[str] = Query(None, description="品种代码"),
trade_type: Optional[str] = Query(None, description="类型: 期货/期权"),
page: int = Query(1, ge=1),
page_size: int = Query(50, ge=1, le=200),
db: Session = Depends(get_analysis_db),
):
"""查询交易记录(支持筛选和分页)"""
query = db.query(TradeRecord)
if start_date:
query = query.filter(TradeRecord.trade_date >= start_date)
if end_date:
query = query.filter(TradeRecord.trade_date <= end_date)
if symbol:
query = query.filter(TradeRecord.symbol == symbol)
if variety:
query = query.filter(TradeRecord.variety == variety)
if trade_type:
query = query.filter(TradeRecord.trade_type == trade_type)
total = query.count()
records = query.order_by(
TradeRecord.trade_date.desc(),
TradeRecord.trade_time.desc(),
).offset((page - 1) * page_size).limit(page_size).all()
return {
"success": True,
"data": {
"total": total,
"page": page,
"page_size": page_size,
"records": [{
"id": r.id,
"trade_type": r.trade_type,
"symbol": r.symbol,
"variety": r.variety,
"symbol_name": r.symbol_name,
"direction": r.direction,
"offset": r.offset,
"price": r.price,
"volume": r.volume,
"amount": r.amount,
"close_pnl": r.close_pnl,
"commission": r.commission,
"trade_date": r.trade_date,
"trade_time": r.trade_time,
"import_batch": r.import_batch,
"source_file": r.source_file,
} for r in records],
},
}
# ==================== 批次管理 ====================
@router.get("/batches")
def get_batches(
db: Session = Depends(get_analysis_db),
):
"""查询导入批次列表"""
batches = db.query(TradeImportBatch).order_by(
TradeImportBatch.created_at.desc()
).all()
return {
"success": True,
"data": [{
"id": b.id,
"batch_id": b.batch_id,
"source_file": b.source_file,
"futures_count": b.futures_count,
"options_count": b.options_count,
"trade_dates": b.trade_dates,
"created_at": b.created_at.strftime('%Y-%m-%d %H:%M:%S') if b.created_at else '',
} for b in batches],
}
@router.delete("/records/{batch_id}")
def delete_batch_records(
batch_id: str,
db: Session = Depends(get_analysis_db),
):
"""按批次删除交易记录"""
count = db.query(TradeRecord).filter(TradeRecord.import_batch == batch_id).count()
if count == 0:
raise HTTPException(status_code=404, detail="未找到该批次的交易记录")
db.query(TradeRecord).filter(TradeRecord.import_batch == batch_id).delete()
db.query(TradeImportBatch).filter(TradeImportBatch.batch_id == batch_id).delete()
db.commit()
return {"success": True, "message": f"已删除 {count} 条交易记录"}
# ==================== 汇总统计 ====================
@router.get("/latest-trade-date")
def get_latest_trade_date(
db: Session = Depends(get_analysis_db),
):
"""获取最后一个有交易记录的交易日"""
from datetime import date
latest = db.query(TradeRecord.trade_date).filter(
TradeRecord.trade_date.isnot(None),
TradeRecord.trade_date != '',
).distinct().order_by(TradeRecord.trade_date.desc()).first()
if latest:
return {"success": True, "data": {"trade_date": latest[0]}}
else:
return {"success": True, "data": {"trade_date": date.today().strftime('%Y-%m-%d')}}
@router.delete("/records-by-date/{trade_date}")
def delete_records_by_date(
trade_date: str,
db: Session = Depends(get_analysis_db),
):
"""删除指定交易日的所有交易记录"""
count = db.query(TradeRecord).filter(TradeRecord.trade_date == trade_date).count()
if count == 0:
raise HTTPException(status_code=404, detail=f"未找到 {trade_date} 的交易记录")
db.query(TradeRecord).filter(TradeRecord.trade_date == trade_date).delete()
db.commit()
return {"success": True, "message": f"已删除 {trade_date}{count} 条交易记录"}
@router.get("/daily-summary")
def get_daily_summary(
start_date: Optional[str] = Query(None),
end_date: Optional[str] = Query(None),
db: Session = Depends(get_analysis_db),
):
"""获取每日交易盈亏汇总"""
data = calc_daily_summary(db, start_date, end_date)
return {"success": True, "data": data}
@router.get("/variety-summary")
def get_variety_summary(
start_date: Optional[str] = Query(None),
end_date: Optional[str] = Query(None),
db: Session = Depends(get_analysis_db),
):
"""获取品种交易盈亏汇总"""
data = calc_variety_summary(db, start_date, end_date)
return {"success": True, "data": data}
@router.get("/statistics")
def get_statistics(
start_date: Optional[str] = Query(None),
end_date: Optional[str] = Query(None),
db: Session = Depends(get_analysis_db),
):
"""获取整体交易统计"""
data = calc_overall_statistics(db, start_date, end_date)
return {"success": True, "data": data}
@router.get("/trade-pairs")
def get_trade_pairs_api(
start_date: Optional[str] = Query(None),
end_date: Optional[str] = Query(None),
symbol: Optional[str] = Query(None),
db: Session = Depends(get_analysis_db),
):
"""获取开平仓配对后的逐笔交易"""
pairs = get_trade_pairs(db, start_date, end_date)
if symbol:
pairs = [p for p in pairs if p["symbol"] == symbol]
return {"success": True, "data": pairs}
# ==================== K线 + 交易标记 ====================
@router.get("/kline-with-trades/{symbol}")
def get_kline_with_trades(
symbol: str,
period: str = Query("daily", description="K线周期: daily/60min/15min/5min"),
start_date: Optional[str] = Query(None),
end_date: Optional[str] = Query(None),
db: Session = Depends(get_analysis_db),
):
"""获取K线数据 + 该品种的交易标记买卖点。symbol 可以是完整合约代码(AG2608)或品种代码(AG)"""
import json
import re
from app.database import SessionLocal as MainSessionLocal
from app.models import MarketData
# 判断是品种代码还是完整合约代码
variety_code = symbol.upper()
if re.match(r'^[A-Za-z]+\d', symbol):
# 完整合约代码如 AG2608
variety_code = re.match(r'^([A-Za-z]+)', symbol).group(1).upper()
kline_symbol = symbol
else:
# 品种代码如 AG从配置中查找当前合约
kline_symbol = _resolve_symbol_from_config(variety_code)
main_db = MainSessionLocal()
try:
market_data = main_db.query(MarketData).filter(
MarketData.symbol == kline_symbol,
MarketData.period == period,
).first()
if not market_data or not market_data.candles_json:
# 尝试模糊匹配:查找以该品种代码开头的合约
market_data = main_db.query(MarketData).filter(
MarketData.symbol.like(f'{variety_code}%'),
MarketData.period == period,
).first()
if not market_data or not market_data.candles_json:
return {"success": False, "message": f"未找到 {kline_symbol}{period} K线数据"}
candles = json.loads(market_data.candles_json)
kline_symbol = market_data.symbol
finally:
main_db.close()
# 获取该品种的交易记录
query = db.query(TradeRecord).filter(TradeRecord.variety == variety_code)
if start_date:
query = query.filter(TradeRecord.trade_date >= start_date)
if end_date:
query = query.filter(TradeRecord.trade_date <= end_date)
trades = query.order_by(TradeRecord.trade_date, TradeRecord.trade_time).all()
trade_markers = []
for t in trades:
trade_markers.append({
"date": t.trade_date,
"time": t.trade_time,
"symbol": t.symbol,
"direction": t.direction,
"offset": t.offset,
"price": t.price,
"volume": t.volume,
"close_pnl": t.close_pnl,
"commission": t.commission,
})
return {
"success": True,
"data": {
"symbol": kline_symbol,
"period": period,
"candles": candles,
"trade_markers": trade_markers,
},
}
def _resolve_symbol_from_config(variety_code: str) -> str:
"""从品种配置中查找品种代码对应的当前合约"""
import json as _json
from pathlib import Path
config_path = Path(__file__).resolve().parent.parent.parent / "config" / "symbols_config.json"
if not config_path.exists():
return variety_code
with open(config_path, "r", encoding="utf-8") as f:
config = _json.load(f)
for name, contract in config.get("futures", {}).items():
if contract.upper().startswith(variety_code.upper()):
return contract
return variety_code
# ==================== AI 逐笔交易分析 ====================
class AnalyzeTradeRequest(BaseModel):
symbol: str
open_date: str
open_time: Optional[str] = None
close_date: Optional[str] = None
close_time: Optional[str] = None
direction: str # 多/空
open_price: Optional[float] = None
close_price: Optional[float] = None
@router.post("/analyze-trade")
async def analyze_trade(
req: AnalyzeTradeRequest,
db: Session = Depends(get_analysis_db),
):
"""AI 分析单笔交易结合多周期K线数据"""
from app.services.ai_analysis import AIAnalysisService
# 收集多周期K线数据
from app.database import SessionLocal as MainSessionLocal
from app.models import MarketData
import json
periods = ["daily", "60min", "15min", "5min"]
kline_context = {}
main_db = MainSessionLocal()
try:
for period in periods:
market_data = main_db.query(MarketData).filter(
MarketData.symbol == req.symbol,
MarketData.period == period,
).first()
if market_data and market_data.candles_json:
candles = json.loads(market_data.candles_json)
kline_context[period] = candles[-50:] if len(candles) > 50 else candles
finally:
main_db.close()
if not kline_context:
return {"success": False, "message": f"未找到 {req.symbol} 的K线数据无法分析"}
# 构建分析提示
prompt = f"""请分析以下期货交易交易的优缺点:
交易信息
- 品种{req.symbol}
- 方向{req.direction}
- 开仓时间{req.open_date} {req.open_time or ''}
- 开仓价格{req.open_price or '未知'}
- 平仓时间{req.close_date or '未知'} {req.close_time or ''}
- 平仓价格{req.close_price or '未知'}
各周期K线数据格式[日期, 开盘, 收盘, 最低, 最高, 成交量]
"""
for period, candles in kline_context.items():
prompt += f"\n{period} 周期(最近{len(candles)}根K线\n"
for c in candles[-10:]:
prompt += f" {c[0]}: 开{c[1]}{c[2]}{c[3]}{c[4]}{c[5]}\n"
prompt += """
请从以下维度分析这笔交易
1. 入场时机分析入场时各周期的趋势如何入场点是否合理
2. 出场时机分析出场时的市场状态是否过早/过晚出场
3. 持仓期间分析持仓期间市场走势是否有更好的操作机会
4. 综合评价这笔交易的主要优点和不足之处
5. 改进建议类似行情下如何优化操作
"""
try:
# 使用 AIAnalysisService 的模型配置和调用能力
service = AIAnalysisService.__new__(AIAnalysisService)
model = service.get_active_model()
if not model:
return {"success": False, "message": "未配置AI模型或模型未激活请先在AI配置页面设置"}
response = service.call_ai_model(prompt, model)
if not response:
return {"success": False, "message": "AI模型返回空响应请稍后重试"}
return {"success": True, "data": {"analysis": response, "symbol": req.symbol}}
except Exception as e:
logger.exception("AI分析交易失败")
return {"success": False, "message": f"AI分析失败: {str(e)}"}

@ -13,7 +13,7 @@ from fastapi.responses import FileResponse, RedirectResponse
from app.database import engine, Base from app.database import engine, Base
from app.user_models import Base as UserBase from app.user_models import Base as UserBase
from app.config import HOST, PORT, LOG_LEVEL from app.config import HOST, PORT, LOG_LEVEL
from app.api import data, tasks, config, futures_analysis, ai_config, auth, review_plan from app.api import data, tasks, config, futures_analysis, ai_config, auth, review_plan, trade_review
from app.services.scheduler import start_scheduler, stop_scheduler from app.services.scheduler import start_scheduler, stop_scheduler
# 配置日志 # 配置日志
@ -121,6 +121,7 @@ app.include_router(config.router, prefix="/api/v1")
app.include_router(futures_analysis.router, prefix="/api/v1") app.include_router(futures_analysis.router, prefix="/api/v1")
app.include_router(ai_config.router, prefix="/api/v1") app.include_router(ai_config.router, prefix="/api/v1")
app.include_router(review_plan.router, prefix="/api/v1") app.include_router(review_plan.router, prefix="/api/v1")
app.include_router(trade_review.router, prefix="/api/v1")
@app.get("/futures-analysis") @app.get("/futures-analysis")

@ -0,0 +1,609 @@
"""
交易结算单解析服务
复用 data/trading_calculate.py 的解析逻辑支持从 .xls 结算单文件中提取交易记录并存入数据库
"""
import re
import uuid
import json
from datetime import datetime
from collections import defaultdict
from pathlib import Path
import xlrd
import pandas as pd
from sqlalchemy.orm import Session as DBSession
from app.analysis_models import TradeRecord, TradeImportBatch
# 品种代码 -> 中文名 映射(反向映射:从合约代码前缀查中文名)
_VARIETY_NAME_MAP = {}
def _load_variety_name_map():
"""从 symbols_config.json 加载品种名称映射"""
global _VARIETY_NAME_MAP
if _VARIETY_NAME_MAP:
return
config_path = Path(__file__).resolve().parent.parent.parent / "config" / "symbols_config.json"
if config_path.exists():
with open(config_path, "r", encoding="utf-8") as f:
config = json.load(f)
# 正向: {"沪银": "AG2608"} -> 反向: {"AG": "沪银"}
for name, contract in config.get("futures", {}).items():
variety = extract_variety(contract)
if variety and variety not in _VARIETY_NAME_MAP:
_VARIETY_NAME_MAP[variety] = name
def extract_variety(contract):
"""提取品种代码(所有前导字母)"""
if pd.isna(contract) or not contract:
return ''
contract = str(contract).strip()
match = re.match(r'^([A-Za-z]+)', contract)
if match:
return match.group(1).upper()
return contract[:2].upper()
def safe_float(value, default=0.0):
"""安全转换为float"""
if pd.isna(value) or value is None or value == '':
return default
try:
return float(value)
except (ValueError, TypeError):
return default
def find_header(sheet, keywords):
"""
动态识别表头行
:param sheet: xlrd sheet对象
:param keywords: 关键词列表
:return: 表头行索引
"""
for row_idx in range(min(20, sheet.nrows)):
row_values = [str(sheet.cell_value(row_idx, col_idx)).strip()
for col_idx in range(sheet.ncols)]
row_text = ' '.join(row_values)
has_keyword = any(kw in row_text for kw in keywords)
has_aux = any(aux in row_text for aux in ["买/卖", "成交价", "权利金"])
if has_keyword and has_aux:
return row_idx
return None
def read_sheet(xls_file_path, sheet_name, trade_type):
"""
读取单个Sheet的数据
:param xls_file_path: Excel文件路径str bytes
:param sheet_name: Sheet名称
:param trade_type: 交易类型期货/期权
:return: DataFrame
"""
try:
wb = xlrd.open_workbook(xls_file_path)
if sheet_name not in wb.sheet_names():
return pd.DataFrame()
sheet = wb.sheet_by_name(sheet_name)
if sheet.nrows == 0:
return pd.DataFrame()
if trade_type == '期货':
keywords = ["合约"]
else:
keywords = ["品种合约"]
header_row = find_header(sheet, keywords)
if header_row is None:
return pd.DataFrame()
headers = [str(sheet.cell_value(header_row, col_idx)).strip()
for col_idx in range(sheet.ncols)]
data = []
for row_idx in range(header_row + 1, sheet.nrows):
row_data = {}
first_cell = str(sheet.cell_value(row_idx, 0)).strip()
if first_cell == '合计' or first_cell == '':
continue
for col_idx, header in enumerate(headers):
if col_idx < sheet.ncols:
value = sheet.cell_value(row_idx, col_idx)
if str(value).strip() == '--':
value = None
row_data[header] = value
row_data['类型'] = trade_type
data.append(row_data)
return pd.DataFrame(data)
except Exception:
return pd.DataFrame()
def parse_settlement_file(file_content: bytes, filename: str) -> tuple[pd.DataFrame, pd.DataFrame]:
"""
解析结算单文件返回期货和期权交易DataFrame
:param file_content: 文件二进制内容
:param filename: 文件名
:return: (df_futures, df_options)
"""
import tempfile
# xlrd 需要文件路径,写入临时文件
with tempfile.NamedTemporaryFile(suffix='.xls', delete=False) as tmp:
tmp.write(file_content)
tmp_path = tmp.name
try:
df_futures = read_sheet(tmp_path, '成交明细', '期货')
df_options = read_sheet(tmp_path, '期权成交明细', '期权')
return df_futures, df_options
finally:
Path(tmp_path).unlink(missing_ok=True)
def _normalize_date(date_val) -> str:
"""将各种日期格式统一为 YYYY-MM-DD"""
if pd.isna(date_val) or date_val is None or date_val == '':
return ''
s = str(date_val).strip()
# 已经是 YYYY-MM-DD
if re.match(r'^\d{4}-\d{2}-\d{2}$', s):
return s
# YYYY/MM/DD
if re.match(r'^\d{4}/\d{2}/\d{2}$', s):
return s.replace('/', '-')
# YYYYMMDD
if re.match(r'^\d{8}$', s):
return f"{s[:4]}-{s[4:6]}-{s[6:8]}"
# xlrd 日期数字 (Excel serial date)
try:
f = float(s)
if 30000 < f < 60000: # 合理的 Excel 日期数字范围
dt = xlrd.xldate_as_datetime(f, 0)
return dt.strftime('%Y-%m-%d')
except (ValueError, TypeError):
pass
return s
def _normalize_time(time_val) -> str:
"""将各种时间格式统一为 HH:MM:SS"""
if pd.isna(time_val) or time_val is None or time_val == '':
return ''
s = str(time_val).strip()
# 已经是 HH:MM:SS
if re.match(r'^\d{1,2}:\d{2}:\d{2}$', s):
return s
# HH:MM
if re.match(r'^\d{1,2}:\d{2}$', s):
return s + ':00'
# xlrd 时间数字
try:
f = float(s)
if 0 <= f < 1:
hours = int(f * 24)
minutes = int((f * 24 - hours) * 60)
seconds = int(((f * 24 - hours) * 60 - minutes) * 60)
return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
except (ValueError, TypeError):
pass
return s
def save_to_db(
db: DBSession,
df_futures: pd.DataFrame,
df_options: pd.DataFrame,
filename: str,
) -> dict:
"""
将解析后的交易记录保存到数据库
:return: {"batch_id": str, "futures_count": int, "options_count": int, "trade_dates": str}
"""
_load_variety_name_map()
batch_id = str(uuid.uuid4())
all_dates = set()
futures_count = 0
options_count = 0
records = []
# 处理期货记录
if not df_futures.empty:
for _, row in df_futures.iterrows():
contract = str(row.get('合约', '')).strip()
if not contract or contract == '合计':
continue
variety = extract_variety(contract)
trade_date = _normalize_date(row.get('实际成交日期', ''))
trade_time = _normalize_time(row.get('成交时间', ''))
bs_flag = str(row.get('买/卖', '')).strip()
oc_flag = str(row.get('开/平', '')).strip()
rec = TradeRecord(
trade_type='期货',
symbol=contract,
variety=variety,
symbol_name=_VARIETY_NAME_MAP.get(variety, ''),
direction='' if '' in bs_flag else '',
offset='' if '' in oc_flag else ('' if '' in oc_flag else ''),
price=safe_float(row.get('成交价')),
volume=safe_float(row.get('手数')),
amount=safe_float(row.get('成交额')),
close_pnl=safe_float(row.get('平仓盈亏')),
commission=safe_float(row.get('手续费')),
trade_date=trade_date,
trade_time=trade_time,
import_batch=batch_id,
source_file=filename,
)
records.append(rec)
futures_count += 1
if trade_date:
all_dates.add(trade_date)
# 处理期权记录
if not df_options.empty:
for _, row in df_options.iterrows():
contract = str(row.get('品种合约', '')).strip()
if not contract or contract == '合计':
continue
variety = extract_variety(contract)
trade_date = _normalize_date(row.get('成交日期', ''))
trade_time = _normalize_time(row.get('成交时间', ''))
bs_flag = str(row.get('买/卖', '')).strip()
rec = TradeRecord(
trade_type='期权',
symbol=contract,
variety=variety,
symbol_name=_VARIETY_NAME_MAP.get(variety, ''),
direction='' if '' in bs_flag else '',
offset='',
price=safe_float(row.get('权利金单价')),
volume=safe_float(row.get('成交量')),
amount=safe_float(row.get('权利金')),
close_pnl=0.0,
commission=safe_float(row.get('手续费')),
trade_date=trade_date,
trade_time=trade_time,
import_batch=batch_id,
source_file=filename,
)
records.append(rec)
options_count += 1
if trade_date:
all_dates.add(trade_date)
if records:
db.add_all(records)
# 日期范围描述
sorted_dates = sorted(all_dates)
trade_dates_str = ''
if sorted_dates:
if len(sorted_dates) == 1:
trade_dates_str = sorted_dates[0]
else:
trade_dates_str = f"{sorted_dates[0]} ~ {sorted_dates[-1]}"
# 保存批次记录
batch = TradeImportBatch(
batch_id=batch_id,
source_file=filename,
futures_count=futures_count,
options_count=options_count,
trade_dates=trade_dates_str,
)
db.add(batch)
db.commit()
return {
"batch_id": batch_id,
"futures_count": futures_count,
"options_count": options_count,
"trade_dates": trade_dates_str,
}
def calc_daily_summary(db: DBSession, start_date: str = None, end_date: str = None) -> list[dict]:
"""按日期汇总交易盈亏"""
query = db.query(TradeRecord)
if start_date:
query = query.filter(TradeRecord.trade_date >= start_date)
if end_date:
query = query.filter(TradeRecord.trade_date <= end_date)
query = query.order_by(TradeRecord.trade_date)
records = query.all()
daily = defaultdict(lambda: {
"trade_date": "",
"total_trades": 0,
"total_pnl": 0.0,
"total_commission": 0.0,
"win_count": 0,
"loss_count": 0,
"max_win": 0.0,
"max_loss": 0.0,
"buy_volume": 0.0,
"sell_volume": 0.0,
"varieties": set(),
})
for r in records:
d = r.trade_date or '未知'
day = daily[d]
day["trade_date"] = d
day["total_trades"] += 1
pnl = (r.close_pnl or 0) - (r.commission or 0)
day["total_pnl"] += pnl
day["total_commission"] += (r.commission or 0)
if pnl > 0:
day["win_count"] += 1
day["max_win"] = max(day["max_win"], pnl)
elif pnl < 0:
day["loss_count"] += 1
day["max_loss"] = min(day["max_loss"], pnl)
if r.direction == '':
day["buy_volume"] += (r.volume or 0)
else:
day["sell_volume"] += (r.volume or 0)
day["varieties"].add(r.variety)
result = []
for d in sorted(daily.keys()):
day = daily[d]
total = day["win_count"] + day["loss_count"]
result.append({
"trade_date": day["trade_date"],
"total_trades": day["total_trades"],
"total_pnl": round(day["total_pnl"], 2),
"total_commission": round(day["total_commission"], 2),
"win_count": day["win_count"],
"loss_count": day["loss_count"],
"win_rate": round(day["win_count"] / total * 100, 1) if total > 0 else 0,
"max_win": round(day["max_win"], 2),
"max_loss": round(day["max_loss"], 2),
"buy_volume": day["buy_volume"],
"sell_volume": day["sell_volume"],
"variety_count": len(day["varieties"]),
})
return result
def calc_variety_summary(db: DBSession, start_date: str = None, end_date: str = None) -> list[dict]:
"""按品种汇总交易盈亏"""
query = db.query(TradeRecord)
if start_date:
query = query.filter(TradeRecord.trade_date >= start_date)
if end_date:
query = query.filter(TradeRecord.trade_date <= end_date)
records = query.all()
varieties = defaultdict(lambda: {
"variety": "",
"symbol_name": "",
"total_trades": 0,
"total_pnl": 0.0,
"total_commission": 0.0,
"total_close_pnl": 0.0,
"total_volume": 0.0,
"total_amount": 0.0,
"buy_count": 0,
"sell_count": 0,
"win_count": 0,
"loss_count": 0,
})
for r in records:
v = varieties[r.variety]
v["variety"] = r.variety
v["symbol_name"] = r.symbol_name or v["symbol_name"]
v["total_trades"] += 1
v["total_close_pnl"] += (r.close_pnl or 0)
v["total_commission"] += (r.commission or 0)
v["total_pnl"] += (r.close_pnl or 0) - (r.commission or 0)
v["total_volume"] += (r.volume or 0)
v["total_amount"] += (r.amount or 0)
if r.direction == '':
v["buy_count"] += 1
else:
v["sell_count"] += 1
pnl = (r.close_pnl or 0) - (r.commission or 0)
if pnl > 0:
v["win_count"] += 1
elif pnl < 0:
v["loss_count"] += 1
result = []
for variety in sorted(varieties.keys()):
v = varieties[variety]
total = v["win_count"] + v["loss_count"]
result.append({
"variety": v["variety"],
"symbol_name": v["symbol_name"],
"total_trades": v["total_trades"],
"total_pnl": round(v["total_pnl"], 2),
"total_close_pnl": round(v["total_close_pnl"], 2),
"total_commission": round(v["total_commission"], 2),
"total_volume": v["total_volume"],
"total_amount": round(v["total_amount"], 2),
"buy_count": v["buy_count"],
"sell_count": v["sell_count"],
"win_count": v["win_count"],
"loss_count": v["loss_count"],
"win_rate": round(v["win_count"] / total * 100, 1) if total > 0 else 0,
})
return result
def calc_overall_statistics(db: DBSession, start_date: str = None, end_date: str = None) -> dict:
"""计算整体交易统计"""
daily = calc_daily_summary(db, start_date, end_date)
if not daily:
return {
"total_trades": 0,
"total_pnl": 0,
"total_commission": 0,
"win_rate": 0,
"profit_loss_ratio": 0,
"max_single_win": 0,
"max_single_loss": 0,
"max_consecutive_wins": 0,
"max_consecutive_losses": 0,
"trading_days": 0,
"avg_daily_pnl": 0,
"max_daily_pnl": 0,
"min_daily_pnl": 0,
}
total_trades = sum(d["total_trades"] for d in daily)
total_pnl = sum(d["total_pnl"] for d in daily)
total_commission = sum(d["total_commission"] for d in daily)
total_wins = sum(d["win_count"] for d in daily)
total_losses = sum(d["loss_count"] for d in daily)
total_decided = total_wins + total_losses
# 最大连续盈/亏
max_consec_wins = 0
max_consec_losses = 0
cur_wins = 0
cur_losses = 0
for d in daily:
if d["total_pnl"] > 0:
cur_wins += 1
cur_losses = 0
max_consec_wins = max(max_consec_wins, cur_wins)
elif d["total_pnl"] < 0:
cur_losses += 1
cur_wins = 0
max_consec_losses = max(max_consec_losses, cur_losses)
else:
cur_wins = 0
cur_losses = 0
# 平均日盈亏
pnl_values = [d["total_pnl"] for d in daily]
# 盈亏比 - 用逐笔数据计算
query = db.query(TradeRecord)
if start_date:
query = query.filter(TradeRecord.trade_date >= start_date)
if end_date:
query = query.filter(TradeRecord.trade_date <= end_date)
all_records = query.all()
wins_pnl = []
losses_pnl = []
for r in all_records:
pnl = (r.close_pnl or 0) - (r.commission or 0)
if pnl > 0:
wins_pnl.append(pnl)
elif pnl < 0:
losses_pnl.append(pnl)
avg_win_val = sum(wins_pnl) / len(wins_pnl) if wins_pnl else 0
avg_loss_val = abs(sum(losses_pnl) / len(losses_pnl)) if losses_pnl else 0
profit_loss_ratio = round(avg_win_val / avg_loss_val, 2) if avg_loss_val > 0 else 0
return {
"total_trades": total_trades,
"total_pnl": round(total_pnl, 2),
"total_commission": round(total_commission, 2),
"win_rate": round(total_wins / total_decided * 100, 1) if total_decided > 0 else 0,
"profit_loss_ratio": profit_loss_ratio,
"max_single_win": round(max(wins_pnl), 2) if wins_pnl else 0,
"max_single_loss": round(min(losses_pnl), 2) if losses_pnl else 0,
"max_consecutive_wins": max_consec_wins,
"max_consecutive_losses": max_consec_losses,
"trading_days": len(daily),
"avg_daily_pnl": round(sum(pnl_values) / len(pnl_values), 2) if pnl_values else 0,
"max_daily_pnl": round(max(pnl_values), 2) if pnl_values else 0,
"min_daily_pnl": round(min(pnl_values), 2) if pnl_values else 0,
}
def get_trade_pairs(db: DBSession, start_date: str = None, end_date: str = None) -> list[dict]:
"""
将开平仓配对生成逐笔交易对
按合约+品种分组按时间排序买开/卖开 卖平/买平 配对
"""
query = db.query(TradeRecord)
if start_date:
query = query.filter(TradeRecord.trade_date >= start_date)
if end_date:
query = query.filter(TradeRecord.trade_date <= end_date)
query = query.order_by(TradeRecord.symbol, TradeRecord.trade_date, TradeRecord.trade_time)
records = query.all()
# 按合约分组
by_symbol = defaultdict(list)
for r in records:
by_symbol[r.symbol].append(r)
pairs = []
pair_id = 0
for symbol, recs in by_symbol.items():
open_positions = [] # 未平仓记录
for r in recs:
if r.offset == '':
open_positions.append(r)
elif r.offset == '' and open_positions:
# 配对:取最早的一条开仓
open_rec = open_positions.pop(0)
pair_id += 1
pnl = (r.close_pnl or 0) - (open_rec.commission or 0) - (r.commission or 0)
direction = '' if open_rec.direction == '' else ''
pairs.append({
"id": pair_id,
"symbol": symbol,
"variety": r.variety,
"symbol_name": r.symbol_name,
"direction": direction,
"open_date": open_rec.trade_date,
"open_time": open_rec.trade_time,
"open_price": open_rec.price,
"close_date": r.trade_date,
"close_time": r.trade_time,
"close_price": r.price,
"volume": open_rec.volume,
"close_pnl": round((r.close_pnl or 0), 2),
"commission": round((open_rec.commission or 0) + (r.commission or 0), 2),
"net_pnl": round(pnl, 2),
"open_batch": open_rec.import_batch,
"close_batch": r.import_batch,
})
elif r.offset == '' or r.offset is None:
# 期权等无开平标记的,按买卖交替配对
if open_positions:
open_rec = open_positions.pop(0)
pair_id += 1
pairs.append({
"id": pair_id,
"symbol": symbol,
"variety": r.variety,
"symbol_name": r.symbol_name,
"direction": '' if open_rec.direction == '' else '',
"open_date": open_rec.trade_date,
"open_time": open_rec.trade_time,
"open_price": open_rec.price,
"close_date": r.trade_date,
"close_time": r.trade_time,
"close_price": r.price,
"volume": open_rec.volume,
"close_pnl": 0,
"commission": round((open_rec.commission or 0) + (r.commission or 0), 2),
"net_pnl": 0,
"open_batch": open_rec.import_batch,
"close_batch": r.import_batch,
})
else:
open_positions.append(r)
return pairs

@ -488,6 +488,96 @@
.rv-spinner { width: 40px; height: 40px; border: 3px solid #F5F5F7; border-top-color: var(--color-brand); border-radius: 50%; animation: rv-spin 1s linear infinite; margin: 0 auto 16px; } .rv-spinner { width: 40px; height: 40px; border: 3px solid #F5F5F7; border-top-color: var(--color-brand); border-radius: 50%; animation: rv-spin 1s linear infinite; margin: 0 auto 16px; }
@keyframes rv-spin { to { transform: rotate(360deg); } } @keyframes rv-spin { to { transform: rotate(360deg); } }
/* ============================================
交易复盘页面样式
============================================ */
#trade-review-view { background: var(--bg-page); border-radius: 20px; padding: 24px 0; min-height: 600px; }
.tr-toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; flex-wrap: wrap; gap: 12px; }
.tr-toolbar-left, .tr-toolbar-right { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.tr-btn { padding: 10px 20px; border-radius: 12px; font-size: 14px; font-weight: 600; cursor: pointer; border: none; display: flex; align-items: center; gap: 8px; transition: all 0.2s; font-family: inherit; }
.tr-btn-primary { background: var(--color-brand); color: #fff; }
.tr-btn-primary:hover { background: #0066d6; box-shadow: 0 4px 10px rgba(0,122,255,0.3); }
.tr-btn-secondary { background: #fff; color: var(--text-secondary); box-shadow: var(--shadow-sm); border: 1px solid rgba(0,0,0,0.05); }
.tr-btn-secondary:hover { background: #F5F5F7; }
.tr-btn-danger { background: rgba(255,59,48,0.1); color: var(--color-up); border: 1px solid rgba(255,59,48,0.2); }
.tr-btn-danger:hover { background: rgba(255,59,48,0.2); }
.tr-date-input { background: #fff; border: 1px solid rgba(0,0,0,0.05); border-radius: 12px; padding: 0 14px; height: 40px; font-size: 13px; color: var(--text-primary); box-shadow: var(--shadow-sm); outline: none; font-family: inherit; }
.tr-date-input:focus { border-color: var(--color-brand); box-shadow: 0 0 0 3px rgba(0,122,255,0.15); }
.tr-select { background: #fff; border: 1px solid rgba(0,0,0,0.05); border-radius: 12px; padding: 0 14px; height: 40px; font-size: 13px; color: var(--text-primary); box-shadow: var(--shadow-sm); outline: none; font-family: inherit; cursor: pointer; }
.tr-sub-nav { display: flex; gap: 8px; margin-bottom: 20px; overflow-x: auto; padding-bottom: 4px; }
.tr-sub-nav-item { padding: 8px 18px; border-radius: 9999px; font-size: 13px; background: #fff; color: var(--text-secondary); cursor: pointer; white-space: nowrap; transition: all 0.2s; text-decoration: none; box-shadow: var(--shadow-sm); font-weight: 500; }
.tr-sub-nav-item:hover { background: var(--color-brand); color: #fff; }
.tr-sub-nav-item.active { background: var(--color-brand); color: #fff; box-shadow: 0 4px 10px rgba(0,122,255,0.2); }
.tr-stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; margin-bottom: 20px; }
.tr-stat-card { background: var(--bg-card); border-radius: 16px; padding: 18px; text-align: center; box-shadow: var(--shadow-sm); transition: transform 0.2s; }
.tr-stat-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
.tr-stat-label { font-size: 11px; color: var(--text-tertiary); margin-bottom: 6px; text-transform: uppercase; font-weight: 600; }
.tr-stat-value { font-size: 22px; font-weight: 700; }
.tr-stat-value.profit { color: var(--color-down); }
.tr-stat-value.loss { color: var(--color-up); }
.tr-stat-sub { font-size: 12px; color: var(--text-tertiary); margin-top: 4px; }
.tr-sub-page { display: none; }
.tr-sub-page.active { display: block; }
.tr-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.tr-table th { background: #F5F5F7; padding: 10px 12px; text-align: left; font-weight: 600; color: var(--text-tertiary); font-size: 12px; white-space: nowrap; }
.tr-table td { padding: 9px 12px; border-bottom: 1px solid rgba(0,0,0,0.04); color: var(--text-primary); white-space: nowrap; }
.tr-table tbody tr { transition: background 0.15s; }
.tr-table tbody tr:hover { background: rgba(0,122,255,0.03); }
.tr-pnl-positive { color: var(--color-down); font-weight: 600; }
.tr-pnl-negative { color: var(--color-up); font-weight: 600; }
.tr-dir-buy { color: var(--color-down); font-weight: 500; }
.tr-dir-sell { color: var(--color-up); font-weight: 500; }
.tr-pagination { display: flex; justify-content: center; align-items: center; gap: 8px; margin-top: 16px; }
.tr-pagination button { padding: 6px 14px; border-radius: 8px; border: 1px solid rgba(0,0,0,0.08); background: #fff; font-size: 12px; cursor: pointer; transition: all 0.2s; }
.tr-pagination button:hover { background: var(--color-brand); color: #fff; border-color: var(--color-brand); }
.tr-pagination button:disabled { opacity: 0.4; cursor: default; }
.tr-pagination span { font-size: 12px; color: var(--text-tertiary); }
.tr-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 300px; gap: 12px; }
.tr-empty-icon { font-size: 56px; opacity: 0.3; }
.tr-empty-text { font-size: 15px; color: var(--text-tertiary); }
.tr-kline-controls { display: flex; gap: 10px; margin-bottom: 16px; align-items: center; }
.tr-kline-chart { width: 100%; height: 500px; background: var(--bg-card); border-radius: 16px; box-shadow: var(--shadow-sm); }
.tr-chart-container { width: 100%; height: 300px; margin-bottom: 16px; background: var(--bg-card); border-radius: 16px; box-shadow: var(--shadow-sm); }
.tr-pairs-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 16px; }
.tr-pair-card { background: var(--bg-card); border-radius: 16px; padding: 18px; box-shadow: var(--shadow-sm); transition: all 0.2s; position: relative; overflow: hidden; }
.tr-pair-card:hover { box-shadow: var(--shadow-md); transform: translateY(-2px); }
.tr-pair-card::before { content: ""; position: absolute; top: 0; left: 0; right: 0; height: 3px; }
.tr-pair-card.profit::before { background: var(--color-down); }
.tr-pair-card.loss::before { background: var(--color-up); }
.tr-pair-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.tr-pair-symbol { font-size: 15px; font-weight: 700; color: var(--text-primary); }
.tr-pair-pnl { font-size: 16px; font-weight: 700; padding: 4px 12px; border-radius: 8px; }
.tr-pair-pnl.profit { color: var(--color-down); background: rgba(52,199,89,0.1); }
.tr-pair-pnl.loss { color: var(--color-up); background: rgba(255,59,48,0.1); }
.tr-pair-info { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; font-size: 12px; }
.tr-pair-info-item { display: flex; justify-content: space-between; padding: 4px 8px; background: #F5F5F7; border-radius: 6px; }
.tr-pair-info-label { color: var(--text-tertiary); }
.tr-pair-info-value { font-weight: 600; color: var(--text-primary); }
.tr-pair-actions { margin-top: 12px; display: flex; gap: 8px; }
.tr-pair-actions button { flex: 1; padding: 8px; border-radius: 8px; border: none; font-size: 12px; font-weight: 600; cursor: pointer; transition: all 0.2s; font-family: inherit; }
.tr-pair-btn-analyze { background: rgba(175,82,222,0.1); color: var(--color-ai); }
.tr-pair-btn-analyze:hover { background: rgba(175,82,222,0.2); }
.tr-batch-item { display: flex; justify-content: space-between; align-items: center; padding: 14px 16px; background: #F5F5F7; border-radius: 12px; margin-bottom: 10px; }
.tr-batch-info { flex: 1; }
.tr-batch-file { font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 4px; }
.tr-batch-meta { font-size: 12px; color: var(--text-tertiary); }
.tr-batch-delete { padding: 6px 14px; border-radius: 8px; border: none; background: rgba(255,59,48,0.1); color: var(--color-up); font-size: 12px; font-weight: 600; cursor: pointer; transition: all 0.2s; }
.tr-batch-delete:hover { background: rgba(255,59,48,0.2); }
@media (max-width: 768px) {
.tr-stats-grid { grid-template-columns: repeat(2, 1fr); }
.tr-pairs-grid { grid-template-columns: 1fr; }
}
@media (max-width: 768px) { @media (max-width: 768px) {
.rv-stats-grid { grid-template-columns: 1fr; } .rv-stats-grid { grid-template-columns: 1fr; }
.rv-green-grid { grid-template-columns: 1fr; } .rv-green-grid { grid-template-columns: 1fr; }
@ -506,6 +596,7 @@
<a href="#" class="nav-item" data-page="market">市场概览</a> <a href="#" class="nav-item" data-page="market">市场概览</a>
<a href="#" class="nav-item" data-page="risk">风险预警</a> <a href="#" class="nav-item" data-page="risk">风险预警</a>
<a href="#" class="nav-item" data-page="review">复盘计划</a> <a href="#" class="nav-item" data-page="review">复盘计划</a>
<a href="#" class="nav-item" data-page="trade-review">交易复盘</a>
</div> </div>
<div class="live-badge"> <div class="live-badge">
<div class="dot"></div> LIVE <div class="dot"></div> LIVE
@ -868,6 +959,161 @@
<div class="rv-empty-text">请选择日期或点击"复盘与计划"按钮生成分析报告</div> <div class="rv-empty-text">请选择日期或点击"复盘与计划"按钮生成分析报告</div>
</div> </div>
</div> </div>
<!-- 交易复盘视图 -->
<div id="trade-review-view" class="view">
<!-- 工具栏 -->
<div class="tr-toolbar">
<div class="tr-toolbar-left">
<input type="file" id="tr-file-input" accept=".xls,.xlsx" style="display:none" />
<button id="tr-btn-import" class="tr-btn tr-btn-primary">
<span>📥</span><span>导入结算单</span>
</button>
<input type="date" id="tr-start-date" class="tr-date-input" title="开始日期" />
<span style="color:var(--text-tertiary);font-size:13px;">~</span>
<input type="date" id="tr-end-date" class="tr-date-input" title="结束日期" />
<button id="tr-btn-query" class="tr-btn tr-btn-secondary">查询</button>
</div>
<div class="tr-toolbar-right">
<button id="tr-btn-delete-date" class="tr-btn tr-btn-danger" title="删除当前查询日期的所有交易记录">
<span>🗑️</span><span>删除当日</span>
</button>
<button id="tr-btn-batches" class="tr-btn tr-btn-secondary">
<span>📋</span><span>批次管理</span>
</button>
</div>
</div>
<!-- 子导航 -->
<nav class="tr-sub-nav">
<a href="#" class="tr-sub-nav-item active" data-sub="records">交易记录</a>
<a href="#" class="tr-sub-nav-item" data-sub="kline">K线标注</a>
<a href="#" class="tr-sub-nav-item" data-sub="daily">每日分析</a>
<a href="#" class="tr-sub-nav-item" data-sub="variety">品种汇总</a>
<a href="#" class="tr-sub-nav-item" data-sub="pairs">逐笔分析</a>
</nav>
<!-- 统计卡片 -->
<div id="tr-stats-grid" class="tr-stats-grid"></div>
<!-- 子页面: 交易记录 -->
<div id="tr-sub-records" class="tr-sub-page active">
<div id="tr-records-table-wrap">
<table class="tr-table">
<thead>
<tr>
<th>日期</th><th>时间</th><th>类型</th><th>合约</th><th>品种</th>
<th>买卖</th><th>开平</th><th>价格</th><th>手数</th>
<th>成交额</th><th>平仓盈亏</th><th>手续费</th><th>文件</th>
</tr>
</thead>
<tbody id="tr-records-body"></tbody>
</table>
</div>
<div id="tr-records-pagination" class="tr-pagination"></div>
<div id="tr-records-empty" class="tr-empty" style="display:none;">
<div class="tr-empty-icon">📥</div>
<div class="tr-empty-text">暂无交易记录,请先导入结算单文件</div>
</div>
</div>
<!-- 子页面: K线标注 -->
<div id="tr-sub-kline" class="tr-sub-page">
<div class="tr-kline-controls">
<select id="tr-kline-symbol" class="tr-select">
<option value="">选择品种</option>
</select>
<select id="tr-kline-period" class="tr-select">
<option value="daily">日线</option>
<option value="60min">60分钟</option>
<option value="15min">15分钟</option>
<option value="5min">5分钟</option>
</select>
<button id="tr-btn-load-kline" class="tr-btn tr-btn-primary">加载K线</button>
</div>
<div id="tr-kline-chart" class="tr-kline-chart"></div>
<div id="tr-kline-empty" class="tr-empty" style="display:none;">
<div class="tr-empty-icon">📈</div>
<div class="tr-empty-text">请选择品种并加载K线数据</div>
</div>
</div>
<!-- 子页面: 每日分析 -->
<div id="tr-sub-daily" class="tr-sub-page">
<div id="tr-daily-chart" class="tr-chart-container"></div>
<div style="overflow-x:auto;">
<table class="tr-table">
<thead>
<tr>
<th>日期</th><th>交易笔数</th><th>净盈亏</th><th>手续费</th>
<th>盈利笔</th><th>亏损笔</th><th>胜率</th>
<th>最大盈利</th><th>最大亏损</th><th>品种数</th>
</tr>
</thead>
<tbody id="tr-daily-body"></tbody>
</table>
</div>
<div id="tr-daily-empty" class="tr-empty" style="display:none;">
<div class="tr-empty-icon">📊</div>
<div class="tr-empty-text">暂无每日数据</div>
</div>
</div>
<!-- 子页面: 品种汇总 -->
<div id="tr-sub-variety" class="tr-sub-page">
<div style="overflow-x:auto;">
<table class="tr-table">
<thead>
<tr>
<th>品种</th><th>名称</th><th>交易笔数</th><th>净盈亏</th>
<th>平仓盈亏</th><th>手续费</th><th>总手数</th>
<th>买入</th><th>卖出</th><th>胜率</th>
</tr>
</thead>
<tbody id="tr-variety-body"></tbody>
</table>
</div>
<div id="tr-variety-empty" class="tr-empty" style="display:none;">
<div class="tr-empty-icon">📋</div>
<div class="tr-empty-text">暂无品种数据</div>
</div>
</div>
<!-- 子页面: 逐笔分析 -->
<div id="tr-sub-pairs" class="tr-sub-page">
<div id="tr-pairs-grid" class="tr-pairs-grid"></div>
<div id="tr-pairs-empty" class="tr-empty" style="display:none;">
<div class="tr-empty-icon">🔍</div>
<div class="tr-empty-text">暂无配对交易数据</div>
</div>
</div>
</div>
<!-- 批次管理弹窗 -->
<div id="tr-batch-modal" class="rv-modal-overlay" onclick="if(event.target===this)trCloseBatchModal()">
<div class="rv-modal" style="max-width:600px;">
<div class="rv-modal-header">
<span class="rv-modal-title">导入批次管理</span>
<button class="rv-modal-close" onclick="trCloseBatchModal()"></button>
</div>
<div class="rv-modal-body">
<div id="tr-batch-list"></div>
</div>
</div>
</div>
<!-- AI分析结果弹窗 -->
<div id="tr-analysis-modal" class="rv-modal-overlay" onclick="if(event.target===this)trCloseAnalysisModal()">
<div class="rv-modal" style="max-width:700px;">
<div class="rv-modal-header">
<span class="rv-modal-title" id="tr-analysis-title">AI 交易分析</span>
<button class="rv-modal-close" onclick="trCloseAnalysisModal()"></button>
</div>
<div class="rv-modal-body">
<div id="tr-analysis-content" style="white-space:pre-wrap;font-size:14px;line-height:1.8;color:var(--text-primary);"></div>
</div>
</div>
</div>
</div> </div>
<!-- 复盘品种详情弹窗(移到 review-view 外部) --> <!-- 复盘品种详情弹窗(移到 review-view 外部) -->

@ -136,6 +136,8 @@ function initEventListeners() {
if (page === 'review') { if (page === 'review') {
showReviewView(); showReviewView();
} else if (page === 'trade-review') {
showTradeReviewView();
} else if (page === 'watched') { } else if (page === 'watched') {
showWatchedView(); showWatchedView();
} else { } else {
@ -154,6 +156,7 @@ function showListView() {
document.getElementById('detail-view').classList.remove('active'); document.getElementById('detail-view').classList.remove('active');
document.getElementById('review-view').classList.remove('active'); document.getElementById('review-view').classList.remove('active');
document.getElementById('watched-view').classList.remove('active'); document.getElementById('watched-view').classList.remove('active');
document.getElementById('trade-review-view').classList.remove('active');
if (klineChart) { if (klineChart) {
klineChart.dispose(); klineChart.dispose();
klineChart = null; klineChart = null;
@ -165,6 +168,7 @@ function showWatchedView() {
document.getElementById('detail-view').classList.remove('active'); document.getElementById('detail-view').classList.remove('active');
document.getElementById('review-view').classList.remove('active'); document.getElementById('review-view').classList.remove('active');
document.getElementById('watched-view').classList.add('active'); document.getElementById('watched-view').classList.add('active');
document.getElementById('trade-review-view').classList.remove('active');
if (klineChart) { if (klineChart) {
klineChart.dispose(); klineChart.dispose();
klineChart = null; klineChart = null;
@ -177,6 +181,8 @@ function showReviewView() {
document.getElementById('list-view').classList.remove('active'); document.getElementById('list-view').classList.remove('active');
document.getElementById('detail-view').classList.remove('active'); document.getElementById('detail-view').classList.remove('active');
document.getElementById('review-view').classList.add('active'); document.getElementById('review-view').classList.add('active');
document.getElementById('watched-view').classList.remove('active');
document.getElementById('trade-review-view').classList.remove('active');
if (klineChart) { if (klineChart) {
klineChart.dispose(); klineChart.dispose();
klineChart = null; klineChart = null;
@ -186,6 +192,20 @@ function showReviewView() {
initReviewPlan(); initReviewPlan();
} }
function showTradeReviewView() {
document.getElementById('list-view').classList.remove('active');
document.getElementById('detail-view').classList.remove('active');
document.getElementById('review-view').classList.remove('active');
document.getElementById('watched-view').classList.remove('active');
document.getElementById('trade-review-view').classList.add('active');
if (klineChart) {
klineChart.dispose();
klineChart = null;
}
initTradeReview();
}
async function showDetailView(symbol) { async function showDetailView(symbol) {
currentSymbol = symbol; currentSymbol = symbol;
document.getElementById('list-view').classList.remove('active'); document.getElementById('list-view').classList.remove('active');
@ -607,6 +627,697 @@ function renderFuturesGrid(data) {
`}).join(''); `}).join('');
} }
// ==================== 交易复盘功能 ====================
const TR_API_BASE = '/api/v1/trade-review';
let trCurrentPage = 1;
let trKlineChart = null;
let trInitialized = false;
function initTradeReview() {
if (trInitialized) return;
trInitialized = true;
// 导入按钮
document.getElementById('tr-btn-import').addEventListener('click', () => {
document.getElementById('tr-file-input').click();
});
document.getElementById('tr-file-input').addEventListener('change', trHandleFileImport);
// 查询按钮
document.getElementById('tr-btn-query').addEventListener('click', () => {
trCurrentPage = 1;
trLoadAllData();
});
// 删除当日按钮
document.getElementById('tr-btn-delete-date').addEventListener('click', trDeleteByDate);
// 批次管理
document.getElementById('tr-btn-batches').addEventListener('click', trShowBatchModal);
// 子导航切换
document.querySelectorAll('.tr-sub-nav-item').forEach(item => {
item.addEventListener('click', function(e) {
e.preventDefault();
document.querySelectorAll('.tr-sub-nav-item').forEach(i => i.classList.remove('active'));
this.classList.add('active');
const sub = this.dataset.sub;
document.querySelectorAll('.tr-sub-page').forEach(p => p.classList.remove('active'));
document.getElementById('tr-sub-' + sub).classList.add('active');
// 延迟加载
if (sub === 'kline') trLoadKlineSymbolOptions();
});
});
// K线加载
document.getElementById('tr-btn-load-kline').addEventListener('click', trLoadKlineWithTrades);
// 初始加载:先获取最后交易日,设为默认日期,再加载数据
trInitDefaultDate();
}
async function trInitDefaultDate() {
try {
const res = await fetch(`${TR_API_BASE}/latest-trade-date`);
const json = await res.json();
if (json.success && json.data.trade_date) {
const dateStr = json.data.trade_date;
document.getElementById('tr-start-date').value = dateStr;
document.getElementById('tr-end-date').value = dateStr;
}
} catch (e) {
console.error('获取最后交易日失败', e);
}
trLoadAllData();
}
function trGetDateRange() {
const start = document.getElementById('tr-start-date').value || null;
const end = document.getElementById('tr-end-date').value || null;
return { start, end };
}
function trBuildParams(extra = {}) {
const { start, end } = trGetDateRange();
const params = new URLSearchParams();
if (start) params.set('start_date', start);
if (end) params.set('end_date', end);
for (const [k, v] of Object.entries(extra)) {
if (v) params.set(k, v);
}
return params.toString();
}
async function trLoadAllData() {
trLoadStatistics();
trLoadRecords();
}
async function trLoadStatistics() {
try {
const res = await fetch(`${TR_API_BASE}/statistics?${trBuildParams()}`);
const json = await res.json();
if (json.success) trRenderStatistics(json.data);
} catch (e) {
console.error('加载统计失败', e);
}
}
function trRenderStatistics(stats) {
const grid = document.getElementById('tr-stats-grid');
const pnlClass = stats.total_pnl >= 0 ? 'profit' : 'loss';
grid.innerHTML = `
<div class="tr-stat-card">
<div class="tr-stat-label">总盈亏</div>
<div class="tr-stat-value ${pnlClass}">${stats.total_pnl >= 0 ? '+' : ''}${stats.total_pnl.toFixed(2)}</div>
<div class="tr-stat-sub">手续费: ${stats.total_commission.toFixed(2)}</div>
</div>
<div class="tr-stat-card">
<div class="tr-stat-label">胜率</div>
<div class="tr-stat-value">${stats.win_rate}%</div>
<div class="tr-stat-sub">盈亏比: ${stats.profit_loss_ratio}</div>
</div>
<div class="tr-stat-card">
<div class="tr-stat-label">交易统计</div>
<div class="tr-stat-value">${stats.total_trades}</div>
<div class="tr-stat-sub">${stats.trading_days} 个交易日</div>
</div>
<div class="tr-stat-card">
<div class="tr-stat-label">日均盈亏</div>
<div class="tr-stat-value ${stats.avg_daily_pnl >= 0 ? 'profit' : 'loss'}">${stats.avg_daily_pnl >= 0 ? '+' : ''}${stats.avg_daily_pnl}</div>
<div class="tr-stat-sub">最大连盈 ${stats.max_consecutive_wins} / 连亏 ${stats.max_consecutive_losses}</div>
</div>
`;
}
async function trLoadRecords() {
try {
const params = trBuildParams({ page: trCurrentPage, page_size: 50 });
const res = await fetch(`${TR_API_BASE}/records?${params}`);
const json = await res.json();
if (json.success) trRenderRecords(json.data);
} catch (e) {
console.error('加载记录失败', e);
}
}
function trRenderRecords(data) {
const tbody = document.getElementById('tr-records-body');
const emptyEl = document.getElementById('tr-records-empty');
const tableWrap = document.getElementById('tr-records-table-wrap');
const pagination = document.getElementById('tr-records-pagination');
if (!data.records || data.records.length === 0) {
tableWrap.style.display = 'none';
pagination.style.display = 'none';
emptyEl.style.display = 'flex';
return;
}
tableWrap.style.display = '';
emptyEl.style.display = 'none';
tbody.innerHTML = data.records.map(r => {
const pnlClass = r.close_pnl > 0 ? 'tr-pnl-positive' : (r.close_pnl < 0 ? 'tr-pnl-negative' : '');
const dirClass = r.direction === '买' ? 'tr-dir-buy' : 'tr-dir-sell';
return `<tr>
<td>${r.trade_date || '-'}</td>
<td>${r.trade_time || '-'}</td>
<td>${r.trade_type}</td>
<td>${r.symbol}</td>
<td>${r.symbol_name || r.variety}</td>
<td class="${dirClass}">${r.direction}</td>
<td>${r.offset || '-'}</td>
<td>${r.price != null ? r.price.toFixed(2) : '-'}</td>
<td>${r.volume || '-'}</td>
<td>${r.amount != null ? r.amount.toFixed(2) : '-'}</td>
<td class="${pnlClass}">${r.close_pnl != null ? r.close_pnl.toFixed(2) : '-'}</td>
<td>${r.commission != null ? r.commission.toFixed(2) : '-'}</td>
<td title="${r.source_file || ''}">${(r.source_file || '').substring(0, 15)}</td>
</tr>`;
}).join('');
// 分页
const totalPages = Math.ceil(data.total / data.page_size);
pagination.style.display = totalPages > 1 ? 'flex' : 'none';
pagination.innerHTML = `
<button ${trCurrentPage <= 1 ? 'disabled' : ''} onclick="trCurrentPage--;trLoadRecords()">上一页</button>
<span>${trCurrentPage} / ${totalPages} ( ${data.total} )</span>
<button ${trCurrentPage >= totalPages ? 'disabled' : ''} onclick="trCurrentPage++;trLoadRecords()">下一页</button>
`;
}
async function trHandleFileImport(e) {
const file = e.target.files[0];
if (!file) return;
e.target.value = '';
const btn = document.getElementById('tr-btn-import');
btn.disabled = true;
btn.querySelector('span:last-child').textContent = '导入中...';
try {
const formData = new FormData();
formData.append('file', file);
const res = await fetch(`${TR_API_BASE}/import`, { method: 'POST', body: formData });
const json = await res.json();
if (json.success) {
alert(json.message);
trCurrentPage = 1;
trLoadAllData();
} else {
alert(json.message || '导入失败');
}
} catch (err) {
alert('导入失败: ' + err.message);
} finally {
btn.disabled = false;
btn.querySelector('span:last-child').textContent = '导入结算单';
}
}
// ==================== 批次管理 ====================
async function trShowBatchModal() {
const modal = document.getElementById('tr-batch-modal');
modal.classList.add('show');
try {
const res = await fetch(`${TR_API_BASE}/batches`);
const json = await res.json();
if (json.success) trRenderBatchList(json.data);
} catch (e) {
console.error('加载批次失败', e);
}
}
function trRenderBatchList(batches) {
const list = document.getElementById('tr-batch-list');
if (!batches || batches.length === 0) {
list.innerHTML = '<div class="tr-empty" style="min-height:100px;"><div class="tr-empty-text">暂无导入记录</div></div>';
return;
}
list.innerHTML = batches.map(b => `
<div class="tr-batch-item">
<div class="tr-batch-info">
<div class="tr-batch-file">${b.source_file}</div>
<div class="tr-batch-meta">期货 ${b.futures_count} | 期权 ${b.options_count} | ${b.trade_dates || '未知日期'} | ${b.created_at}</div>
</div>
<button class="tr-batch-delete" onclick="trDeleteBatch('${b.batch_id}')">删除</button>
</div>
`).join('');
}
async function trDeleteBatch(batchId) {
if (!confirm('确认删除该批次的所有交易记录?')) return;
try {
const res = await fetch(`${TR_API_BASE}/records/${batchId}`, { method: 'DELETE' });
const json = await res.json();
if (json.success) {
alert(json.message);
trShowBatchModal();
trCurrentPage = 1;
trLoadAllData();
}
} catch (e) {
alert('删除失败: ' + e.message);
}
}
function trCloseBatchModal() {
document.getElementById('tr-batch-modal').classList.remove('show');
}
async function trDeleteByDate() {
const startDate = document.getElementById('tr-start-date').value;
const endDate = document.getElementById('tr-end-date').value;
if (!startDate || !endDate) {
alert('请先选择要删除的日期范围');
return;
}
if (startDate !== endDate) {
alert('删除功能仅支持单个日期,请确保开始日期和结束日期相同');
return;
}
const tradeDate = startDate;
// 第一次确认:查询该日期的记录数
try {
const queryRes = await fetch(`${TR_API_BASE}/records?start_date=${tradeDate}&end_date=${tradeDate}&page=1&page_size=1`);
const queryJson = await queryRes.json();
const recordCount = queryJson.success ? queryJson.data.total : 0;
if (recordCount === 0) {
alert(`${tradeDate} 没有交易记录,无需删除`);
return;
}
// 第一次确认弹窗
const firstConfirm = confirm(
`⚠️ 警告:即将删除交易数据\n\n` +
`交易日:${tradeDate}\n` +
`记录数量:${recordCount}\n\n` +
`此操作将永久删除该日期的所有交易记录,不可恢复!\n\n` +
`点击"确定"继续,点击"取消"返回`
);
if (!firstConfirm) return;
// 第二次确认弹窗(更强烈的警告)
const secondConfirm = confirm(
`🔴 最终确认\n\n` +
`您确定要删除 ${tradeDate}${recordCount} 条交易记录吗?\n\n` +
`删除后无法恢复,请确认!`
);
if (!secondConfirm) return;
// 执行删除
const res = await fetch(`${TR_API_BASE}/records-by-date/${tradeDate}`, { method: 'DELETE' });
const json = await res.json();
if (json.success) {
alert(`${json.message}`);
trCurrentPage = 1;
trLoadAllData();
} else {
alert(json.message || '删除失败');
}
} catch (e) {
alert('删除失败: ' + e.message);
}
}
// ==================== K线标注 ====================
async function trLoadKlineSymbolOptions() {
try {
// 从品种汇总获取已交易的品种代码
const res = await fetch(`${TR_API_BASE}/variety-summary?${trBuildParams()}`);
const json = await res.json();
if (!json.success) return;
const select = document.getElementById('tr-kline-symbol');
const current = select.value;
select.innerHTML = '<option value="">选择品种</option>';
// 用已交易的品种代码K线API会按variety匹配交易记录
json.data.forEach(v => {
const opt = document.createElement('option');
opt.value = v.variety;
opt.textContent = `${v.symbol_name || v.variety} (${v.variety})`;
select.appendChild(opt);
});
if (current) select.value = current;
} catch (e) {
console.error('加载品种选项失败', e);
}
}
async function trLoadKlineWithTrades() {
const symbol = document.getElementById('tr-kline-symbol').value;
const period = document.getElementById('tr-kline-period').value;
if (!symbol) { alert('请选择品种'); return; }
const chartDom = document.getElementById('tr-kline-chart');
const emptyEl = document.getElementById('tr-kline-empty');
try {
const params = trBuildParams({ period });
const res = await fetch(`${TR_API_BASE}/kline-with-trades/${symbol}?${params}`);
const json = await res.json();
if (!json.success || !json.data.candles || json.data.candles.length === 0) {
chartDom.style.display = 'none';
emptyEl.style.display = 'flex';
if (trKlineChart) { trKlineChart.dispose(); trKlineChart = null; }
return;
}
chartDom.style.display = '';
emptyEl.style.display = 'none';
trRenderKlineWithMarkers(json.data);
} catch (e) {
console.error('加载K线失败', e);
alert('加载K线数据失败');
}
}
function trRenderKlineWithMarkers(data) {
if (trKlineChart) trKlineChart.dispose();
const chartDom = document.getElementById('tr-kline-chart');
trKlineChart = echarts.init(chartDom, 'dark');
const candles = data.candles;
const dates = candles.map(c => c[0]);
const values = candles.map(c => [parseFloat(c[1]), parseFloat(c[2]), parseFloat(c[3]), parseFloat(c[4])]);
const volumes = candles.map(c => [parseInt(c[5]), parseFloat(c[2]) >= parseFloat(c[1]) ? 1 : -1]);
// 构建买卖标记
const buyMarkers = [];
const sellMarkers = [];
const markers = data.trade_markers || [];
markers.forEach(m => {
const dateIdx = dates.indexOf(m.date);
if (dateIdx === -1) return;
const candle = candles[dateIdx];
const price = parseFloat(m.price);
const low = parseFloat(candle[3]);
const high = parseFloat(candle[4]);
// 确保标记价格在K线范围内
const markerPrice = Math.max(low, Math.min(high, price));
if (m.direction === '买') {
buyMarkers.push({
name: `${m.offset || ''}`,
coord: [dateIdx, markerPrice],
value: `${m.offset || ''} ${price}`,
itemStyle: { color: '#34C759' },
});
} else {
sellMarkers.push({
name: `${m.offset || ''}`,
coord: [dateIdx, markerPrice],
value: `${m.offset || ''} ${price}`,
itemStyle: { color: '#FF3B30' },
});
}
});
const option = {
backgroundColor: 'transparent',
animation: false,
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross' },
backgroundColor: 'rgba(10, 15, 25, 0.95)',
borderColor: 'rgba(56, 189, 248, 0.2)',
textStyle: { color: '#e2e8f0', fontSize: 12 },
},
legend: { data: ['K线'], top: 10, textStyle: { color: '#94a3b8' } },
grid: [
{ left: 70, right: 20, top: 40, height: '55%' },
{ left: 70, right: 20, top: '62%', height: '14%' },
],
xAxis: [
{ type: 'category', data: dates, boundaryGap: true, axisLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }, axisLabel: { color: '#64748b', fontSize: 10 }, splitLine: { show: false } },
{ type: 'category', gridIndex: 1, data: dates, axisLine: { show: false }, axisLabel: { show: false }, splitLine: { show: false } },
],
yAxis: [
{ scale: true, splitArea: { show: false }, axisLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }, axisLabel: { color: '#64748b' } },
{ scale: true, gridIndex: 1, splitNumber: 2, axisLabel: { show: false }, axisLine: { show: false }, splitLine: { show: false } },
],
dataZoom: [
{ type: 'inside', xAxisIndex: [0, 1], start: candles.length > 60 ? 60 : 0, end: 100 },
{ show: true, xAxisIndex: [0, 1], type: 'slider', bottom: 10, height: 16, start: candles.length > 60 ? 60 : 0, end: 100 },
],
series: [
{
name: 'K线',
type: 'candlestick',
data: values,
itemStyle: {
color: '#34C759', color0: '#FF3B30',
borderColor: '#34C759', borderColor0: '#FF3B30',
},
markPoint: {
symbol: 'pin',
symbolSize: 40,
data: [
...buyMarkers.map(m => ({
...m,
symbol: 'triangle',
symbolSize: 14,
symbolRotate: 0,
label: { show: true, formatter: 'B', fontSize: 9, color: '#fff' },
})),
...sellMarkers.map(m => ({
...m,
symbol: 'triangle',
symbolSize: 14,
symbolRotate: 180,
label: { show: true, formatter: 'S', fontSize: 9, color: '#fff' },
})),
],
},
},
{
name: '成交量',
type: 'bar',
xAxisIndex: 1,
yAxisIndex: 1,
data: volumes.map(v => ({
value: v[0],
itemStyle: { color: v[1] > 0 ? 'rgba(52,199,89,0.5)' : 'rgba(255,59,48,0.5)' },
})),
},
],
};
trKlineChart.setOption(option);
}
// ==================== 每日分析 ====================
async function trLoadDailySummary() {
try {
const res = await fetch(`${TR_API_BASE}/daily-summary?${trBuildParams()}`);
const json = await res.json();
if (json.success) trRenderDailySummary(json.data);
} catch (e) {
console.error('加载每日汇总失败', e);
}
}
function trRenderDailySummary(data) {
const tbody = document.getElementById('tr-daily-body');
const emptyEl = document.getElementById('tr-daily-empty');
const chartDom = document.getElementById('tr-daily-chart');
if (!data || data.length === 0) {
tbody.innerHTML = '';
emptyEl.style.display = 'flex';
chartDom.style.display = 'none';
return;
}
emptyEl.style.display = 'none';
chartDom.style.display = '';
tbody.innerHTML = data.map(d => {
const pnlClass = d.total_pnl > 0 ? 'tr-pnl-positive' : (d.total_pnl < 0 ? 'tr-pnl-negative' : '');
return `<tr>
<td>${d.trade_date}</td>
<td>${d.total_trades}</td>
<td class="${pnlClass}">${d.total_pnl >= 0 ? '+' : ''}${d.total_pnl}</td>
<td>${d.total_commission}</td>
<td>${d.win_count}</td>
<td>${d.loss_count}</td>
<td>${d.win_rate}%</td>
<td class="tr-pnl-positive">${d.max_win > 0 ? '+' + d.max_win : '-'}</td>
<td class="tr-pnl-negative">${d.max_loss < 0 ? d.max_loss : '-'}</td>
<td>${d.variety_count}</td>
</tr>`;
}).join('');
// 渲染柱状图
trRenderDailyChart(data);
}
function trRenderDailyChart(data) {
const chartDom = document.getElementById('tr-daily-chart');
const chart = echarts.init(chartDom);
chart.setOption({
backgroundColor: 'transparent',
tooltip: { trigger: 'axis' },
grid: { left: 60, right: 20, top: 20, bottom: 40 },
xAxis: { type: 'category', data: data.map(d => d.trade_date), axisLabel: { fontSize: 10, rotate: 30 } },
yAxis: { type: 'value', axisLabel: { fontSize: 11 } },
series: [{
type: 'bar',
data: data.map(d => ({
value: d.total_pnl,
itemStyle: { color: d.total_pnl >= 0 ? '#34C759' : '#FF3B30' },
})),
}],
});
}
// ==================== 品种汇总 ====================
async function trLoadVarietySummary() {
try {
const res = await fetch(`${TR_API_BASE}/variety-summary?${trBuildParams()}`);
const json = await res.json();
if (json.success) trRenderVarietySummary(json.data);
} catch (e) {
console.error('加载品种汇总失败', e);
}
}
function trRenderVarietySummary(data) {
const tbody = document.getElementById('tr-variety-body');
const emptyEl = document.getElementById('tr-variety-empty');
if (!data || data.length === 0) {
tbody.innerHTML = '';
emptyEl.style.display = 'flex';
return;
}
emptyEl.style.display = 'none';
tbody.innerHTML = data.map(v => {
const pnlClass = v.total_pnl > 0 ? 'tr-pnl-positive' : (v.total_pnl < 0 ? 'tr-pnl-negative' : '');
return `<tr>
<td>${v.variety}</td>
<td>${v.symbol_name || '-'}</td>
<td>${v.total_trades}</td>
<td class="${pnlClass}">${v.total_pnl >= 0 ? '+' : ''}${v.total_pnl}</td>
<td>${v.total_close_pnl}</td>
<td>${v.total_commission}</td>
<td>${v.total_volume}</td>
<td>${v.buy_count}</td>
<td>${v.sell_count}</td>
<td>${v.win_rate}%</td>
</tr>`;
}).join('');
}
// ==================== 逐笔分析 ====================
async function trLoadTradePairs() {
try {
const res = await fetch(`${TR_API_BASE}/trade-pairs?${trBuildParams()}`);
const json = await res.json();
if (json.success) trRenderTradePairs(json.data);
} catch (e) {
console.error('加载逐笔数据失败', e);
}
}
function trRenderTradePairs(pairs) {
const grid = document.getElementById('tr-pairs-grid');
const emptyEl = document.getElementById('tr-pairs-empty');
if (!pairs || pairs.length === 0) {
grid.innerHTML = '';
emptyEl.style.display = 'flex';
return;
}
emptyEl.style.display = 'none';
grid.innerHTML = pairs.map(p => {
const pnlClass = p.net_pnl >= 0 ? 'profit' : 'loss';
const dirText = p.direction === '多' ? '做多' : '做空';
const dirClass = p.direction === '多' ? 'tr-dir-buy' : 'tr-dir-sell';
return `
<div class="tr-pair-card ${pnlClass}">
<div class="tr-pair-header">
<span class="tr-pair-symbol">${p.symbol_name || p.symbol} <span class="${dirClass}" style="font-size:12px;">${dirText}</span></span>
<span class="tr-pair-pnl ${pnlClass}">${p.net_pnl >= 0 ? '+' : ''}${p.net_pnl}</span>
</div>
<div class="tr-pair-info">
<div class="tr-pair-info-item"><span class="tr-pair-info-label">开仓</span><span class="tr-pair-info-value">${p.open_date} ${p.open_time || ''}</span></div>
<div class="tr-pair-info-item"><span class="tr-pair-info-label">平仓</span><span class="tr-pair-info-value">${p.close_date || '-'} ${p.close_time || ''}</span></div>
<div class="tr-pair-info-item"><span class="tr-pair-info-label">开仓价</span><span class="tr-pair-info-value">${p.open_price != null ? p.open_price.toFixed(2) : '-'}</span></div>
<div class="tr-pair-info-item"><span class="tr-pair-info-label">平仓价</span><span class="tr-pair-info-value">${p.close_price != null ? p.close_price.toFixed(2) : '-'}</span></div>
<div class="tr-pair-info-item"><span class="tr-pair-info-label">手数</span><span class="tr-pair-info-value">${p.volume || '-'}</span></div>
<div class="tr-pair-info-item"><span class="tr-pair-info-label">手续费</span><span class="tr-pair-info-value">${p.commission}</span></div>
</div>
<div class="tr-pair-actions">
<button class="tr-pair-btn-analyze" onclick='trAnalyzeTrade(${JSON.stringify(p)})'>AI 分析</button>
</div>
</div>`;
}).join('');
}
async function trAnalyzeTrade(pair) {
const modal = document.getElementById('tr-analysis-modal');
const title = document.getElementById('tr-analysis-title');
const content = document.getElementById('tr-analysis-content');
title.textContent = `AI 分析 - ${pair.symbol_name || pair.symbol} ${pair.direction}`;
content.textContent = '正在分析中,请稍候...';
modal.classList.add('show');
try {
const res = await fetch(`${TR_API_BASE}/analyze-trade`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
symbol: pair.symbol,
open_date: pair.open_date,
open_time: pair.open_time,
close_date: pair.close_date,
close_time: pair.close_time,
direction: pair.direction,
open_price: pair.open_price,
close_price: pair.close_price,
}),
});
const json = await res.json();
if (json.success) {
content.textContent = json.data.analysis;
} else {
content.textContent = json.message || '分析失败';
}
} catch (e) {
content.textContent = '分析请求失败: ' + e.message;
}
}
function trCloseAnalysisModal() {
document.getElementById('tr-analysis-modal').classList.remove('show');
}
// 子页面切换时懒加载
document.addEventListener('click', function(e) {
if (!e.target.classList.contains('tr-sub-nav-item')) return;
const sub = e.target.dataset.sub;
setTimeout(() => {
if (sub === 'daily') trLoadDailySummary();
else if (sub === 'variety') trLoadVarietySummary();
else if (sub === 'pairs') trLoadTradePairs();
}, 50);
});
function formatNumber(num) { function formatNumber(num) {
if (num === 0 || num === undefined || num === null) return '--'; if (num === 0 || num === undefined || num === null) return '--';
return num.toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 2 }); return num.toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 2 });

Binary file not shown.

Binary file not shown.

@ -0,0 +1,241 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>期货智析 - 全平台 (Apple Light)</title>
<style>
:root {
--bg-page: #F5F5F7; --bg-card: #FFFFFF; --bg-inset: #F5F5F7;
--text-primary: #1D1D1F; --text-secondary: #6E6E73; --text-tertiary: #86868B;
--border-light: rgba(0,0,0,0.05);
--color-up: #FF3B30; --color-down: #34C759; --color-neutral: #FF9F0A; --color-brand: #007AFF; --color-ai: #AF52DE;
--shadow-sm: 0 1px 2px rgba(0,0,0,0.04); --shadow-md: 0 4px 12px rgba(0,0,0,0.06); --shadow-lg: 0 8px 24px rgba(0,0,0,0.1);
--radius-card: 20px; --radius-pill: 100px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", sans-serif; background: var(--bg-page); color: var(--text-primary); -webkit-font-smoothing: antialiased; padding-bottom: 40px; }
a { text-decoration: none; color: var(--color-brand); }
.header { background: rgba(255,255,255,0.8); backdrop-filter: blur(20px); border-bottom: 1px solid var(--border-light); padding: 0 40px; height: 64px; display: flex; align-items: center; justify-content: space-between; position: sticky; top: 0; z-index: 100; }
.logo { font-size: 18px; font-weight: 700; letter-spacing: -0.02em; }
.nav-items { display: flex; gap: 24px; font-size: 14px; font-weight: 500; }
.nav-item { color: var(--text-secondary); cursor: pointer; padding: 8px 0; border-bottom: 2px solid transparent; transition: all 0.2s; }
.nav-item:hover { color: var(--text-primary); }
.nav-item.active { color: var(--text-primary); border-bottom-color: var(--color-brand); }
.live-badge { display: flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; color: var(--color-down); background: rgba(52,199,89,0.1); padding: 4px 10px; border-radius: var(--radius-pill); }
.dot { width: 6px; height: 6px; background: var(--color-down); border-radius: 50%; animation: pulse 2s infinite; }
@keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.5; } 100% { opacity: 1; } }
.container { max-width: 1200px; margin: 0 auto; padding: 32px 24px; }
.view { display: none; }
.view.active { display: block; }
.toolbar { display: flex; gap: 16px; margin-bottom: 24px; }
.search-box { flex: 1; background: var(--bg-card); border: 1px solid var(--border-light); border-radius: 12px; padding: 0 16px; height: 44px; font-size: 14px; color: var(--text-secondary); box-shadow: var(--shadow-sm); }
.pills { display: flex; gap: 8px; }
.pill { padding: 6px 14px; border-radius: var(--radius-pill); font-size: 13px; font-weight: 500; background: #E8E8ED; color: var(--text-secondary); cursor: pointer; transition: all 0.2s; }
.pill.active { background: var(--text-primary); color: white; }
.card { background: var(--bg-card); border-radius: var(--radius-card); padding: 24px; box-shadow: var(--shadow-md); transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); cursor: pointer; }
.card:hover { transform: translateY(-4px); box-shadow: var(--shadow-lg); }
.stats-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 32px; }
.stat-card { background: var(--bg-card); border-radius: 16px; padding: 16px; display: flex; gap: 12px; align-items: center; box-shadow: var(--shadow-sm); }
.stat-icon { width: 32px; height: 32px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; }
.stat-icon.blue { background: rgba(0,122,255,0.1); color: var(--color-brand); }
.stat-icon.green { background: rgba(52,199,89,0.1); color: var(--color-down); }
.stat-icon.red { background: rgba(255,59,48,0.1); color: var(--color-up); }
.stat-icon.orange { background: rgba(255,159,10,0.1); color: var(--color-neutral); }
.stat-val { font-size: 16px; font-weight: 700; }
.stat-label { font-size: 11px; color: var(--text-tertiary); }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 20px; }
.card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 16px; }
.card-left { display: flex; gap: 12px; align-items: flex-start; }
.code-box { width: 36px; height: 36px; background: #F5F5F7; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 600; color: var(--text-tertiary); }
.info-group { display: flex; flex-direction: column; }
.name-row { display: flex; align-items: center; gap: 6px; font-weight: 600; font-size: 15px; }
.contract-code { font-size: 12px; color: var(--text-tertiary); margin-top: 2px; }
.price-area { text-align: right; }
.price { font-size: 26px; font-weight: 700; letter-spacing: -0.02em; line-height: 1; }
.price.up { color: var(--color-up); }
.price.down { color: var(--color-down); }
.change { font-size: 13px; font-weight: 600; margin-top: 4px; }
.change.up { color: var(--color-up); }
.change.down { color: var(--color-down); }
.action-pill { display: inline-block; padding: 4px 12px; border-radius: var(--radius-pill); font-size: 12px; font-weight: 600; margin-bottom: 16px; }
.action-pill.do-more { background: rgba(52,199,89,0.1); color: var(--color-down); }
.action-pill.watch { background: rgba(255,159,10,0.1); color: var(--color-neutral); }
.metrics { display: flex; gap: 24px; margin-bottom: 16px; }
.metric { flex: 1; }
.metric-label { font-size: 11px; color: var(--text-tertiary); margin-bottom: 4px; }
.bar-bg { height: 4px; background: #F5F5F7; border-radius: 2px; margin-top: 6px; overflow: hidden; }
.bar-fill { height: 100%; border-radius: 2px; }
.timeframes { display: flex; gap: 8px; margin-bottom: 16px; }
.tf { padding: 4px 10px; border-radius: 8px; font-size: 11px; color: var(--text-tertiary); background: #F5F5F7; cursor: pointer; }
.tf.active { background: #E8F2FF; color: var(--color-brand); font-weight: 500; }
.card-footer { display: flex; justify-content: space-between; align-items: center; border-top: 1px solid #F5F5F7; padding-top: 12px; font-size: 12px; }
.support-resist { color: var(--text-tertiary); }
.support-resist span { margin-right: 8px; }
.support-resist .red { color: var(--color-up); }
.support-resist .green { color: var(--color-down); }
.fav-icon { font-size: 15px; color: #D1D1D6; cursor: pointer; transition: all 0.2s; display: inline-flex; align-items: center; }
.fav-icon.active { color: #FF9F0A; transform: scale(1.1); }
/* Market Overview */
.event-banner { display: flex; gap: 16px; overflow-x: auto; padding-bottom: 8px; margin-bottom: 24px; }
.event-card { flex: 1; min-width: 240px; background: var(--bg-card); border-radius: 16px; padding: 16px; box-shadow: var(--shadow-sm); border: 1px solid var(--border-light); }
.event-card.bearish { border-left: 3px solid var(--color-up); }
.event-card.bullish { border-left: 3px solid var(--color-down); }
.event-header { display: flex; justify-content: space-between; margin-bottom: 8px; align-items: center; }
.event-tag { font-size: 10px; font-weight: 700; padding: 2px 6px; border-radius: 4px; }
.event-tag.urgent { background: #FFF0F0; color: var(--color-up); }
.event-tag.policy { background: #E8F2FF; color: var(--color-brand); }
.event-timer { font-size: 11px; color: var(--text-tertiary); font-weight: 500; }
.event-title { font-size: 14px; font-weight: 600; margin-bottom: 6px; line-height: 1.3; }
.event-impact { display: flex; gap: 8px; align-items: center; font-size: 12px; margin-bottom: 8px; }
.impact-badge { font-size: 10px; padding: 2px 6px; border-radius: 4px; font-weight: 600; }
.impact-badge.bull { background: rgba(255,59,48,0.1); color: var(--color-up); }
.impact-badge.bear { background: rgba(52,199,89,0.1); color: var(--color-down); }
.mini-contracts { display: flex; gap: 4px; }
.mini-tag { font-size: 10px; background: #F5F5F7; padding: 2px 6px; border-radius: 4px; color: var(--text-secondary); }
.overview-layout { display: grid; grid-template-columns: 2fr 1fr; gap: 20px; }
.section-header { font-size: 13px; font-weight: 600; color: var(--text-secondary); margin-bottom: 12px; display: flex; justify-content: space-between; }
.heatmap-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 20px; }
.heat-cell { padding: 12px; border-radius: 12px; display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center; min-height: 80px; transition: transform 0.2s; }
.heat-cell:hover { transform: scale(1.03); }
.heat-cell .name { font-weight: 600; margin-bottom: 4px; }
.heat-cell .val { font-size: 12px; font-weight: 700; }
.heat-up-3 { background: #FF3B30; color: white; }
.heat-up-1 { background: rgba(255,59,48,0.15); color: var(--color-up); }
.heat-down-2 { background: rgba(52,199,89,0.3); color: #10803D; }
.heat-down-1 { background: rgba(52,199,89,0.1); color: var(--color-down); }
.heat-flat { background: #E8E8ED; color: var(--text-secondary); }
.limit-list { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.limit-col h4 { font-size: 11px; color: var(--text-tertiary); margin-bottom: 8px; }
.limit-row { display: flex; justify-content: space-between; font-size: 13px; margin-bottom: 6px; }
.limit-row span:first-child { font-weight: 500; }
.limit-row span:last-child { font-weight: 600; }
.side-panel { display: flex; flex-direction: column; gap: 16px; }
.panel-box { background: var(--bg-card); border-radius: 16px; padding: 16px; box-shadow: var(--shadow-sm); }
.panel-box-title { font-size: 13px; font-weight: 600; margin-bottom: 12px; }
.flow-row { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; font-size: 12px; }
.flow-code { width: 30px; font-weight: 600; }
.flow-bar-bg { flex: 1; height: 6px; background: #F5F5F7; border-radius: 3px; overflow: hidden; }
.flow-bar { height: 100%; border-radius: 3px; }
.flow-val { width: 50px; text-align: right; font-weight: 600; }
.calendar-item { display: flex; gap: 12px; margin-bottom: 12px; align-items: flex-start; }
.cal-time { font-size: 11px; color: var(--text-tertiary); margin-top: 2px; }
.cal-event { font-size: 13px; font-weight: 500; margin-bottom: 2px; }
.cal-stars { color: var(--color-neutral); font-size: 10px; }
/* Risk Alerts */
.alerts-layout { display: grid; grid-template-columns: 300px 1fr 280px; gap: 20px; }
.alerts-sidebar { display: flex; flex-direction: column; gap: 16px; }
.rule-group { background: var(--bg-card); border-radius: 16px; padding: 16px; box-shadow: var(--shadow-sm); }
.rule-group-title { font-size: 12px; font-weight: 600; color: var(--text-tertiary); margin-bottom: 12px; }
.rule-item { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; font-size: 13px; cursor: pointer; }
.toggle { width: 36px; height: 20px; background: #E8E8ED; border-radius: 10px; position: relative; cursor: pointer; transition: background 0.2s; }
.toggle.on { background: var(--color-brand); }
.toggle::after { content: ''; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; background: white; border-radius: 50%; transition: transform 0.2s; box-shadow: 0 1px 2px rgba(0,0,0,0.1); }
.toggle.on::after { transform: translateX(16px); }
.alert-feed { display: flex; flex-direction: column; gap: 12px; }
.alert-filters { display: flex; gap: 8px; margin-bottom: 12px; }
.alert-card { background: var(--bg-card); border-radius: 16px; padding: 16px; box-shadow: var(--shadow-sm); position: relative; overflow: hidden; }
.alert-card.urgent { border-left: 3px solid var(--color-up); }
.alert-time { font-size: 11px; color: var(--text-tertiary); margin-bottom: 6px; display: flex; justify-content: space-between; }
.alert-title { font-size: 14px; font-weight: 600; margin-bottom: 4px; }
.alert-desc { font-size: 12px; color: var(--text-secondary); line-height: 1.4; margin-bottom: 8px; }
.alert-actions { display: flex; gap: 12px; font-size: 12px; font-weight: 500; }
.alert-actions span { color: var(--color-brand); cursor: pointer; }
.alert-actions span:hover { text-decoration: underline; }
.ai-scan-card { background: linear-gradient(135deg, #AF52DE 0%, #8E44AD 100%); border-radius: 16px; padding: 16px; color: white; box-shadow: var(--shadow-md); margin-bottom: 16px; }
.ai-scan-title { font-size: 13px; font-weight: 600; margin-bottom: 8px; display: flex; align-items: center; gap: 6px; }
.ai-scan-item { background: rgba(255,255,255,0.15); border-radius: 8px; padding: 8px; margin-bottom: 8px; font-size: 11px; line-height: 1.4; }
</style>
</head>
<body>
<header class="header">
<div class="logo">◈ 期货智析</div>
<div class="nav-items">
<div class="nav-item active" onclick="showView('list-view', this)">品种分析</div>
<div class="nav-item" onclick="showView('market-view', this)">市场概览</div>
<div class="nav-item" onclick="showView('alerts-view', this)">风险预警</div>
</div>
<div class="live-badge"><div class="dot"></div> LIVE</div>
</header>
<script>
function toggleFav(el) { el.classList.toggle('active'); event.stopPropagation(); }
function showView(viewId, navItem) {
document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
document.getElementById(viewId).classList.add('active');
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
navItem.classList.add('active');
}
</script>
<!-- 品种分析 -->
<div id="list-view" class="view active">
<div class="container">
<div class="toolbar"><input type="text" class="search-box" placeholder="🔍 搜索品种名称或代码..."><div class="pills"><div class="pill active">全部</div><div class="pill">能源</div><div class="pill">金属</div><div class="pill">农产品</div></div></div>
<div class="stats-row"><div class="stat-card"><div class="stat-icon blue"></div><div><div class="stat-val">32</div><div class="stat-label">监控品种</div></div></div><div class="stat-card"><div class="stat-icon green"></div><div><div class="stat-val">7</div><div class="stat-label">上涨趋势</div></div></div><div class="stat-card"><div class="stat-icon red"></div><div><div class="stat-val">10</div><div class="stat-label">下跌趋势</div></div></div><div class="stat-card"><div class="stat-icon orange"></div><div><div class="stat-val">15</div><div class="stat-label">震荡整理</div></div></div></div>
<div class="grid">
<div class="card"><div class="card-header"><div class="card-left"><div class="code-box">SC</div><div class="info-group"><div class="name-row">原油 <span class="fav-icon" onclick="toggleFav(this)"></span></div><div class="contract-code">SC2606</div></div></div><div class="price-area"><div class="price down">¥644.5</div><div class="change down">↓ -2.02%</div></div></div><div class="action-pill watch">观望</div><div class="metrics"><div class="metric"><div class="metric-label">成功率 60%</div><div class="bar-bg"><div class="bar-fill" style="width:60%; background:var(--color-neutral);"></div></div></div><div class="metric"><div class="metric-label">趋势评分 30</div><div class="bar-bg"><div class="bar-fill" style="width:30%; background:var(--color-up);"></div></div></div></div><div class="timeframes"><div class="tf active">15M</div><div class="tf">1H</div><div class="tf">4H</div></div><div class="card-footer"><div class="support-resist"><span>压力 <b class="red">642.4</b></span><span>支撑 <b class="green">631.7</b></span></div><a href="#" class="link">详情 →</a></div></div>
<div class="card"><div class="card-header"><div class="card-left"><div class="code-box">AG</div><div class="info-group"><div class="name-row">沪银 <span class="fav-icon" onclick="toggleFav(this)"></span></div><div class="contract-code">AG2606</div></div></div><div class="price-area"><div class="price up">¥18,643</div><div class="change up">↑ +2.27%</div></div></div><div class="action-pill do-more">做多</div><div class="metrics"><div class="metric"><div class="metric-label">成功率 85%</div><div class="bar-bg"><div class="bar-fill" style="width:85%; background:var(--color-down);"></div></div></div><div class="metric"><div class="metric-label">趋势评分 65</div><div class="bar-bg"><div class="bar-fill" style="width:65%; background:var(--color-down);"></div></div></div></div><div class="timeframes"><div class="tf active">15M</div><div class="tf">1H</div><div class="tf">4H</div></div><div class="card-footer"><div class="support-resist"><span>压力 <b class="red">18,685</b></span><span>支撑 <b class="green">18,584</b></span></div><a href="#" class="link">详情 →</a></div></div>
</div>
</div>
</div>
<!-- 市场概览 -->
<div id="market-view" class="view">
<div class="container">
<div class="event-banner">
<div class="event-card bearish"><div class="event-header"><span class="event-tag urgent">🔥 突发</span><span class="event-timer">T-00:15</span></div><div class="event-title">EIA 原油库存意外大幅下降 500 万桶</div><div class="event-impact"><span class="impact-badge bull">利多原油</span></div><div class="mini-contracts"><span class="mini-tag">SC</span><span class="mini-tag">LU</span></div></div>
<div class="event-card bullish"><div class="event-header"><span class="event-tag policy">🏛️ 政策</span><span class="event-timer">已公布</span></div><div class="event-title">央行宣布降准 0.25%,释放长期资金</div><div class="event-impact"><span class="impact-badge bear">利多有色</span></div><div class="mini-contracts"><span class="mini-tag">IF</span><span class="mini-tag">CU</span></div></div>
</div>
<div class="overview-layout">
<div class="left-col">
<div class="section-header"><span>🔥 市场热力图</span></div>
<div class="heatmap-grid">
<div class="heat-cell heat-up-3" style="grid-column: span 2;"><div class="name">SC 原油</div><div class="val">+3.8%</div></div>
<div class="heat-cell heat-up-1"><div class="name">AG 沪银</div><div class="val">+2.8%</div></div>
<div class="heat-cell heat-down-2"><div class="name">SA 纯碱</div><div class="val">-3.1%</div></div>
<div class="heat-cell heat-flat"><div class="name">M 豆粕</div><div class="val">0.0%</div></div>
<div class="heat-cell heat-up-2"><div class="name">IF 股指</div><div class="val">+1.8%</div></div>
<div class="heat-cell heat-down-1"><div class="name">RB 螺纹</div><div class="val">-0.5%</div></div>
</div>
<div class="limit-list"><div class="limit-col"><h4>🔴 领涨</h4><div class="limit-row"><span>SC 原油</span><span style="color:var(--color-up)">+3.8%</span></div><div class="limit-row"><span>AG 沪银</span><span style="color:var(--color-up)">+2.8%</span></div></div><div class="limit-col"><h4>🟢 领跌</h4><div class="limit-row"><span>SA 纯碱</span><span style="color:var(--color-down)">-3.1%</span></div><div class="limit-row"><span>FG 玻璃</span><span style="color:var(--color-down)">-1.9%</span></div></div></div>
</div>
<div class="side-panel">
<div class="panel-box"><div class="panel-box-title">💰 主力资金流向</div><div class="flow-row"><div class="flow-code">SC</div><div class="flow-bar-bg"><div class="flow-bar" style="width:80%;background:var(--color-up);"></div></div><div class="flow-val" style="color:var(--color-up)">+5.2亿</div></div><div class="flow-row"><div class="flow-code">SA</div><div class="flow-bar-bg"><div class="flow-bar" style="width:70%;background:var(--color-down);"></div></div><div class="flow-val" style="color:var(--color-down)">-4.5亿</div></div></div>
<div class="panel-box"><div class="panel-box-title">📅 宏观日历</div><div class="calendar-item"><div class="cal-time">23:30</div><div><div class="cal-event">EIA 原油库存</div><div class="cal-stars">★★★</div></div></div><div class="calendar-item"><div class="cal-time">周五</div><div><div class="cal-event">非农数据</div><div class="cal-stars">★★★★</div></div></div></div>
</div>
</div>
</div>
</div>
<!-- 风险预警 -->
<div id="alerts-view" class="view">
<div class="container">
<div class="alerts-layout">
<!-- 左侧AI 风险指数 & 统计 -->
<div class="side-panel">
<div class="ai-scan-card"><div class="ai-scan-title">✦ 今日风险指数: 72/100</div><div class="ai-scan-item"><b>疑似洗盘</b><br>纯碱 10:45 急跌后迅速拉回,量能异常,疑似主力洗盘。</div><div class="ai-scan-item"><b>流动性枯竭</b><br>胶板 2610 连续 3 分钟无成交,注意滑点风险。</div></div>
<div class="panel-box"><div class="panel-box-title">📊 预警统计</div><div style="display:flex;justify-content:space-between;font-size:12px;margin-bottom:4px;"><span>今日触发</span><span style="font-weight:600;">24 次</span></div><div style="display:flex;justify-content:space-between;font-size:12px;margin-bottom:4px;"><span>误报率</span><span style="font-weight:600;color:var(--color-down);">12%</span></div></div>
</div>
<!-- 中间:预警流 -->
<div class="alert-feed">
<div class="alert-filters"><div class="pill active">全部</div><div class="pill">紧急 (3)</div><div class="pill">事件驱动 (2)</div></div>
<div class="alert-card urgent"><div class="alert-time">14:32 · 关联事件EIA 库存</div><div class="alert-title">🔥 燃油快速突破 60 日均线</div><div class="alert-desc">关联事件EIA 库存超预期下降 | 5 分钟涨幅 1.5%</div><div class="alert-actions"><span>查看详情</span><span>AI 分析</span><span>加入自选</span></div></div>
<div class="alert-card urgent"><div class="alert-time">13:15</div><div class="alert-title">🔴 沪银成交量异常放大</div><div class="alert-desc">当前成交量为过去 20 日均值的 4.2 倍 | 疑似主力入场</div><div class="alert-actions"><span>查看详情</span><span>AI 分析</span></div></div>
<div class="alert-card"><div class="alert-time">11:20</div><div class="alert-title">螺纹 MACD 出现金叉</div><div class="alert-desc">15 分钟级别 MACD 快线上穿慢线</div><div class="alert-actions"><span>查看详情</span><span>AI 分析</span></div></div>
<div class="alert-card"><div class="alert-time">09:30</div><div class="alert-title">开盘跳空 5 品种</div><div class="alert-desc">原油、沪银、铁矿等高开超 1%</div><div class="alert-actions"><span>查看详情</span></div></div>
</div>
<!-- 右侧:规则设置 -->
<div class="alerts-sidebar">
<div class="rule-group"><div class="rule-group-title">价格与指标</div><div class="rule-item" onclick="this.querySelector('.toggle').classList.toggle('on')"><span>价格突破 60 日均线</span><div class="toggle on"></div></div><div class="rule-item" onclick="this.querySelector('.toggle').classList.toggle('on')"><span>MACD 金叉/死叉</span><div class="toggle on"></div></div></div>
<div class="rule-group"><div class="rule-group-title">量能与事件</div><div class="rule-item" onclick="this.querySelector('.toggle').classList.toggle('on')"><span>成交量突增 > 3 倍</span><div class="toggle on"></div></div><div class="rule-item" onclick="this.querySelector('.toggle').classList.toggle('on')"><span>热点事件关联异动</span><div class="toggle on"></div></div></div>
</div>
</div>
</div>
</div>
</body>
</html>

@ -0,0 +1,459 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>期货智析</title>
<style>
:root{--bg-page:#F5F5F7;--bg-card:#fff;--bg-inset:#F5F5F7;--text-primary:#1D1D1F;--text-secondary:#6E6E73;--text-tertiary:#86868B;--border-light:rgba(0,0,0,0.05);--color-up:#FF3B30;--color-down:#34C759;--color-neutral:#FF9F0A;--color-brand:#007AFF;--color-ai:#AF52DE;--shadow-sm:0 1px 2px rgba(0,0,0,0.04);--shadow-md:0 4px 12px rgba(0,0,0,0.06);--shadow-lg:0 8px 24px rgba(0,0,0,0.1);--radius-card:20px;--radius-pill:100px}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","Helvetica Neue",sans-serif;background:var(--bg-page);color:var(--text-primary);-webkit-font-smoothing:antialiased;padding-bottom:40px}
a{text-decoration:none;color:var(--color-brand)}
.header{background:rgba(255,255,255,0.8);backdrop-filter:blur(20px);border-bottom:1px solid var(--border-light);padding:0 40px;height:64px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:100}
.logo{font-size:18px;font-weight:700}
.nav-items{display:flex;gap:20px;font-size:14px;font-weight:500}
.nav-item{color:var(--text-secondary);cursor:pointer;padding:8px 0;border-bottom:2px solid transparent;transition:all .2s}
.nav-item:hover{color:var(--text-primary)}
.nav-item.active{color:var(--text-primary);border-bottom-color:var(--color-brand)}
.live-badge{display:flex;align-items:center;gap:6px;font-size:12px;font-weight:600;color:var(--color-down);background:rgba(52,199,89,0.1);padding:4px 10px;border-radius:var(--radius-pill)}
.dot{width:6px;height:6px;background:var(--color-down);border-radius:50%;animation:pulse 2s infinite}
@keyframes pulse{0%{opacity:1}50%{opacity:.5}100%{opacity:1}}
.container{max-width:1200px;margin:0 auto;padding:32px 24px}
.view{display:none}.view.active{display:block}
.toolbar{display:flex;gap:16px;margin-bottom:24px}
.search-box{flex:1;background:var(--bg-card);border:1px solid var(--border-light);border-radius:12px;padding:0 16px;height:44px;font-size:14px;color:var(--text-secondary);box-shadow:var(--shadow-sm)}
.pills{display:flex;gap:8px}
.pill{padding:6px 14px;border-radius:var(--radius-pill);font-size:13px;font-weight:500;background:#E8E8ED;color:var(--text-secondary);cursor:pointer}
.pill.active{background:var(--text-primary);color:#fff}
.card{background:var(--bg-card);border-radius:var(--radius-card);padding:24px;box-shadow:var(--shadow-md);cursor:pointer}
.card:hover{transform:translateY(-4px);box-shadow:var(--shadow-lg)}
.stats-row{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-bottom:32px}
.stat-card{background:var(--bg-card);border-radius:16px;padding:16px;display:flex;gap:12px;align-items:center;box-shadow:var(--shadow-sm)}
.stat-icon{width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:bold}
.stat-icon.blue{background:rgba(0,122,255,0.1);color:var(--color-brand)}
.stat-icon.green{background:rgba(52,199,89,0.1);color:var(--color-down)}
.stat-icon.red{background:rgba(255,59,48,0.1);color:var(--color-up)}
.stat-icon.orange{background:rgba(255,159,10,0.1);color:var(--color-neutral)}
.stat-val{font-size:16px;font-weight:700}
.stat-label{font-size:11px;color:var(--text-tertiary)}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:20px}
.card-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:16px}
.card-left{display:flex;gap:12px;align-items:flex-start}
.code-box{width:36px;height:36px;background:#F5F5F7;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:600;color:var(--text-tertiary)}
.info-group{display:flex;flex-direction:column}
.name-row{display:flex;align-items:center;gap:6px;font-weight:600;font-size:15px}
.contract-code{font-size:12px;color:var(--text-tertiary);margin-top:2px}
.price-area{text-align:right}
.price{font-size:26px;font-weight:700;line-height:1}
.price.up{color:var(--color-up)}.price.down{color:var(--color-down)}
.change{font-size:13px;font-weight:600;margin-top:4px}
.change.up{color:var(--color-up)}.change.down{color:var(--color-down)}
.action-pill{display:inline-block;padding:4px 12px;border-radius:var(--radius-pill);font-size:12px;font-weight:600;margin-bottom:16px}
.action-pill.do-more{background:rgba(52,199,89,0.1);color:var(--color-down)}
.action-pill.watch{background:rgba(255,159,10,0.1);color:var(--color-neutral)}
.metrics{display:flex;gap:24px;margin-bottom:16px}
.metric{flex:1}
.metric-label{font-size:11px;color:var(--text-tertiary);margin-bottom:4px}
.bar-bg{height:4px;background:#F5F5F7;border-radius:2px;margin-top:6px;overflow:hidden}
.bar-fill{height:100%;border-radius:2px}
.timeframes{display:flex;gap:8px;margin-bottom:16px}
.tf{padding:4px 10px;border-radius:8px;font-size:11px;color:var(--text-tertiary);background:#F5F5F7;cursor:pointer}
.tf.active{background:#E8F2FF;color:var(--color-brand);font-weight:500}
.card-footer{display:flex;justify-content:space-between;align-items:center;border-top:1px solid #F5F5F7;padding-top:12px;font-size:12px}
.support-resist{color:var(--text-tertiary)}
.support-resist span{margin-right:8px}
.support-resist .red{color:var(--color-up)}.support-resist .green{color:var(--color-down)}
.fav-icon{font-size:15px;color:#D1D1D6;cursor:pointer;display:inline-flex;align-items:center}
.fav-icon.active{color:#FF9F0A}
.event-banner{display:flex;gap:16px;overflow-x:auto;padding-bottom:8px;margin-bottom:24px}
.event-card{flex:1;min-width:240px;background:var(--bg-card);border-radius:16px;padding:16px;box-shadow:var(--shadow-sm);border:1px solid var(--border-light)}
.event-card.bearish{border-left:3px solid var(--color-up)}
.event-card.bullish{border-left:3px solid var(--color-down)}
.event-header{display:flex;justify-content:space-between;margin-bottom:8px;align-items:center}
.event-tag{font-size:10px;font-weight:700;padding:2px 6px;border-radius:4px}
.event-tag.urgent{background:#FFF0F0;color:var(--color-up)}
.event-tag.policy{background:#E8F2FF;color:var(--color-brand)}
.event-timer{font-size:11px;color:var(--text-tertiary);font-weight:500}
.event-title{font-size:14px;font-weight:600;margin-bottom:6px}
.event-impact{display:flex;gap:8px;align-items:center;font-size:12px;margin-bottom:8px}
.impact-badge{font-size:10px;padding:2px 6px;border-radius:4px;font-weight:600}
.impact-badge.bull{background:rgba(255,59,48,0.1);color:var(--color-up)}
.impact-badge.bear{background:rgba(52,199,89,0.1);color:var(--color-down)}
.mini-contracts{display:flex;gap:4px}
.mini-tag{font-size:10px;background:#F5F5F7;padding:2px 6px;border-radius:4px;color:var(--text-secondary)}
.overview-layout{display:grid;grid-template-columns:2fr 1fr;gap:20px}
.section-header{font-size:13px;font-weight:600;color:var(--text-secondary);margin-bottom:12px}
.heatmap-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-bottom:20px}
.heat-cell{padding:12px;border-radius:12px;display:flex;flex-direction:column;justify-content:center;align-items:center;min-height:80px}
.heat-cell .name{font-weight:600;margin-bottom:4px}
.heat-cell .val{font-size:12px;font-weight:700}
.heat-up-3{background:#FF3B30;color:#fff}
.heat-up-1{background:rgba(255,59,48,0.15);color:var(--color-up)}
.heat-down-2{background:rgba(52,199,89,0.3);color:#10803D}
.heat-flat{background:#E8E8ED;color:var(--text-secondary)}
.side-panel{display:flex;flex-direction:column;gap:16px}
.panel-box{background:var(--bg-card);border-radius:16px;padding:16px;box-shadow:var(--shadow-sm)}
.panel-box-title{font-size:13px;font-weight:600;margin-bottom:12px}
.flow-row{display:flex;align-items:center;gap:8px;margin-bottom:8px;font-size:12px}
.flow-code{width:30px;font-weight:600}
.flow-bar-bg{flex:1;height:6px;background:#F5F5F7;border-radius:3px;overflow:hidden}
.flow-bar{height:100%;border-radius:3px}
.flow-val{width:50px;text-align:right;font-weight:600}
.calendar-item{display:flex;gap:12px;margin-bottom:12px;align-items:flex-start}
.cal-time{font-size:11px;color:var(--text-tertiary);margin-top:2px}
.cal-event{font-size:13px;font-weight:500}
.cal-stars{color:var(--color-neutral);font-size:10px}
.alerts-layout{display:grid;grid-template-columns:300px 1fr 280px;gap:20px}
.rule-group{background:var(--bg-card);border-radius:16px;padding:16px;box-shadow:var(--shadow-sm)}
.rule-group-title{font-size:12px;font-weight:600;color:var(--text-tertiary);margin-bottom:12px}
.rule-item{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;font-size:13px;cursor:pointer}
.toggle{width:36px;height:20px;background:#E8E8ED;border-radius:10px;position:relative;cursor:pointer}
.toggle.on{background:var(--color-brand)}
.toggle::after{content:;position:absolute;top:2px;left:2px;width:16px;height:16px;background:#fff;border-radius:50%;transition:transform .2s}
.toggle.on::after{transform:translateX(16px)}
.alert-feed{display:flex;flex-direction:column;gap:12px}
.alert-filters{display:flex;gap:8px;margin-bottom:12px}
.alert-card{background:var(--bg-card);border-radius:16px;padding:16px;box-shadow:var(--shadow-sm)}
.alert-card.urgent{border-left:3px solid var(--color-up)}
.alert-time{font-size:11px;color:var(--text-tertiary);margin-bottom:6px}
.alert-title{font-size:14px;font-weight:600;margin-bottom:4px}
.alert-desc{font-size:12px;color:var(--text-secondary);line-height:1.4;margin-bottom:8px}
.alert-actions{display:flex;gap:12px;font-size:12px;font-weight:500}
.alert-actions span{color:var(--color-brand);cursor:pointer}
.ai-scan-card{background:linear-gradient(135deg,#AF52DE,#8E44AD);border-radius:16px;padding:16px;color:#fff;box-shadow:var(--shadow-md);margin-bottom:16px}
.ai-scan-title{font-size:13px;font-weight:600;margin-bottom:8px}
.ai-scan-item{background:rgba(255,255,255,0.15);border-radius:8px;padding:8px;margin-bottom:8px;font-size:11px}
.summary-header{background:var(--bg-card);border-radius:20px;padding:20px 24px;box-shadow:var(--shadow-sm);margin-bottom:24px;display:flex;justify-content:space-between;align-items:center}
.summary-header h2{font-size:20px;font-weight:700}
.summary-header .date{font-size:14px;color:var(--text-secondary);background:var(--bg-inset);padding:6px 14px;border-radius:8px;font-weight:500}
.summary-layout{display:grid;grid-template-columns:1fr 360px;gap:20px}
.summary-left{display:grid;grid-template-columns:1fr 1fr;gap:16px}
.rank-section{background:var(--bg-card);border-radius:16px;padding:16px;box-shadow:var(--shadow-sm)}
.rank-title{font-size:13px;font-weight:600;margin-bottom:12px;padding-bottom:8px;border-bottom:1px solid var(--border-light)}
.rank-row{display:flex;justify-content:space-between;align-items:center;padding:7px 0;border-bottom:1px dashed rgba(0,0,0,0.04);cursor:pointer;transition:background .15s}.rank-row:hover{background:var(--bg-inset);border-radius:6px}
.rank-row:last-child{border-bottom:none}
.rank-rank{width:20px;font-size:12px;font-weight:700;color:var(--text-tertiary);text-align:center}
.rank-rank.top{color:var(--color-brand)}
.rank-name{flex:1;margin-left:8px;font-size:13px;font-weight:500}
.rank-val{font-size:13px;font-weight:600}
.rank-val.up{color:var(--color-up)}.rank-val.down{color:var(--color-down)}
.plan-header{font-size:15px;font-weight:700;margin-bottom:16px;color:var(--color-ai)}
.plan-card{background:var(--bg-card);border-radius:16px;padding:20px;box-shadow:var(--shadow-md);margin-bottom:16px;border:1px solid var(--border-light);position:relative}
.plan-card::before{content:"AI";position:absolute;top:-9px;left:16px;background:var(--color-ai);color:#fff;font-size:10px;padding:2px 8px;border-radius:8px;font-weight:600}
.plan-top{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;margin-top:2px}
.plan-code{font-size:16px;font-weight:700}
.plan-badge{font-size:10px;padding:4px 12px;border-radius:var(--radius-pill);font-weight:700}
.plan-badge.long{background:rgba(52,199,89,0.15);color:var(--color-down)}
.plan-badge.short{background:rgba(255,59,48,0.15);color:var(--color-up)}
.plan-ai{background:#F8F5FF;border-radius:12px;padding:12px;margin-bottom:12px;font-size:12px;line-height:1.5;color:var(--text-secondary)}
.plan-ai strong{color:var(--color-ai)}
.plan-targets{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-bottom:12px}
.plan-target{background:var(--bg-inset);border-radius:10px;padding:10px 8px;text-align:center}
.plan-target-label{font-size:10px;color:var(--text-tertiary);margin-bottom:4px}
.plan-target-val{font-size:14px;font-weight:700}
.plan-target-val.green{color:var(--color-down)}.plan-target-val.red{color:var(--color-up)}.plan-target-val.neutral{color:var(--text-primary)}
.plan-actions{display:flex;gap:8px}
.plan-btn{flex:1;padding:8px 0;border:none;border-radius:10px;font-size:12px;font-weight:600;cursor:pointer}
.plan-btn.primary{background:var(--color-brand);color:#fff}
.plan-btn.secondary{background:var(--bg-inset);color:var(--text-secondary)}
.plan-note{font-size:10px;color:var(--text-tertiary);text-align:center;margin-top:8px}.plan-list{display:flex;flex-direction:column;gap:6px}.plan-list-item{background:var(--bg-card);border-radius:12px;padding:12px 16px;border:1px solid var(--border-light);cursor:pointer;display:flex;justify-content:space-between;align-items:center;transition:all .2s}.plan-list-item:hover{box-shadow:var(--shadow-md);transform:translateY(-1px)}.plan-list-item.expanded{border-color:var(--color-ai);box-shadow:var(--shadow-md)}.plan-list-left{display:flex;align-items:center;gap:10px}.plan-list-code{font-size:14px;font-weight:700}.plan-list-score{font-size:12px;font-weight:600;color:var(--color-ai);background:#F8F5FF;padding:2px 8px;border-radius:6px}.plan-list-arrow{font-size:12px;color:var(--text-tertiary);transition:transform .2s}.plan-list-item.expanded .plan-list-arrow{transform:rotate(180deg)}.plan-detail{max-height:0;overflow:hidden;transition:max-height .35s ease}.plan-detail.open{max-height:600px}
.modal-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.4);backdrop-filter:blur(8px);z-index:1000;display:none;align-items:center;justify-content:center}
.modal-overlay.show{display:flex}
.modal{background:var(--bg-card);display:flex;flex-direction:column;border-radius:24px;width:92%;max-width:860px;max-height:85vh;overflow-y:auto;box-shadow:var(--shadow-lg);animation:modalIn .25s ease}
@keyframes modalIn{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}
.modal-header{display:flex;justify-content:space-between;align-items:center;padding:24px 28px 20px;border-bottom:1px solid var(--border-light)}
.modal-title{font-size:20px;font-weight:700}
.modal-close{width:32px;height:32px;border-radius:50%;border:none;background:var(--bg-inset);font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center}
.modal-close:hover{background:#E8E8ED}
.modal-body{padding:16px 28px 24px}
.modal-section{margin-bottom:16px}
.modal-section-title{font-size:12px;font-weight:600;color:var(--text-tertiary);margin-bottom:8px}
.modal-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.modal-stat{background:var(--bg-inset);border-radius:12px;padding:12px;text-align:center}
.modal-stat-label{font-size:11px;color:var(--text-tertiary);margin-bottom:4px}
.modal-stat-val{font-size:18px;font-weight:700}
.modal-stat-val.up{color:var(--color-up)}.modal-stat-val.down{color:var(--color-down)}
.modal-ai-box{background:#F8F5FF;border-radius:12px;padding:14px;font-size:13px;line-height:1.6}
.modal-ai-box strong{color:var(--color-ai)}
.modal-footer{padding:16px 24px 20px;border-top:1px solid var(--border-light);display:flex;gap:8px}
.kline-chart{width:100%;height:480px;background:var(--bg-inset);border-radius:12px;margin-bottom:12px}
.modal-section-title-row{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}
</style></head><body>
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
<header class="header">
<div class="logo">期货智析</div>
<div class="nav-items">
<div class="nav-item active" onclick="showView('list-view',this)">品种分析</div>
<div class="nav-item" onclick="showView('market-view',this)">市场概览</div>
<div class="nav-item" onclick="showView('alerts-view',this)">风险预警</div>
<div class="nav-item" onclick="showView('summary-view',this)">复盘计划</div>
</div>
<div class="live-badge"><div class="dot"></div>LIVE</div>
</header>
<script>
function toggleFav(e){e.classList.toggle("active");event.stopPropagation()}
function showView(id,n){
document.querySelectorAll(".view").forEach(function(v){v.classList.remove("active")});
document.getElementById(id).classList.add("active");
document.querySelectorAll(".nav-item").forEach(function(x){x.classList.remove("active")});
n.classList.add("active")}
var modalData={
SC:{name:"SC 原油",price:"644.5",chg:"-2.02%",dir:"down",vol:"58.2万手",oi:"28.4万手",amp:"5.2%",rsi:"62",macd:"金叉",trend:"偏空",signal:"观望",
logic:"EIA库存超预期下降+OPEC+延长减产,但宏观需求预期偏弱。"},
RB:{name:"RB 螺纹",price:"3,485",chg:"+0.85%",dir:"up",vol:"52.1万手",oi:"38.5万手",amp:"2.1%",rsi:"48",macd:"死叉",trend:"震荡",signal:"观望",
logic:"淡季累库压力较大,政策预期与基本面博弈。"},
AG:{name:"AG 沪银",price:"18,643",chg:"+2.27%",dir:"up",vol:"45.8万手",oi:"18.2万手",amp:"4.5%",rsi:"71",macd:"金叉",trend:"偏多",signal:"偏多",
logic:"降息预期+央行购金+避险情绪共振,多头趋势延续。"},
SA:{name:"SA 纯碱",price:"1,825",chg:"-3.10%",dir:"down",vol:"38.6万手",oi:"15.8万手",amp:"6.8%",rsi:"28",macd:"死叉",trend:"偏空",signal:"偏空",
logic:"高库存+光伏需求下滑+新产能投放,基本面偏弱。"},
IF:{name:"IF 股指",price:"3,856",chg:"+1.80%",dir:"up",vol:"32.4万手",oi:"42.8万手",amp:"2.4%",rsi:"58",macd:"金叉",trend:"偏多",signal:"偏多",
logic:"央行降准+外资流入,情绪回暖。"},
FG:{name:"FG 玻璃",price:"1,258",chg:"-1.90%",dir:"down",vol:"22.3万手",oi:"12.1万手",amp:"3.9%",rsi:"35",macd:"死叉",trend:"偏空",signal:"偏空",
logic:"地产竣工走弱+库存高位,需求端承压。"},
LU:{name:"LU 燃油",price:"4,520",chg:"+0.65%",dir:"up",vol:"18.5万手",oi:"8.2万手",amp:"3.2%",rsi:"52",macd:"中性",trend:"震荡",signal:"观望",
logic:"跟随原油走势,自身供需平衡。"},
M:{name:"M 豆粕",price:"3,185",chg:"+0.35%",dir:"up",vol:"28.6万手",oi:"24.2万手",amp:"1.8%",rsi:"45",macd:"中性",trend:"震荡",signal:"观望",
logic:"美豆震荡+国内养殖需求平稳。"},
MA:{name:"MA 甲醇",price:"2,456",chg:"-0.55%",dir:"down",vol:"25.1万手",oi:"20.8万手",amp:"2.0%",rsi:"42",macd:"中性",trend:"震荡",signal:"观望",
logic:"港口库存偏高+煤价成本支撑。"}
};
var klineData={
SC:[
[20260520,642,658,640,655,125000],[20260521,656,662,648,650,118000],[20260522,650,655,645,652,132000],[20260523,653,660,648,658,140000],[20260524,659,668,655,660,155000],[20260525,661,665,650,652,148000],[20260526,652,658,642,645,138000],[20260527,645,652,638,648,142000],[20260528,649,655,640,642,135000],[20260529,642,650,635,645,150000],[20260530,646,658,642,655,162000],[20260531,656,668,650,662,175000],[20260601,663,670,640,644.5,188000]
],
RB:[
[20260520,3520,3550,3500,3530,85000],[20260521,3532,3560,3510,3545,82000],[20260522,3545,3570,3520,3555,88000],[20260523,3556,3580,3540,3570,92000],[20260524,3572,3590,3550,3560,95000],[20260525,3560,3575,3530,3540,90000],[20260526,3540,3555,3510,3520,86000],[20260527,3520,3540,3490,3500,88000],[20260528,3500,3520,3480,3495,84000],[20260529,3495,3510,3470,3480,92000],[20260530,3482,3500,3460,3475,88000],[20260531,3475,3495,3455,3485,86000],[20260601,3486,3510,3470,3485,90000]
],
AG:[
[20260520,18200,18450,18100,18350,55000],[20260521,18360,18500,18250,18420,58000],[20260522,18430,18600,18350,18550,62000],[20260523,18560,18700,18480,18620,65000],[20260524,18630,18800,18550,18750,70000],[20260525,18760,18900,18650,18700,68000],[20260526,18710,18780,18550,18600,64000],[20260527,18610,18700,18480,18550,62000],[20260528,18560,18650,18400,18480,60000],[20260529,18490,18580,18350,18420,63000],[20260530,18430,18600,18380,18520,66000],[20260531,18530,18700,18480,18650,70000],[20260601,18660,18800,18580,18643,75000]
],
SA:[
[20260520,1920,1950,1900,1935,42000],[20260521,1936,1960,1910,1920,45000],[20260522,1920,1940,1890,1900,48000],[20260523,1900,1920,1870,1880,52000],[20260524,1880,1900,1860,1870,55000],[20260525,1870,1890,1850,1860,50000],[20260526,1860,1880,1840,1850,48000],[20260527,1850,1870,1830,1845,46000],[20260528,1845,1860,1820,1830,44000],[20260529,1830,1850,1810,1820,47000],[20260530,1820,1840,1800,1810,50000],[20260531,1810,1830,1790,1800,48000],[20260601,1800,1850,1780,1825,52000]
],
IF:[
[20260520,3820,3850,3800,3835,35000],[20260521,3836,3860,3810,3850,36000],[20260522,3851,3870,3830,3860,37000],[20260523,3862,3880,3840,3870,38000],[20260524,3872,3890,3850,3880,40000],[20260525,3880,3895,3860,3875,38000],[20260526,3875,3890,3850,3860,36000],[20260527,3860,3880,3840,3855,35000],[20260528,3855,3870,3830,3845,34000],[20260529,3845,3860,3820,3840,36000],[20260530,3840,3860,3820,3850,37000],[20260531,3850,3870,3835,3860,38000],[20260601,3862,3880,3845,3856,40000]
],
FG:[
[20260520,1320,1350,1300,1335,28000],[20260521,1336,1355,1310,1320,30000],[20260522,1320,1340,1290,1300,32000],[20260523,1300,1320,1280,1290,35000],[20260524,1290,1310,1270,1280,38000],[20260525,1280,1300,1260,1270,36000],[20260526,1270,1290,1250,1260,34000],[20260527,1260,1280,1240,1255,32000],[20260528,1255,1270,1235,1250,30000],[20260529,1250,1265,1230,1240,33000],[20260530,1240,1260,1225,1235,31000],[20260531,1235,1255,1220,1230,30000],[20260601,1230,1270,1215,1258,32000]
],
LU:[
[20260520,4480,4520,4460,4500,22000],[20260521,4502,4530,4480,4510,23000],[20260522,4512,4540,4490,4520,24000],[20260523,4522,4550,4500,4535,25000],[20260524,4536,4560,4510,4545,26000],[20260525,4546,4565,4520,4530,24000],[20260526,4530,4550,4500,4510,22000],[20260527,4510,4535,4490,4505,23000],[20260528,4505,4525,4480,4495,21000],[20260529,4495,4515,4470,4490,22000],[20260530,4490,4510,4470,4500,23000],[20260531,4500,4525,4480,4510,24000],[20260601,4510,4540,4490,4520,25000]
],
M:[
[20260520,3150,3180,3130,3165,32000],[20260521,3166,3190,3140,3175,33000],[20260522,3176,3200,3155,3190,35000],[20260523,3192,3210,3170,3200,36000],[20260524,3202,3220,3180,3210,38000],[20260525,3212,3225,3190,3200,36000],[20260526,3200,3215,3180,3190,34000],[20260527,3190,3205,3170,3185,33000],[20260528,3185,3200,3165,3180,32000],[20260529,3180,3195,3160,3175,34000],[20260530,3175,3190,3155,3180,33000],[20260531,3180,3195,3160,3185,34000],[20260601,3186,3200,3170,3185,35000]
],
MA:[
[20260520,2480,2510,2460,2495,28000],[20260521,2496,2520,2470,2485,30000],[20260522,2486,2510,2460,2475,32000],[20260523,2476,2500,2450,2465,34000],[20260524,2466,2490,2440,2455,36000],[20260525,2455,2480,2435,2445,34000],[20260526,2445,2470,2425,2440,32000],[20260527,2440,2460,2420,2435,30000],[20260528,2435,2455,2415,2430,28000],[20260529,2430,2450,2410,2425,30000],[20260530,2425,2445,2405,2420,29000],[20260531,2420,2440,2400,2415,28000],[20260601,2415,2470,2400,2456,30000]
]
};
var klineChart=null;
function renderKline(code){
if(!klineData[code])return;
var container=document.getElementById('kline-container');
if(!container)return;
if(!klineChart){klineChart=echarts.init(container)}
var raw=klineData[code];
var dates=raw.map(function(r){return r[0].toString()});
var ohlc=raw.map(function(r){return[r[1],r[2],r[3],r[4]]});
var vols=raw.map(function(r){return r[5]});
var ma5=[],ma10=[];
for(var i=0;i<ohlc.length;i++){
var c=ohlc[i][3];var s5=0,c5=0,s10=0,c10=0;
for(var j=0;j<5&&i-j>=0;j++){s5+=ohlc[i-j][3];c5++}
for(var j=0;j<10&&i-j>=0;j++){s10+=ohlc[i-j][3];c10++}
ma5.push(c5>0?s5/c5:null);ma10.push(c10>0?s10/c10:null);
}
var opt={
grid:[{left:50,right:20,top:20,height:'55%'},{left:50,right:20,top:'78%',height:'18%'}],
xAxis:[{type:'category',data:dates,gridIndex:0,axisLine:{lineStyle:{color:'#eee'}},axisLabel:{show:false}},{type:'category',data:dates,gridIndex:1,axisLine:{lineStyle:{color:'#eee'}},axisLabel:{show:false}}],
yAxis:[{scale:true,splitArea:{show:true,areaStyle:{color:['#fff','#f9f9f9']}},axisLine:{lineStyle:{color:'#eee'}}},{scale:true,gridIndex:1,splitArea:{show:true,areaStyle:{color:['#fff','#f9f9f9']}},axisLine:{lineStyle:{color:'#eee'}},axisLabel:{show:false}}],
dataZoom:[{type:'inside',xAxisIndex:[0,1],start:30,end:100}],
tooltip:{trigger:'axis',axisPointer:{type:'cross'},formatter:function(p){if(!p[0])return'';var d=p[0].dataIndex;var o=ohlc[d];return dates[d].substring(4)+'<br>开: '+o[0]+' 高: '+o[1]+'<br>低: '+o[2]+' 收: '+o[3]+'<br>量: '+vols[d]}},
series:[
{name:'K线',type:'candlestick',data:ohlc,itemStyle:{color:'#FF3B30',color0:'#34C759',borderColor:'#FF3B30',borderColor0:'#34C759'}},
{name:'MA5',type:'line',data:ma5,smooth:true,lineStyle:{width:1.5,color:'#AF52DE'}},
{name:'MA10',type:'line',data:ma10,smooth:true,lineStyle:{width:1.5,color:'#FF9F0A'}},
{name:'成交量',type:'bar',xAxisIndex:1,yAxisIndex:1,data:vols.map(function(v,i){return{value:v,itemStyle:{color:ohlc[i][3]>=ohlc[i][0]?'rgba(255,59,48,0.5)':'rgba(52,199,89,0.5)'}}})}
],
legend:{data:['K线','MA5','MA10','成交量'],bottom:0,itemWidth:12,itemHeight:8,textStyle:{fontSize:10}}
};
klineChart.setOption(opt,true);
}
function showDetail(code){
var d=modalData[code];if(!d)return;
var el=document.getElementById('modal-body');
el.innerHTML='<div class="modal-section"><div class="modal-section-title">实时行情</div><div class="modal-grid">'+
'<div class="modal-stat"><div class="modal-stat-label">最新价</div><div class="modal-stat-val '+d.dir+'">'+d.price+'</div></div>'+
'<div class="modal-stat"><div class="modal-stat-label">涨跌幅</div><div class="modal-stat-val '+d.dir+'">'+d.chg+'</div></div>'+
'<div class="modal-stat"><div class="modal-stat-label">成交量</div><div class="modal-stat-val">'+d.vol+'</div></div>'+
'<div class="modal-stat"><div class="modal-stat-label">持仓量</div><div class="modal-stat-val">'+d.oi+'</div></div>'+
'<div class="modal-stat"><div class="modal-stat-label">振幅</div><div class="modal-stat-val">'+d.amp+'</div></div>'+
'<div class="modal-stat"><div class="modal-stat-label">RSI</div><div class="modal-stat-val">'+d.rsi+'</div></div></div></div>'+
'<div class="modal-section"><div class="modal-section-title">技术信号</div><div class="modal-grid">'+
'<div class="modal-stat"><div class="modal-stat-label">MACD</div><div class="modal-stat-val">'+d.macd+'</div></div>'+
'<div class="modal-stat"><div class="modal-stat-label">趋势</div><div class="modal-stat-val">'+d.trend+'</div></div>'+
'<div class="modal-stat"><div class="modal-stat-label">信号</div><div class="modal-stat-val "+(d.signal=="偏多"?"down":d.signal=="偏空"?"up":"")+"">'+d.signal+'</div></div>'+
'<div class="modal-stat"><div class="modal-stat-label">AI建议</div><div class="modal-stat-val" style="color:var(--color-ai)">查看分析</div></div></div></div>'+
'<div class="modal-section"><div class="modal-section-title">分析逻辑</div><div class="modal-ai-box">'+d.logic+'</div></div>';
document.getElementById('modal-title').textContent=d.name;
setTimeout(function(){renderKline(code)},100);
document.getElementById('detail-modal').classList.add('show');
}
function closeModal(){document.getElementById("detail-modal").classList.remove("show");if(klineChart){klineChart.dispose();klineChart=null}}
function togglePlan(el){
var detail=el.nextElementSibling;
var isOpen=detail.classList.contains("open");
document.querySelectorAll(".plan-detail.open").forEach(function(d){d.classList.remove("open");d.previousElementSibling.classList.remove("expanded")});
if(!isOpen){detail.classList.add("open");el.classList.add("expanded")}}
</script>
<div id="list-view" class="view active"><div class="container">
<div class="toolbar"><input type="text" class="search-box" placeholder="搜索合约代码或名称..."><div class="pills"><div class="pill active">全部</div><div class="pill">能源</div><div class="pill">金属</div><div class="pill">农产品</div><div class="pill">化工</div></div></div>
<div class="stats-row">
<div class="stat-card"><div class="stat-icon blue">=</div><div><div class="stat-val">32</div><div class="stat-label">监控品种</div></div></div>
<div class="stat-card"><div class="stat-icon green">&#x2197;</div><div><div class="stat-val">7</div><div class="stat-label">上涨趋势</div></div></div>
<div class="stat-card"><div class="stat-icon red">&#x2198;</div><div><div class="stat-val">10</div><div class="stat-label">下跌趋势</div></div></div>
<div class="stat-card"><div class="stat-icon orange">&#x2194;</div><div><div class="stat-val">15</div><div class="stat-label">震荡整理</div></div></div>
</div>
<div class="grid">
<div class="card"><div class="card-header"><div class="card-left"><div class="code-box">SC</div><div class="info-group"><div class="name-row">原油 <span class="fav-icon" onclick="toggleFav(this)">&#x2605;</span></div><div class="contract-code">SC2606</div></div></div><div class="price-area"><div class="price down">644.5</div><div class="change down">-2.02%</div></div></div><div class="action-pill watch">观望</div><div class="metrics"><div class="metric"><div class="metric-label">成功率 60%</div><div class="bar-bg"><div class="bar-fill" style="width:60%;background:var(--color-neutral)"></div></div></div></div><div class="timeframes"><div class="tf active">15M</div><div class="tf">1H</div><div class="tf">4H</div></div><div class="card-footer"><div class="support-resist"><span>压力 <b class="red">642.4</b></span></div><a href="#">详情</a></div></div>
<div class="card"><div class="card-header"><div class="card-left"><div class="code-box">AG</div><div class="info-group"><div class="name-row">沪银 <span class="fav-icon active" onclick="toggleFav(this)">&#x2605;</span></div><div class="contract-code">AG2606</div></div></div><div class="price-area"><div class="price up">18,643</div><div class="change up">+2.27%</div></div></div><div class="action-pill do-more">做多</div><div class="metrics"><div class="metric"><div class="metric-label">成功率 85%</div><div class="bar-bg"><div class="bar-fill" style="width:85%;background:var(--color-down)"></div></div></div></div><div class="timeframes"><div class="tf active">15M</div><div class="tf">1H</div><div class="tf">4H</div></div><div class="card-footer"><div class="support-resist"><span>压力 <b class="red">18,685</b></span></div><a href="#">详情</a></div></div>
</div></div></div>
<div id="market-view" class="view"><div class="container">
<div class="event-banner">
<div class="event-card bearish"><div class="event-header"><span class="event-tag urgent">突发</span><span class="event-timer">T-00:15</span></div><div class="event-title">EIA 原油库存意外大幅下降 500 万桶</div><div class="event-impact"><span class="impact-badge bull">利多原油</span></div><div class="mini-contracts"><span class="mini-tag">SC</span><span class="mini-tag">LU</span></div></div>
<div class="event-card bullish"><div class="event-header"><span class="event-tag policy">政策</span><span class="event-timer">已公布</span></div><div class="event-title">央行宣布降准 0.25%</div><div class="event-impact"><span class="impact-badge bear">利多有色</span></div><div class="mini-contracts"><span class="mini-tag">IF</span><span class="mini-tag">CU</span></div></div>
</div>
<div class="overview-layout"><div class="left-col">
<div class="section-header">市场热力图</div>
<div class="heatmap-grid">
<div class="heat-cell heat-up-3" style="grid-column:span 2"><div class="name">SC 原油</div><div class="val">+3.8%</div></div>
<div class="heat-cell heat-up-1"><div class="name">AG 沪银</div><div class="val">+2.8%</div></div>
<div class="heat-cell heat-down-2"><div class="name">SA 纯碱</div><div class="val">-3.1%</div></div>
<div class="heat-cell heat-flat"><div class="name">M 豆粕</div><div class="val">0.0%</div></div>
<div class="heat-cell heat-up-1"><div class="name">IF 股指</div><div class="val">+1.8%</div></div>
<div class="heat-cell heat-flat"><div class="name">RB 螺纹</div><div class="val">-0.5%</div></div>
</div></div>
<div class="side-panel">
<div class="panel-box"><div class="panel-box-title">主力资金流向</div>
<div class="flow-row"><div class="flow-code">SC</div><div class="flow-bar-bg"><div class="flow-bar" style="width:80%;background:var(--color-up)"></div></div><div class="flow-val" style="color:var(--color-up)">+5.2亿</div></div>
<div class="flow-row"><div class="flow-code">SA</div><div class="flow-bar-bg"><div class="flow-bar" style="width:70%;background:var(--color-down)"></div></div><div class="flow-val" style="color:var(--color-down)">-4.5亿</div></div>
</div>
<div class="panel-box"><div class="panel-box-title">宏观日历</div>
<div class="calendar-item"><div class="cal-time">23:30</div><div><div class="cal-event">EIA 原油库存</div><div class="cal-stars">&#x2605;&#x2605;&#x2605;</div></div></div>
<div class="calendar-item"><div class="cal-time">周五</div><div><div class="cal-event">非农数据</div><div class="cal-stars">&#x2605;&#x2605;&#x2605;&#x2605;</div></div></div>
</div></div></div></div></div>
<div id="alerts-view" class="view"><div class="container"><div class="alerts-layout">
<div class="side-panel">
<div class="ai-scan-card"><div class="ai-scan-title">今日风险指数: 72/100</div>
<div class="ai-scan-item"><b>疑似洗盘</b><br>纯碱 10:45 急跌后迅速拉回。</div>
<div class="ai-scan-item"><b>流动性枯竭</b><br>胶板 2610 连续 3 分钟无成交。</div></div>
<div class="panel-box"><div class="panel-box-title">预警统计</div>
<div style="display:flex;justify-content:space-between;font-size:12px;margin-bottom:4px"><span>今日触发</span><span style="font-weight:600">24 次</span></div>
<div style="display:flex;justify-content:space-between;font-size:12px"><span>误报率</span><span style="font-weight:600;color:var(--color-down)">12%</span></div></div></div>
<div class="alert-feed">
<div class="alert-filters"><div class="pill active">全部</div><div class="pill">紧急</div><div class="pill">事件驱动</div></div>
<div class="alert-card urgent"><div class="alert-time">14:32 - 关联事件: EIA</div><div class="alert-title">燃油快速突破 60 日均线</div><div class="alert-desc">EIA 库存超预期下降 | 5 分钟涨幅 1.5%</div><div class="alert-actions"><span>查看详情</span><span>AI 分析</span><span>加入自选</span></div></div>
<div class="alert-card urgent"><div class="alert-time">13:15</div><div class="alert-title">沪银成交量异常放大</div><div class="alert-desc">成交量为 20 日均值的 4.2 倍</div><div class="alert-actions"><span>查看详情</span><span>AI 分析</span></div></div>
<div class="alert-card"><div class="alert-time">11:20</div><div class="alert-title">螺纹 MACD 金叉</div><div class="alert-desc">15 分钟级别 MACD 快线上穿慢线</div><div class="alert-actions"><span>查看详情</span></div></div>
</div>
<div class="alerts-sidebar">
<div class="rule-group"><div class="rule-group-title">价格与指标</div>
<div class="rule-item" onclick="this.querySelector('.toggle').classList.toggle('on')"><span>价格突破 60 日均线</span><div class="toggle on"></div></div>
<div class="rule-item" onclick="this.querySelector('.toggle').classList.toggle('on')"><span>MACD 金叉/死叉</span><div class="toggle on"></div></div>
</div>
<div class="rule-group"><div class="rule-group-title">量能与事件</div>
<div class="rule-item" onclick="this.querySelector('.toggle').classList.toggle('on')"><span>成交量突增 > 3 倍</span><div class="toggle on"></div></div>
<div class="rule-item" onclick="this.querySelector('.toggle').classList.toggle('on')"><span>热点事件关联异动</span><div class="toggle"></div></div>
</div></div>
</div></div></div>
<div id="summary-view" class="view"><div class="container">
<div class="summary-header"><h2>复盘与交易计划</h2><div class="date">2026-06-01 (周一)</div></div>
<div class="summary-layout"><div class="summary-left">
<div class="rank-section"><div class="rank-title">成交量排名 Top 5</div>
<div class="rank-row" onclick="showDetail('SC')"><div class="rank-rank top">1</div><div class="rank-name">SC 原油</div><div class="rank-val up">58.2 万手</div></div>
<div class="rank-row" onclick="showDetail('RB')"><div class="rank-rank top">2</div><div class="rank-name">RB 螺纹</div><div class="rank-val up">52.1 万手</div></div>
<div class="rank-row" onclick="showDetail('AG')"><div class="rank-rank top">3</div><div class="rank-name">AG 沪银</div><div class="rank-val up">45.8 万手</div></div>
<div class="rank-row" onclick="showDetail('SA')"><div class="rank-rank">4</div><div class="rank-name">SA 纯碱</div><div class="rank-val up">38.6 万手</div></div>
<div class="rank-row" onclick="showDetail('IF')"><div class="rank-rank">5</div><div class="rank-name">IF 股指</div><div class="rank-val">32.4 万手</div></div>
</div>
<div class="rank-section"><div class="rank-title">振幅排名 Top 5</div>
<div class="rank-row" onclick="showDetail('SA')"><div class="rank-rank top">1</div><div class="rank-name">SA 纯碱</div><div class="rank-val">6.8%</div></div>
<div class="rank-row" onclick="showDetail('SC')"><div class="rank-rank top">2</div><div class="rank-name">SC 原油</div><div class="rank-val">5.2%</div></div>
<div class="rank-row" onclick="showDetail('AG')"><div class="rank-rank top">3</div><div class="rank-name">AG 沪银</div><div class="rank-val">4.5%</div></div>
<div class="rank-row" onclick="showDetail('FG')"><div class="rank-rank">4</div><div class="rank-name">FG 玻璃</div><div class="rank-val">3.9%</div></div>
<div class="rank-row" onclick="showDetail('LU')"><div class="rank-rank">5</div><div class="rank-name">LU 燃油</div><div class="rank-val">3.2%</div></div>
</div>
<div class="rank-section"><div class="rank-title">涨跌幅排名 Top 5</div>
<div class="rank-row" onclick="showDetail('SC')"><div class="rank-rank top">1</div><div class="rank-name">SC 原油</div><div class="rank-val up">+3.8%</div></div>
<div class="rank-row" onclick="showDetail('AG')"><div class="rank-rank top">2</div><div class="rank-name">AG 沪银</div><div class="rank-val up">+2.8%</div></div>
<div class="rank-row" onclick="showDetail('IF')"><div class="rank-rank top">3</div><div class="rank-name">IF 股指</div><div class="rank-val up">+1.8%</div></div>
<div class="rank-row" onclick="showDetail('SA')"><div class="rank-rank">4</div><div class="rank-name">SA 纯碱</div><div class="rank-val down">-3.1%</div></div>
<div class="rank-row" onclick="showDetail('FG')"><div class="rank-rank">5</div><div class="rank-name">FG 玻璃</div><div class="rank-val down">-1.9%</div></div>
</div>
<div class="rank-section"><div class="rank-title">持仓量排名 Top 5</div>
<div class="rank-row" onclick="showDetail('IF')"><div class="rank-rank top">1</div><div class="rank-name">IF 股指</div><div class="rank-val">42.8 万手</div></div>
<div class="rank-row" onclick="showDetail('RB')"><div class="rank-rank top">2</div><div class="rank-name">RB 螺纹</div><div class="rank-val">38.5 万手</div></div>
<div class="rank-row" onclick="showDetail('SC')"><div class="rank-rank top">3</div><div class="rank-name">SC 原油</div><div class="rank-val">28.4 万手</div></div>
<div class="rank-row" onclick="showDetail('M')"><div class="rank-rank">4</div><div class="rank-name">M 豆粕</div><div class="rank-val">24.2 万手</div></div>
<div class="rank-row" onclick="showDetail('MA')"><div class="rank-rank">5</div><div class="rank-name">MA 甲醇</div><div class="rank-val">20.8 万手</div></div>
</div></div>
<div>
<div class="plan-header">交易机会</div>
<div class="plan-list">
<div class="plan-list-item" onclick="togglePlan(this)">
<div class="plan-list-left"><span class="plan-list-code">SC 原油</span><span class="plan-badge long">做多</span></div>
<div style="display:flex;align-items:center;gap:8px"><span class="plan-list-score">88</span><span class="plan-list-arrow">&#x25BC;</span></div>
</div>
<div class="plan-detail">
<div class="plan-card" style="margin-bottom:0">
<div class="plan-ai"><strong>评分: 88/100</strong><br>多空逻辑: EIA 库存超预期下降 + OPEC+ 延长减产 + 地缘局势升温,供给端紧缩共振。<br>入选理由: 量价齐升5/15/30min 周期共振向上MACD 金叉确认。</div>
<div class="plan-targets">
<div class="plan-target"><div class="plan-target-label">入场</div><div class="plan-target-val neutral">645-648</div></div>
<div class="plan-target"><div class="plan-target-label">止损</div><div class="plan-target-val red">638</div></div>
<div class="plan-target"><div class="plan-target-label">止盈</div><div class="plan-target-val green">670-685</div></div>
</div>
<div class="plan-actions"><button class="plan-btn primary">加入自选</button></div>
<div class="plan-note">置信度: 高 | 仓位建议: 15%-20%</div>
</div></div>
<div class="plan-list-item" onclick="togglePlan(this)">
<div class="plan-list-left"><span class="plan-list-code">SA 纯碱</span><span class="plan-badge short">做空</span></div>
<div style="display:flex;align-items:center;gap:8px"><span class="plan-list-score">82</span><span class="plan-list-arrow">&#x25BC;</span></div>
</div>
<div class="plan-detail">
<div class="plan-card" style="margin-bottom:0">
<div class="plan-ai"><strong>评分: 82/100</strong><br>多空逻辑: 高库存 + 光伏玻璃需求下滑 + 新产能投产预期,基本面偏弱。<br>入选理由: 日内振幅 6.8% 居首15min 级别顶背离确认,资金净流出 4.5 亿。</div>
<div class="plan-targets">
<div class="plan-target"><div class="plan-target-label">入场</div><div class="plan-target-val neutral">1,820-1,830</div></div>
<div class="plan-target"><div class="plan-target-label">止损</div><div class="plan-target-val red">1,860</div></div>
<div class="plan-target"><div class="plan-target-label">止盈</div><div class="plan-target-val green">1,750-1,720</div></div>
</div>
<div class="plan-actions"><button class="plan-btn primary">加入自选</button></div>
<div class="plan-note">置信度: 中高 | 仓位建议: 10%-15%</div>
</div></div>
<div class="plan-list-item" onclick="togglePlan(this)">
<div class="plan-list-left"><span class="plan-list-code">AG 沪银</span><span class="plan-badge long">做多</span></div>
<div style="display:flex;align-items:center;gap:8px"><span class="plan-list-score">79</span><span class="plan-list-arrow">&#x25BC;</span></div>
</div>
<div class="plan-detail">
<div class="plan-card" style="margin-bottom:0">
<div class="plan-ai"><strong>评分: 79/100</strong><br>多空逻辑: 全球降息预期 + 央行持续增持黄金 + 避险情绪升温。<br>入选理由: 成交量 45.8 万手放量突破,多周期均线多头排列。</div>
<div class="plan-targets">
<div class="plan-target"><div class="plan-target-label">入场</div><div class="plan-target-val neutral">18,500-18,600</div></div>
<div class="plan-target"><div class="plan-target-label">止损</div><div class="plan-target-val red">18,350</div></div>
<div class="plan-target"><div class="plan-target-label">止盈</div><div class="plan-target-val green">19,000-19,200</div></div>
</div>
<div class="plan-actions"><button class="plan-btn primary">加入自选</button></div>
<div class="plan-note">置信度: 中 | 仓位建议: 10%</div>
</div></div>
</div>
<div class="plan-note" style="font-size:11px;margin-top:16px">以上由 AI 基于多维因子自动生成,仅供参考,不构成投资建议。</div>
</div></div></div></div>
<div id="detail-modal" class="modal-overlay" onclick="if(event.target===this)closeModal()">
<div class="modal"><div class="modal-header"><div class="modal-title" id="modal-title">品种详情</div><button class="modal-close" onclick="closeModal()">&#x2715;</button></div><div class="kline-chart" id="kline-container"></div><div class="modal-body" id="modal-body"></div><div class="modal-footer"><button class="plan-btn primary" onclick="closeModal()">加入自选</button><button class="plan-btn secondary" onclick="closeModal()">关闭</button></div></div></div>
</body></html>

@ -0,0 +1,338 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>期货智析 - Android 平板版</title>
<style>
:root {
--bg-page: #F1F3F5;
--bg-card: #FFFFFF;
--bg-inset: #F8F9FA;
--text-primary: #1D1D1F;
--text-secondary: #5C5C5C;
--text-tertiary: #8E8E93;
--border-light: #E5E5EA;
--color-up: #FF3B30;
--color-down: #34C759;
--color-neutral: #FF9F0A;
--color-brand: #007AFF;
--color-ai: #AF52DE;
--shadow-sm: 0 1px 2px rgba(0,0,0,0.04);
--shadow-md: 0 4px 12px rgba(0,0,0,0.06);
--shadow-lg: 0 8px 24px rgba(0,0,0,0.1);
--radius-card: 16px;
--radius-pill: 100px;
}
* { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
body { font-family: "Roboto", "Segoe UI Variable", sans-serif; background: var(--bg-page); color: var(--text-primary); overflow-x: hidden; }
a { text-decoration: none; color: var(--color-brand); }
.top-bar { background: var(--bg-card); padding: 12px 24px; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--border-light); position: sticky; top: 0; z-index: 100; }
.logo { font-size: 18px; font-weight: 700; color: var(--text-primary); }
.live-badge { display: flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; color: var(--color-down); background: rgba(52,199,89,0.1); padding: 6px 12px; border-radius: var(--radius-pill); }
.dot { width: 6px; height: 6px; background: var(--color-down); border-radius: 50%; animation: pulse 2s infinite; }
@keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.5; } 100% { opacity: 1; } }
.tabs { display: flex; justify-content: space-around; background: var(--bg-card); padding: 8px 12px; border-bottom: 1px solid var(--border-light); }
.tab { padding: 8px 16px; font-size: 14px; font-weight: 500; color: var(--text-secondary); border-radius: 8px; cursor: pointer; }
.tab.active { background: rgba(0,122,255,0.1); color: var(--color-brand); font-weight: 600; }
.container { padding: 16px; padding-bottom: 32px; }
.view { display: none; }
.view.active { display: block; }
/* --- Components --- */
.toolbar { display: flex; gap: 12px; margin-bottom: 20px; }
.search-box { flex: 1; background: var(--bg-inset); border: 1px solid var(--border-light); border-radius: 12px; padding: 0 16px; height: 44px; font-size: 14px; }
.pills { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 4px; }
.pill { padding: 8px 16px; border-radius: 12px; font-size: 14px; font-weight: 500; background: var(--bg-card); color: var(--text-secondary); cursor: pointer; white-space: nowrap; border: 1px solid var(--border-light); }
.pill.active { background: var(--color-brand); color: white; border-color: var(--color-brand); }
.stats-row { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; margin-bottom: 20px; }
.stat-card { background: var(--bg-card); border-radius: 12px; padding: 16px; display: flex; gap: 12px; align-items: center; box-shadow: var(--shadow-sm); }
.stat-icon { width: 40px; height: 40px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 16px; font-weight: bold; }
.stat-icon.blue { background: rgba(0,122,255,0.1); color: var(--color-brand); }
.stat-icon.green { background: rgba(52,199,89,0.1); color: var(--color-down); }
.stat-icon.red { background: rgba(255,59,48,0.1); color: var(--color-up); }
.stat-icon.orange { background: rgba(255,159,10,0.1); color: var(--color-neutral); }
.stat-val { font-size: 18px; font-weight: 700; }
.stat-label { font-size: 12px; color: var(--text-tertiary); }
.grid { display: grid; grid-template-columns: 1fr; gap: 16px; }
.card { background: var(--bg-card); border-radius: var(--radius-card); padding: 20px; box-shadow: var(--shadow-md); }
.card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 16px; }
.card-left { display: flex; gap: 12px; align-items: flex-start; }
.code-box { width: 44px; height: 44px; background: var(--bg-inset); border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: 600; color: var(--text-secondary); }
.info-group { display: flex; flex-direction: column; }
.name-row { display: flex; align-items: center; gap: 6px; font-weight: 600; font-size: 16px; }
.contract-code { font-size: 13px; color: var(--text-tertiary); margin-top: 2px; }
.price-area { text-align: right; }
.price { font-size: 28px; font-weight: 700; line-height: 1; }
.price.up { color: var(--color-up); }
.price.down { color: var(--color-down); }
.change { font-size: 14px; font-weight: 600; margin-top: 4px; }
.change.up { color: var(--color-up); }
.change.down { color: var(--color-down); }
.action-pill { display: inline-block; padding: 6px 14px; border-radius: var(--radius-pill); font-size: 13px; font-weight: 600; margin-bottom: 16px; }
.action-pill.do-more { background: rgba(52,199,89,0.1); color: var(--color-down); }
.action-pill.watch { background: rgba(255,159,10,0.1); color: var(--color-neutral); }
.metrics { display: flex; gap: 24px; margin-bottom: 16px; }
.metric { flex: 1; }
.metric-label { font-size: 12px; color: var(--text-tertiary); margin-bottom: 4px; }
.bar-bg { height: 6px; background: var(--bg-inset); border-radius: 3px; margin-top: 6px; overflow: hidden; }
.bar-fill { height: 100%; border-radius: 3px; }
.timeframes { display: flex; gap: 8px; margin-bottom: 16px; }
.tf { padding: 6px 12px; border-radius: 8px; font-size: 12px; color: var(--text-tertiary); background: var(--bg-inset); cursor: pointer; }
.tf.active { background: #E8F2FF; color: var(--color-brand); font-weight: 500; }
.card-footer { display: flex; justify-content: space-between; align-items: center; border-top: 1px solid #F5F5F7; padding-top: 14px; font-size: 13px; }
.support-resist { color: var(--text-tertiary); }
.support-resist span { margin-right: 10px; }
.support-resist .red { color: var(--color-up); }
.support-resist .green { color: var(--color-down); }
.link { color: var(--color-brand); font-weight: 500; }
.fav-icon { font-size: 16px; color: #D1D1D6; cursor: pointer; padding: 4px; }
.fav-icon.active { color: #FF9F0A; }
/* --- Market Overview --- */
.event-banner { display: flex; gap: 12px; overflow-x: auto; padding-bottom: 8px; margin-bottom: 20px; }
.event-card { min-width: 220px; flex: 1; background: var(--bg-card); border-radius: 12px; padding: 16px; box-shadow: var(--shadow-sm); border: 1px solid var(--border-light); }
.event-card.bearish { border-left: 4px solid var(--color-up); }
.event-card.bullish { border-left: 4px solid var(--color-down); }
.event-header { display: flex; justify-content: space-between; margin-bottom: 6px; align-items: center; }
.event-tag { font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 4px; }
.event-tag.urgent { background: #FFF0F0; color: var(--color-up); }
.event-tag.policy { background: #E8F2FF; color: var(--color-brand); }
.event-timer { font-size: 12px; color: var(--text-tertiary); font-weight: 500; }
.event-title { font-size: 15px; font-weight: 600; margin-bottom: 6px; line-height: 1.3; }
.event-impact { display: flex; gap: 8px; align-items: center; font-size: 13px; margin-bottom: 8px; }
.impact-badge { font-size: 11px; padding: 3px 8px; border-radius: 4px; font-weight: 600; }
.impact-badge.bull { background: rgba(255,59,48,0.1); color: var(--color-up); }
.impact-badge.bear { background: rgba(52,199,89,0.1); color: var(--color-down); }
.mini-contracts { display: flex; gap: 6px; }
.mini-tag { font-size: 11px; background: var(--bg-inset); padding: 3px 8px; border-radius: 4px; color: var(--text-secondary); }
.overview-layout { display: flex; flex-direction: column; gap: 20px; }
.section-header { font-size: 14px; font-weight: 600; color: var(--text-secondary); margin-bottom: 12px; display: flex; justify-content: space-between; }
.heatmap-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; margin-bottom: 20px; }
.heat-cell { padding: 16px; border-radius: 12px; display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center; min-height: 100px; }
.heat-cell .name { font-weight: 600; margin-bottom: 6px; font-size: 15px; }
.heat-cell .val { font-size: 14px; font-weight: 700; }
.heat-up-3 { background: #FF3B30; color: white; }
.heat-up-1 { background: rgba(255,59,48,0.15); color: var(--color-up); }
.heat-down-2 { background: rgba(52,199,89,0.3); color: #10803D; }
.heat-down-1 { background: rgba(52,199,89,0.1); color: var(--color-down); }
.heat-flat { background: #E8E8ED; color: var(--text-secondary); }
.limit-list { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.limit-col h4 { font-size: 12px; color: var(--text-tertiary); margin-bottom: 10px; font-weight: 600; }
.limit-row { display: flex; justify-content: space-between; font-size: 14px; margin-bottom: 8px; }
.limit-row span:first-child { font-weight: 500; }
.limit-row span:last-child { font-weight: 600; }
.side-panel { display: flex; flex-direction: column; gap: 16px; }
.panel-box { background: var(--bg-card); border-radius: 16px; padding: 16px; box-shadow: var(--shadow-sm); }
.panel-box-title { font-size: 14px; font-weight: 600; margin-bottom: 12px; }
.flow-row { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; font-size: 14px; }
.flow-code { width: 40px; font-weight: 600; }
.flow-bar-bg { flex: 1; height: 8px; background: var(--bg-inset); border-radius: 4px; overflow: hidden; }
.flow-bar { height: 100%; border-radius: 4px; }
.flow-val { width: 60px; text-align: right; font-weight: 600; }
.calendar-item { display: flex; gap: 12px; margin-bottom: 12px; align-items: flex-start; }
.cal-time { font-size: 12px; color: var(--text-tertiary); margin-top: 2px; }
.cal-event { font-size: 14px; font-weight: 500; margin-bottom: 2px; }
.cal-stars { color: var(--color-neutral); font-size: 12px; }
/* --- Risk Alerts --- */
.alerts-layout { display: grid; grid-template-columns: 1fr; gap: 16px; }
.alerts-sidebar { background: var(--bg-card); border-radius: 16px; padding: 16px; box-shadow: var(--shadow-sm); }
.rule-group { margin-bottom: 16px; }
.rule-group-title { font-size: 13px; font-weight: 600; color: var(--text-tertiary); margin-bottom: 12px; border-bottom: 1px solid var(--border-light); padding-bottom: 6px; }
.rule-item { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; font-size: 15px; padding: 4px 0; }
.toggle { width: 44px; height: 24px; background: #E8E8ED; border-radius: 12px; position: relative; cursor: pointer; transition: background 0.2s; }
.toggle.on { background: var(--color-brand); }
.toggle::after { content: ''; position: absolute; top: 2px; left: 2px; width: 20px; height: 20px; background: white; border-radius: 50%; transition: transform 0.2s; box-shadow: 0 2px 4px rgba(0,0,0,0.15); }
.toggle.on::after { transform: translateX(20px); }
.alert-feed { display: flex; flex-direction: column; gap: 16px; }
.alert-filters { display: flex; gap: 8px; margin-bottom: 8px; }
.alert-card { background: var(--bg-card); border-radius: 16px; padding: 16px; box-shadow: var(--shadow-sm); position: relative; overflow: hidden; }
.alert-card.urgent { border-left: 4px solid var(--color-up); }
.alert-time { font-size: 12px; color: var(--text-tertiary); margin-bottom: 6px; display: flex; justify-content: space-between; }
.alert-title { font-size: 16px; font-weight: 600; margin-bottom: 6px; }
.alert-desc { font-size: 14px; color: var(--text-secondary); line-height: 1.4; margin-bottom: 10px; }
.alert-actions { display: flex; gap: 16px; font-size: 14px; font-weight: 500; }
.alert-actions span { color: var(--color-brand); cursor: pointer; padding: 8px 0; }
.ai-scan-card { background: linear-gradient(135deg, #AF52DE 0%, #8E44AD 100%); border-radius: 16px; padding: 16px; color: white; box-shadow: var(--shadow-md); }
.ai-scan-title { font-size: 14px; font-weight: 600; margin-bottom: 10px; display: flex; align-items: center; gap: 8px; }
.ai-scan-item { background: rgba(255,255,255,0.15); border-radius: 10px; padding: 10px; margin-bottom: 10px; font-size: 13px; line-height: 1.4; }
</style>
</head>
<body>
<div class="top-bar">
<div class="logo">◈ 期货智析</div>
<div class="live-badge"><div class="dot"></div> LIVE</div>
</div>
<div class="tabs">
<div class="tab active" onclick="showView('list-view', this)">品种分析</div>
<div class="tab" onclick="showView('market-view', this)">市场概览</div>
<div class="tab" onclick="showView('alerts-view', this)">风险预警</div>
</div>
<script>
function toggleFav(el) { el.classList.toggle('active'); event.stopPropagation(); }
function showView(viewId, tab) {
document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
document.getElementById(viewId).classList.add('active');
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
}
</script>
<!-- === List View === -->
<div id="list-view" class="view active">
<div class="container">
<div class="toolbar">
<input type="text" class="search-box" placeholder="🔍 搜索...">
</div>
<div class="pills" style="margin-bottom: 20px;">
<div class="pill active">全部</div><div class="pill">能源</div><div class="pill">金属</div><div class="pill">农产品</div>
</div>
<div class="stats-row">
<div class="stat-card"><div class="stat-icon blue"></div><div><div class="stat-val">32</div><div class="stat-label">监控品种</div></div></div>
<div class="stat-card"><div class="stat-icon green"></div><div><div class="stat-val">7</div><div class="stat-label">上涨趋势</div></div></div>
<div class="stat-card"><div class="stat-icon red"></div><div><div class="stat-val">10</div><div class="stat-label">下跌趋势</div></div></div>
<div class="stat-card"><div class="stat-icon orange"></div><div><div class="stat-val">15</div><div class="stat-label">震荡整理</div></div></div>
</div>
<div class="grid">
<div class="card">
<div class="card-header"><div class="card-left"><div class="code-box">SC</div><div class="info-group"><div class="name-row">原油 <span class="fav-icon" onclick="toggleFav(this)"></span></div><div class="contract-code">SC2606</div></div></div><div class="price-area"><div class="price down">¥644.5</div><div class="change down">↓ -2.02%</div></div></div>
<div class="action-pill watch">观望</div>
<div class="metrics"><div class="metric"><div class="metric-label">成功率 60%</div><div class="bar-bg"><div class="bar-fill" style="width:60%; background:var(--color-neutral);"></div></div></div><div class="metric"><div class="metric-label">趋势评分 30</div><div class="bar-bg"><div class="bar-fill" style="width:30%; background:var(--color-up);"></div></div></div></div>
<div class="timeframes"><div class="tf active">15M</div><div class="tf">1H</div><div class="tf">4H</div></div>
<div class="card-footer"><div class="support-resist"><span>压力 <b class="red">642.4</b></span><span>支撑 <b class="green">631.7</b></span></div><a href="#" class="link">详情 →</a></div>
</div>
<div class="card">
<div class="card-header"><div class="card-left"><div class="code-box">AG</div><div class="info-group"><div class="name-row">沪银 <span class="fav-icon" onclick="toggleFav(this)"></span></div><div class="contract-code">AG2606</div></div></div><div class="price-area"><div class="price up">¥18,643</div><div class="change up">↑ +2.27%</div></div></div>
<div class="action-pill do-more">做多</div>
<div class="metrics"><div class="metric"><div class="metric-label">成功率 85%</div><div class="bar-bg"><div class="bar-fill" style="width:85%; background:var(--color-down);"></div></div></div><div class="metric"><div class="metric-label">趋势评分 65</div><div class="bar-bg"><div class="bar-fill" style="width:65%; background:var(--color-down);"></div></div></div></div>
<div class="timeframes"><div class="tf active">15M</div><div class="tf">1H</div><div class="tf">4H</div></div>
<div class="card-footer"><div class="support-resist"><span>压力 <b class="red">18,685</b></span><span>支撑 <b class="green">18,584</b></span></div><a href="#" class="link">详情 →</a></div>
</div>
</div>
</div>
</div>
<!-- === Market View === -->
<div id="market-view" class="view">
<div class="container">
<div class="event-banner">
<div class="event-card bearish">
<div class="event-header"><span class="event-tag urgent">🔥 突发</span><span class="event-timer">T-00:15</span></div>
<div class="event-title">EIA 原油库存意外大幅下降 500 万桶</div>
<div class="event-impact"><span class="impact-badge bull">利多原油</span></div>
<div class="mini-contracts"><span class="mini-tag">SC</span><span class="mini-tag">LU</span></div>
</div>
<div class="event-card bullish">
<div class="event-header"><span class="event-tag policy">🏛️ 政策</span><span class="event-timer">已公布</span></div>
<div class="event-title">央行宣布降准 0.25%</div>
<div class="event-impact"><span class="impact-badge bear">利多有色</span></div>
<div class="mini-contracts"><span class="mini-tag">IF</span><span class="mini-tag">CU</span></div>
</div>
</div>
<div class="section-header"><span>🔥 市场热力图</span></div>
<div class="heatmap-grid">
<div class="heat-cell heat-up-3"><div class="name">SC 原油</div><div class="val">+3.8%</div></div>
<div class="heat-cell heat-up-1"><div class="name">AG 沪银</div><div class="val">+2.8%</div></div>
<div class="heat-cell heat-down-2"><div class="name">SA 纯碱</div><div class="val">-3.1%</div></div>
<div class="heat-cell heat-down-1"><div class="name">RB 螺纹</div><div class="val">-0.5%</div></div>
<div class="heat-cell heat-flat"><div class="name">M 豆粕</div><div class="val">0.0%</div></div>
<div class="heat-cell heat-up-2"><div class="name">IF 股指</div><div class="val">+1.8%</div></div>
</div>
<div class="limit-list">
<div class="limit-col"><h4>🔴 领涨</h4><div class="limit-row"><span>SC 原油</span><span style="color:var(--color-up)">+3.8%</span></div><div class="limit-row"><span>AG 沪银</span><span style="color:var(--color-up)">+2.8%</span></div></div>
<div class="limit-col"><h4>🟢 领跌</h4><div class="limit-row"><span>SA 纯碱</span><span style="color:var(--color-down)">-3.1%</span></div><div class="limit-row"><span>FG 玻璃</span><span style="color:var(--color-down)">-1.9%</span></div></div>
</div>
<div class="side-panel" style="margin-top: 20px;">
<div class="panel-box">
<div class="panel-box-title">💰 主力资金流向</div>
<div class="flow-row"><div class="flow-code">SC</div><div class="flow-bar-bg"><div class="flow-bar" style="width:80%;background:var(--color-up);"></div></div><div class="flow-val" style="color:var(--color-up)">+5.2亿</div></div>
<div class="flow-row"><div class="flow-code">SA</div><div class="flow-bar-bg"><div class="flow-bar" style="width:70%;background:var(--color-down);"></div></div><div class="flow-val" style="color:var(--color-down)">-4.5亿</div></div>
</div>
<div class="panel-box">
<div class="panel-box-title">📅 宏观日历</div>
<div class="calendar-item"><div class="cal-time">23:30</div><div class="cal-content"><div class="cal-event">EIA 原油库存</div><div class="cal-stars">★★★</div></div></div>
<div class="calendar-item"><div class="cal-time">周五</div><div class="cal-content"><div class="cal-event">非农数据</div><div class="cal-stars">★★★★</div></div></div>
</div>
</div>
</div>
</div>
<!-- === Alerts View === -->
<div id="alerts-view" class="view">
<div class="container">
<div class="alerts-layout">
<div class="alerts-sidebar">
<div class="rule-group">
<div class="rule-group-title">价格与指标</div>
<div class="rule-item"><span>价格突破 60 日均线</span><div class="toggle on"></div></div>
<div class="rule-item"><span>MACD 金叉/死叉</span><div class="toggle on"></div></div>
</div>
<div class="rule-group">
<div class="rule-group-title">量能与事件</div>
<div class="rule-item"><span>成交量突增 > 3 倍</span><div class="toggle on"></div></div>
<div class="rule-item"><span>热点事件关联异动</span><div class="toggle"></div></div>
</div>
</div>
<div style="display:flex;flex-direction:column;gap:16px;">
<div class="ai-scan-card">
<div class="ai-scan-title">✦ 今日风险指数: 72/100</div>
<div class="ai-scan-item"><b>疑似洗盘</b><br>纯碱 10:45 急跌后迅速拉回,量能异常。</div>
<div class="ai-scan-item"><b>流动性枯竭</b><br>胶板 2610 连续 3 分钟无成交。</div>
</div>
<div class="alert-filters">
<div class="pill active" style="font-size:13px;padding:6px 12px;">全部</div>
<div class="pill" style="font-size:13px;padding:6px 12px;">紧急 (3)</div>
<div class="pill" style="font-size:13px;padding:6px 12px;">事件驱动</div>
</div>
<div class="alert-feed">
<div class="alert-card urgent">
<div class="alert-time">14:32 · 关联事件EIA 库存</div>
<div class="alert-title">🔥 燃油快速突破 60 日均线</div>
<div class="alert-desc">关联事件EIA 库存超预期下降 | 5 分钟涨幅 1.5%</div>
<div class="alert-actions"><span>查看详情</span><span>AI 分析</span><span>加入自选</span></div>
</div>
<div class="alert-card">
<div class="alert-time">13:15</div>
<div class="alert-title">🔴 沪银成交量异常放大</div>
<div class="alert-desc">当前成交量为过去 20 日均值的 4.2 倍</div>
<div class="alert-actions"><span>查看详情</span><span>AI 分析</span></div>
</div>
<div class="alert-card">
<div class="alert-time">11:20</div>
<div class="alert-title">螺纹 MACD 出现金叉</div>
<div class="alert-desc">15 分钟级别 MACD 快线上穿慢线</div>
<div class="alert-actions"><span>查看详情</span></div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

@ -0,0 +1,381 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1440, initial-scale=1.0">
<title>期货智析 - Windows 桌面端 (完整版)</title>
<style>
:root {
--bg-mica: #F3F3F3; --bg-solid: #FFFFFF; --bg-hover: rgba(0,0,0,0.03); --bg-active: rgba(0,122,255,0.06);
--bg-overlay: rgba(0,0,0,0.35); --border-light: #E5E5E5; --border-focus: #007AFF;
--text-primary: #1A1A1A; --text-secondary: #616161; --text-tertiary: #8E8E93;
--color-up: #FF3B30; --color-down: #34C759; --color-neutral: #FF9F0A; --color-brand: #007AFF; --color-ai: #AF52DE;
--shadow-card: 0 2px 6px rgba(0,0,0,0.04), 0 0 1px rgba(0,0,0,0.06);
--shadow-dialog: 0 16px 48px rgba(0,0,0,0.18), 0 4px 12px rgba(0,0,0,0.1);
--radius-window: 8px; --radius-card: 6px; --radius-dialog: 8px;
--font-win: "Segoe UI Variable", "Segoe UI", system-ui, sans-serif;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: var(--font-win); background: #E8E8E8; color: var(--text-primary); display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 40px; }
.app-window { width: 1440px; height: 900px; background: var(--bg-mica); border-radius: var(--radius-window); box-shadow: 0 24px 48px rgba(0,0,0,0.15), 0 0 0 1px rgba(0,0,0,0.1); display: flex; flex-direction: column; overflow: hidden; }
/* 标题栏 */
.title-bar { height: 32px; background: var(--bg-solid); border-bottom: 1px solid var(--border-light); display: flex; align-items: center; justify-content: space-between; padding: 0 0 0 12px; user-select: none; }
.title-left { display: flex; align-items: center; gap: 10px; font-size: 12px; font-weight: 500; }
.app-icon { width: 14px; height: 14px; background: var(--color-brand); border-radius: 3px; }
.window-controls { display: flex; height: 100%; }
.win-btn { width: 46px; height: 32px; display: flex; align-items: center; justify-content: center; cursor: pointer; }
.win-btn:hover { background: var(--bg-hover); } .win-btn.close:hover { background: #C42B1C; color: white; }
.win-btn svg { width: 10px; height: 10px; }
/* 布局 */
.main-layout { flex: 1; display: flex; overflow: hidden; }
.sidebar { width: 220px; background: var(--bg-solid); border-right: 1px solid var(--border-light); display: flex; flex-direction: column; padding: 8px 0; }
.nav-section { padding: 4px 12px; margin-bottom: 8px; }
.nav-label { font-size: 11px; font-weight: 600; color: var(--text-tertiary); padding: 4px 8px; margin-bottom: 4px; }
.nav-item { display: flex; align-items: center; gap: 10px; padding: 6px 8px; border-radius: 4px; cursor: pointer; font-size: 13px; color: var(--text-secondary); transition: all 0.15s; }
.nav-item:hover { background: var(--bg-hover); color: var(--text-primary); } .nav-item.active { background: var(--bg-active); color: var(--color-brand); font-weight: 500; }
.nav-badge { margin-left: auto; font-size: 10px; background: var(--bg-hover); padding: 1px 6px; border-radius: 10px; color: var(--text-tertiary); }
.nav-item.active .nav-badge { background: rgba(0,122,255,0.1); color: var(--color-brand); }
.sidebar-footer { margin-top: auto; padding: 12px; border-top: 1px solid var(--border-light); }
.status-pill { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--text-tertiary); padding: 4px 8px; }
.status-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--color-down); }
.content-area { display: none; flex-direction: column; flex: 1; overflow: hidden; } .content-area.active { display: flex; }
.scroll-content { flex: 1; overflow-y: auto; padding: 16px; }
.scroll-content::-webkit-scrollbar { width: 6px; } .scroll-content::-webkit-scrollbar-thumb { background: #C1C1C1; border-radius: 3px; }
/* 通用组件 */
.toolbar { height: 48px; background: var(--bg-mica); border-bottom: 1px solid var(--border-light); display: flex; align-items: center; gap: 12px; padding: 0 16px; }
.search-box { flex: 1; max-width: 320px; background: var(--bg-solid); border: 1px solid var(--border-light); border-radius: 4px; padding: 6px 10px; font-size: 13px; outline: none; }
.search-box:focus { border-color: var(--border-focus); } .search-box::placeholder { color: var(--text-tertiary); }
.filter-pills { display: flex; gap: 6px; }
.pill { padding: 4px 12px; border-radius: 4px; font-size: 12px; background: transparent; border: 1px solid var(--border-light); color: var(--text-secondary); cursor: pointer; }
.pill:hover { background: var(--bg-hover); } .pill.active { background: var(--color-brand); color: white; border-color: var(--color-brand); }
.toolbar-actions { margin-left: auto; display: flex; gap: 8px; }
.tool-btn { width: 32px; height: 32px; border-radius: 4px; border: 1px solid var(--border-light); background: var(--bg-solid); cursor: pointer; display: flex; align-items: center; justify-content: center; color: var(--text-secondary); }
.tool-btn:hover { background: var(--bg-hover); color: var(--text-primary); }
.card { background: var(--bg-solid); border: 1px solid var(--border-light); border-radius: var(--radius-card); padding: 14px; cursor: pointer; transition: all 0.15s; }
.card:hover { border-color: #D0D0D0; box-shadow: var(--shadow-card); }
.card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 10px; }
.card-left { display: flex; gap: 10px; align-items: center; }
.code-box { width: 34px; height: 34px; background: var(--bg-mica); border-radius: 4px; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 600; color: var(--text-tertiary); border: 1px solid var(--border-light); }
.info-group { display: flex; flex-direction: column; } .name-row { font-weight: 600; font-size: 14px; display: flex; align-items: center; gap: 4px; } .contract-code { font-size: 11px; color: var(--text-tertiary); }
.price-area { text-align: right; } .price { font-size: 20px; font-weight: 700; } .price.up { color: var(--color-up); } .price.down { color: var(--color-down); } .change { font-size: 11px; font-weight: 600; margin-top: 2px; } .change.up { color: var(--color-up); } .change.down { color: var(--color-down); }
.action-pill { display: inline-block; padding: 3px 10px; border-radius: 3px; font-size: 11px; font-weight: 600; margin-bottom: 10px; } .action-pill.do-more { background: rgba(52,199,89,0.1); color: var(--color-down); } .action-pill.watch { background: rgba(255,159,10,0.1); color: var(--color-neutral); }
.metrics { display: flex; gap: 16px; margin-bottom: 10px; } .metric { flex: 1; } .metric-label { font-size: 10px; color: var(--text-tertiary); } .bar-bg { height: 3px; background: var(--bg-mica); border-radius: 1.5px; margin-top: 4px; overflow: hidden; } .bar-fill { height: 100%; border-radius: 1.5px; }
.timeframes { display: flex; gap: 6px; margin-bottom: 10px; } .tf { padding: 3px 8px; border-radius: 3px; font-size: 10px; background: var(--bg-mica); border: 1px solid var(--border-light); cursor: pointer; } .tf.active { background: #E8F2FF; color: var(--color-brand); border-color: rgba(0,122,255,0.2); }
.card-footer { display: flex; justify-content: space-between; border-top: 1px solid var(--border-light); padding-top: 10px; font-size: 11px; color: var(--text-tertiary); } .link { color: var(--color-brand); }
.fav-icon { font-size: 13px; color: #C5C5C5; cursor: pointer; } .fav-icon:hover { color: #FF9F0A; } .fav-icon.active { color: #FF9F0A; }
.status-bar { height: 24px; background: var(--bg-solid); border-top: 1px solid var(--border-light); display: flex; align-items: center; justify-content: space-between; padding: 0 16px; font-size: 11px; color: var(--text-tertiary); }
.status-left, .status-right { display: flex; gap: 16px; } .status-item { display: flex; align-items: center; gap: 4px; }
/* 详情页 */
.detail-toolbar { height: 40px; background: var(--bg-mica); border-bottom: 1px solid var(--border-light); display: flex; align-items: center; padding: 0 16px; gap: 12px; }
.back-btn { padding: 4px 10px; border-radius: 4px; border: 1px solid var(--border-light); background: var(--bg-solid); font-size: 12px; cursor: pointer; }
.detail-title { font-size: 14px; font-weight: 600; } .detail-title span { color: var(--text-tertiary); font-weight: 400; margin-left: 8px; }
.detail-content { flex: 1; display: flex; overflow: hidden; }
.chart-panel { flex: 1.6; padding: 16px; overflow-y: auto; border-right: 1px solid var(--border-light); } .data-panel { flex: 1.4; padding: 16px; overflow-y: auto; background: var(--bg-solid); }
.chart-placeholder { width: 100%; height: 320px; background: var(--bg-solid); border: 1px solid var(--border-light); border-radius: var(--radius-card); display: flex; align-items: center; justify-content: center; color: var(--text-tertiary); margin-bottom: 16px; }
.chart-tf-bar { display: flex; gap: 6px; margin-bottom: 12px; } .chart-tf { padding: 4px 10px; border-radius: 4px; font-size: 11px; background: var(--bg-solid); border: 1px solid var(--border-light); cursor: pointer; } .chart-tf.active { background: #E8F2FF; color: var(--color-brand); }
.indicator-section { margin-bottom: 16px; } .section-title { font-size: 12px; font-weight: 600; margin-bottom: 8px; } .indicator-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.indicator-card { background: var(--bg-mica); border: 1px solid var(--border-light); border-radius: 4px; padding: 10px; } .indicator-label { font-size: 10px; color: var(--text-tertiary); } .indicator-value { font-size: 14px; font-weight: 600; } .indicator-signal { font-size: 10px; } .indicator-signal.buy { color: var(--color-down); } .indicator-signal.sell { color: var(--color-up); }
.panel-card { background: var(--bg-mica); border: 1px solid var(--border-light); border-radius: var(--radius-card); padding: 12px; margin-bottom: 12px; } .panel-card .card-title { font-size: 12px; font-weight: 600; margin-bottom: 10px; }
.data-row { display: flex; justify-content: space-between; padding: 4px 0; font-size: 11px; border-bottom: 1px solid var(--border-light); } .data-row:last-child { border-bottom: none; } .data-value.up { color: var(--color-up); } .data-value.down { color: var(--color-down); }
.ai-trigger { display: flex; align-items: center; justify-content: center; gap: 6px; padding: 8px; background: linear-gradient(135deg, var(--color-brand), var(--color-ai)); color: white; border-radius: 4px; font-weight: 600; cursor: pointer; margin-top: 8px; }
.ai-preview { background: var(--bg-solid); border: 1px solid var(--border-light); border-radius: var(--radius-card); padding: 12px; display: flex; align-items: center; gap: 12px; cursor: pointer; } .ai-preview:hover { border-color: var(--color-ai); box-shadow: 0 0 0 1px var(--color-ai); } .ai-preview-icon { width: 36px; height: 36px; background: linear-gradient(135deg, var(--color-brand), var(--color-ai)); border-radius: 6px; display: flex; align-items: center; justify-content: center; color: white; } .ai-preview-text { flex: 1; } .ai-preview-title { font-size: 12px; font-weight: 600; } .ai-preview-desc { font-size: 11px; color: var(--text-tertiary); } .ai-preview-arrow { color: var(--text-tertiary); }
/* 弹窗 */
.modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: var(--bg-overlay); z-index: 1000; align-items: center; justify-content: center; } .modal-overlay.active { display: flex; }
.modal-dialog { width: 520px; background: var(--bg-solid); border-radius: var(--radius-dialog); box-shadow: var(--shadow-dialog); border: 1px solid var(--border-light); overflow: hidden; animation: modalIn 0.2s; }
@keyframes modalIn { from { opacity: 0; transform: scale(0.96) translateY(8px); } to { opacity: 1; transform: scale(1) translateY(0); } }
.modal-header { height: 40px; background: var(--bg-mica); border-bottom: 1px solid var(--border-light); display: flex; align-items: center; justify-content: space-between; padding: 0 16px; } .modal-title { font-size: 13px; font-weight: 600; display: flex; align-items: center; gap: 6px; } .modal-title .ai-dot { width: 8px; height: 8px; background: var(--color-ai); border-radius: 50%; } .modal-close { width: 28px; height: 28px; border: none; background: transparent; cursor: pointer; } .modal-close:hover { background: var(--bg-hover); }
.modal-body { padding: 16px; max-height: 480px; overflow-y: auto; }
.ai-summary { background: linear-gradient(135deg, rgba(175,82,222,0.06), rgba(0,122,255,0.06)); border: 1px solid rgba(175,82,222,0.15); border-radius: 6px; padding: 12px; margin-bottom: 16px; } .ai-summary .label { font-size: 10px; color: var(--color-ai); font-weight: 600; margin-bottom: 4px; } .ai-summary .content { font-size: 12px; line-height: 1.6; }
.ai-section { margin-bottom: 16px; } .ai-section-title { font-size: 11px; font-weight: 600; margin-bottom: 8px; color: var(--text-secondary); }
.ai-metric-row { display: flex; gap: 8px; margin-bottom: 12px; } .ai-metric-card { flex: 1; background: var(--bg-mica); border: 1px solid var(--border-light); border-radius: 4px; padding: 10px; text-align: center; } .ai-metric-val { font-size: 18px; font-weight: 700; } .ai-metric-label { font-size: 10px; color: var(--text-tertiary); }
.ai-price-list { display: flex; flex-direction: column; gap: 6px; } .ai-price-item { display: flex; justify-content: space-between; padding: 6px 10px; background: var(--bg-mica); border-radius: 4px; font-size: 12px; } .ai-price-item .type { color: var(--text-secondary); }
.ai-risk { background: rgba(255,59,48,0.04); border: 1px solid rgba(255,59,48,0.1); border-radius: 4px; padding: 10px; display: flex; gap: 8px; align-items: flex-start; } .ai-risk-text { font-size: 11px; }
.modal-footer { height: 48px; background: var(--bg-mica); border-top: 1px solid var(--border-light); display: flex; align-items: center; justify-content: flex-end; gap: 8px; padding: 0 16px; } .btn { padding: 6px 16px; border-radius: 4px; font-size: 12px; cursor: pointer; border: 1px solid var(--border-light); background: var(--bg-solid); } .btn:hover { background: var(--bg-hover); } .btn-primary { background: var(--color-brand); color: white; border-color: var(--color-brand); }
/* ========== 市场概览 ========== */
.event-banner { display: flex; gap: 12px; padding: 16px; background: var(--bg-mica); border-bottom: 1px solid var(--border-light); overflow-x: auto; }
.event-card { flex: 1; min-width: 280px; background: var(--bg-solid); border: 1px solid var(--border-light); border-radius: var(--radius-card); padding: 12px; border-left: 3px solid var(--color-ai); cursor: pointer; transition: 0.15s; }
.event-card:hover { box-shadow: var(--shadow-card); transform: translateY(-1px); } .event-card.bullish { border-left-color: var(--color-down); } .event-card.bearish { border-left-color: var(--color-up); }
.event-header { display: flex; justify-content: space-between; margin-bottom: 6px; } .event-tag { font-size: 10px; padding: 2px 6px; border-radius: 3px; font-weight: 600; } .event-tag.urgent { background: rgba(255,59,48,0.1); color: var(--color-up); } .event-tag.policy { background: rgba(0,122,255,0.1); color: var(--color-brand); }
.event-title { font-size: 12px; font-weight: 600; margin-bottom: 6px; } .event-impact { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; } .impact-badge { font-size: 10px; padding: 2px 6px; border-radius: 10px; font-weight: 600; } .impact-badge.bull { background: rgba(255,59,48,0.1); color: var(--color-up); } .impact-badge.bear { background: rgba(52,199,89,0.1); color: var(--color-down); }
.event-contracts { display: flex; gap: 4px; } .mini-contract { font-size: 9px; padding: 2px 4px; background: var(--bg-mica); border: 1px solid var(--border-light); border-radius: 3px; }
.event-timer { font-size: 10px; color: var(--text-tertiary); font-family: monospace; }
.overview-grid { display: grid; grid-template-columns: 1.5fr 1fr; gap: 16px; padding: 16px; height: calc(100% - 90px); }
.overview-left { display: flex; flex-direction: column; gap: 12px; }
.section-header { font-size: 12px; font-weight: 600; display: flex; justify-content: space-between; align-items: center; }
.heatmap-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; min-height: 280px; }
.heat-cell { border-radius: 4px; padding: 10px; display: flex; flex-direction: column; justify-content: space-between; cursor: pointer; transition: 0.1s; border: 1px solid rgba(0,0,0,0.05); } .heat-cell:hover { transform: scale(1.02); box-shadow: 0 4px 8px rgba(0,0,0,0.15); }
.heat-up-3 { background: #D32F2F; color: #fff; } .heat-up-2 { background: #FF3B30; color: #fff; } .heat-up-1 { background: #FF8A80; color: #fff; }
.heat-down-3 { background: #2E7D32; color: #fff; } .heat-down-2 { background: #34C759; color: #fff; } .heat-down-1 { background: #81C784; color: #fff; } .heat-flat { background: #E5E5E5; }
.limit-list { background: var(--bg-solid); border: 1px solid var(--border-light); border-radius: var(--radius-card); padding: 12px; display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.limit-row { display: flex; justify-content: space-between; padding: 6px 0; border-bottom: 1px solid var(--border-light); font-size: 11px; } .limit-row:last-child { border-bottom: none; }
.side-panel { display: flex; flex-direction: column; gap: 12px; }
.panel-box { background: var(--bg-solid); border: 1px solid var(--border-light); border-radius: var(--radius-card); padding: 12px; } .panel-box-title { font-size: 11px; font-weight: 600; margin-bottom: 10px; display: flex; align-items: center; gap: 4px; }
.flow-row { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; font-size: 11px; } .flow-code { width: 30px; font-weight: 600; } .flow-bar-bg { flex: 1; height: 4px; background: var(--bg-mica); border-radius: 2px; overflow: hidden; } .flow-bar { height: 100%; border-radius: 2px; } .flow-val { width: 50px; text-align: right; font-weight: 600; font-size: 10px; }
.calendar-item { display: flex; gap: 10px; margin-bottom: 10px; font-size: 11px; } .cal-time { width: 40px; color: var(--text-tertiary); } .cal-stars { color: var(--color-neutral); font-size: 10px; }
/* ========== 风险预警 ========== */
.alerts-layout { display: grid; grid-template-columns: 300px 1fr 280px; height: 100%; overflow: hidden; }
.alerts-left { border-right: 1px solid var(--border-light); background: var(--bg-solid); padding: 16px; overflow-y: auto; }
.alerts-center { padding: 16px; overflow-y: auto; }
.alerts-right { border-left: 1px solid var(--border-light); background: var(--bg-solid); padding: 16px; overflow-y: auto; }
.rule-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; font-weight: 600; font-size: 13px; }
.rule-category { font-size: 10px; color: var(--text-tertiary); font-weight: 600; margin: 12px 0 6px; text-transform: uppercase; }
.rule-item { display: flex; justify-content: space-between; align-items: center; padding: 8px; border-radius: 4px; font-size: 12px; cursor: pointer; transition: 0.15s; margin-bottom: 2px; }
.rule-item:hover { background: var(--bg-hover); }
.rule-switch { width: 32px; height: 16px; border-radius: 8px; position: relative; transition: 0.2s; }
.rule-switch.on { background: var(--color-down); } .rule-switch.off { background: #CCC; }
.rule-switch::after { content: ''; position: absolute; top: 2px; left: 2px; width: 12px; height: 12px; background: white; border-radius: 50%; transition: 0.2s; }
.rule-switch.on::after { left: 18px; }
.alert-filters { display: flex; gap: 8px; margin-bottom: 16px; }
.timeline { position: relative; padding-left: 20px; } .timeline::before { content: ''; position: absolute; left: 6px; top: 0; bottom: 0; width: 2px; background: var(--border-light); }
.alert-card { background: var(--bg-solid); border: 1px solid var(--border-light); border-radius: var(--radius-card); padding: 12px; margin-bottom: 12px; position: relative; transition: 0.15s; }
.alert-card:hover { box-shadow: var(--shadow-card); }
.alert-card::before { content: ''; position: absolute; left: -17px; top: 16px; width: 8px; height: 8px; border-radius: 50%; border: 2px solid var(--bg-solid); }
.alert-card.urgent { border-left: 3px solid var(--color-up); } .alert-card.urgent::before { background: var(--color-up); }
.alert-card.event { border-left: 3px solid var(--color-ai); background: linear-gradient(90deg, rgba(175,82,222,0.04), transparent); } .alert-card.event::before { background: var(--color-ai); border: none; content: '🔥'; font-size: 10px; left: -19px; top: 14px; width: 12px; height: 12px; }
.alert-card.normal::before { background: var(--color-brand); }
.alert-time { font-size: 10px; color: var(--text-tertiary); margin-bottom: 4px; }
.alert-title { font-size: 12px; font-weight: 600; margin-bottom: 4px; }
.alert-desc { font-size: 11px; color: var(--text-secondary); margin-bottom: 8px; }
.alert-actions { display: flex; gap: 8px; font-size: 11px; } .alert-action { color: var(--color-brand); cursor: pointer; } .alert-action:hover { text-decoration: underline; }
.ai-scan-card { background: linear-gradient(135deg, rgba(175,82,222,0.1), rgba(0,122,255,0.05)); border: 1px solid var(--color-ai); border-radius: var(--radius-card); padding: 12px; margin-bottom: 12px; }
.ai-scan-title { font-size: 12px; font-weight: 600; color: var(--color-ai); margin-bottom: 8px; display: flex; align-items: center; gap: 4px; }
.ai-scan-item { font-size: 11px; background: var(--bg-solid); border-radius: 4px; padding: 8px; margin-bottom: 6px; border: 1px solid var(--border-light); }
</style>
</head>
<body>
<script>
function toggleFav(el){el.classList.toggle('active')}
function toggleSwitch(el){const s=el.querySelector('.rule-switch');s.classList.toggle('on');s.classList.toggle('off');}
function showView(id) {
document.querySelectorAll('.content-area').forEach(el => el.classList.remove('active'));
document.getElementById(id).classList.add('active');
document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
if(id==='list-view'||id==='detail-view') document.querySelectorAll('.nav-item')[0].classList.add('active');
if(id==='market-view') document.querySelectorAll('.nav-item')[1].classList.add('active');
if(id==='alerts-view') document.querySelectorAll('.nav-item')[2].classList.add('active');
}
function showDetail(){showView('detail-view');}
function hideDetail(){showView('list-view');}
function showModal(){document.querySelector('.modal-overlay').classList.add('active');}
function hideModal(){document.querySelector('.modal-overlay').classList.remove('active');}
</script>
<div class="app-window">
<div class="title-bar">
<div class="title-left"><div class="app-icon"></div><span>期货智析 Pro</span></div>
<div class="window-controls">
<div class="win-btn minimize"><svg viewBox="0 0 10 10"><path d="M1 5h8" stroke="currentColor" stroke-width="1"/></svg></div>
<div class="win-btn maximize"><svg viewBox="0 0 10 10"><rect x="1.5" y="1.5" width="7" height="7" fill="none" stroke="currentColor" stroke-width="1"/></svg></div>
<div class="win-btn close"><svg viewBox="0 0 10 10"><path d="M2 2l6 6M8 2l-6 6" stroke="currentColor" stroke-width="1.2"/></svg></div>
</div>
</div>
<div class="main-layout">
<aside class="sidebar">
<div class="nav-section">
<div class="nav-item active" onclick="showView('list-view')"><span>📊</span> 品种分析</div>
<div class="nav-item" onclick="showView('market-view')"><span>🌍</span> 市场概览</div>
<div class="nav-item" onclick="showView('alerts-view')"><span>⚠️</span> 风险预警 <span class="nav-badge" style="background:var(--color-up);color:white">3</span></div>
</div>
<div class="nav-section">
<div class="nav-label">板块分类</div>
<div class="nav-item"><span>🛢️</span> 能源化工 <span class="nav-badge">8</span></div>
<div class="nav-item"><span>⚙️</span> 有色金属 <span class="nav-badge">10</span></div>
<div class="nav-item"><span>🌾</span> 农产品 <span class="nav-badge">9</span></div>
</div>
<div class="nav-section">
<div class="nav-label">工具</div>
<div class="nav-item"><span>⚙️</span> 设置</div>
</div>
<div class="sidebar-footer"><div class="status-pill"><div class="status-dot"></div> 行情正常 | 12ms</div></div>
</aside>
<!-- 列表页 -->
<main class="content-area active" id="list-view">
<div class="toolbar">
<input class="search-box" placeholder="搜索品种或代码...">
<div class="filter-pills"><div class="pill active">全部</div><div class="pill">能源</div><div class="pill">金属</div></div>
<div class="toolbar-actions"><button class="tool-btn"></button></div>
</div>
<div class="scroll-content">
<div class="grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:12px;">
<div class="card" onclick="showDetail()"><div class="card-header"><div class="card-left"><div class="code-box">SC</div><div class="info-group"><div class="name-row">原油 <span class="fav-icon" onclick="toggleFav(this)"></span></div><div class="contract-code">SC2606</div></div></div><div class="price-area"><div class="price down">644.5</div><div class="change down">↓ -2.02%</div></div></div><div class="action-pill watch">观望</div><div class="card-footer"><a href="#" class="link">详情 →</a></div></div>
<div class="card"><div class="card-header"><div class="card-left"><div class="code-box">FU</div><div class="info-group"><div class="name-row">燃油 <span class="fav-icon active" onclick="toggleFav(this)"></span></div><div class="contract-code">FU2606</div></div></div><div class="price-area"><div class="price up">4,680</div><div class="change up">↑ +1.56%</div></div></div><div class="action-pill do-more">做多</div><div class="card-footer"><a href="#" class="link">详情 →</a></div></div>
<div class="card"><div class="card-header"><div class="card-left"><div class="code-box">AG</div><div class="info-group"><div class="name-row">沪银 <span class="fav-icon" onclick="toggleFav(this)"></span></div><div class="contract-code">AG2606</div></div></div><div class="price-area"><div class="price up">18,643</div><div class="change up">↑ +2.27%</div></div></div><div class="action-pill do-more">做多</div><div class="card-footer"><a href="#" class="link">详情 →</a></div></div>
<div class="card"><div class="card-header"><div class="card-left"><div class="code-box">AU</div><div class="info-group"><div class="name-row">沪金 <span class="fav-icon" onclick="toggleFav(this)"></span></div><div class="contract-code">AU2606</div></div></div><div class="price-area"><div class="price up">993</div><div class="change up">↑ +0.4%</div></div></div><div class="action-pill watch">观望</div><div class="card-footer"><a href="#" class="link">详情 →</a></div></div>
</div>
</div>
<div class="status-bar"><div class="status-left"><div class="status-item">📡 CTP</div></div><div class="status-right"><span>自动刷新 ON</span></div></div>
</main>
<!-- 详情页 -->
<main class="content-area" id="detail-view">
<div class="detail-toolbar"><div class="back-btn" onclick="hideDetail()">← 返回</div><div class="detail-title">原油 <span>SC2606</span></div><div style="margin-left:auto;"><span class="fav-icon active" onclick="toggleFav(this)"></span> <span class="price down" style="font-size:18px;">644.5</span></div></div>
<div class="detail-content">
<div class="chart-panel">
<div class="chart-tf-bar"><div class="chart-tf active">15M</div><div class="chart-tf">1H</div><div class="chart-tf">4H</div></div>
<div class="chart-placeholder">📈 K 线图区域</div>
<div class="ai-preview" onclick="showModal()"><div class="ai-preview-icon"></div><div class="ai-preview-text"><div class="ai-preview-title">AI 多因子分析报告</div><div class="ai-preview-desc">综合评分 38 · 偏空</div></div><div class="ai-preview-arrow"></div></div>
</div>
<div class="data-panel">
<div class="panel-card"><div class="card-title">持仓数据</div><div class="data-row"><span>持仓量</span><span class="data-value">28.4 万手</span></div><div class="data-row"><span>日增仓</span><span class="data-value down">-1.2 万</span></div></div>
<div class="panel-card"><div class="card-title">资金流向</div><div class="data-row"><span>主力净流入</span><span class="data-value down">-2.8 亿</span></div></div>
<button class="ai-trigger" onclick="showModal()">✦ 生成 AI 报告</button>
</div>
</div>
<div class="status-bar"><div class="status-left"><div class="status-item">📡 CTP</div></div><div class="status-right"><span>SC2606</span></div></div>
</main>
<!-- 市场概览页 -->
<main class="content-area" id="market-view">
<div class="event-banner">
<div class="event-card bearish">
<div class="event-header"><span class="event-tag urgent">🔥 突发</span><span class="event-timer">T-00:15</span></div>
<div class="event-title">EIA 原油库存意外大幅下降 500 万桶</div>
<div class="event-impact"><span class="impact-badge bull">利多原油</span><span>供给收缩预期强烈</span></div>
<div class="event-contracts"><span class="mini-contract">SC</span><span class="mini-contract">LU</span></div>
</div>
<div class="event-card bullish">
<div class="event-header"><span class="event-tag policy">🏛️ 政策</span><span class="event-timer">已公布</span></div>
<div class="event-title">央行宣布降准 0.25%,释放长期资金</div>
<div class="event-impact"><span class="impact-badge bear">利多有色/股指</span><span>流动性改善</span></div>
<div class="event-contracts"><span class="mini-contract">IF</span><span class="mini-contract">CU</span></div>
</div>
</div>
<div class="overview-grid">
<div class="overview-left">
<div class="section-header"><span>🔥 市场热力图</span><span style="color:var(--text-tertiary);font-size:11px;">按涨跌幅</span></div>
<div class="heatmap-grid">
<div class="heat-cell heat-up-3" style="grid-column:span 2;"><div style="font-weight:700;">SC 原油</div><div>+3.8%</div></div>
<div class="heat-cell heat-up-1"><div>AG 沪银</div><div>+2.8%</div></div>
<div class="heat-cell heat-down-2"><div>SA 纯碱</div><div>-3.1%</div></div>
<div class="heat-cell heat-down-1"><div>RB 螺纹</div><div>-0.5%</div></div>
<div class="heat-cell heat-flat"><div>M 豆粕</div><div>0.0%</div></div>
<div class="heat-cell heat-up-2"><div>IF 股指</div><div>+1.8%</div></div>
<div class="heat-cell heat-down-1"><div>FG 玻璃</div><div>-1.9%</div></div>
</div>
<div class="limit-list">
<div><div style="font-size:10px;color:var(--text-tertiary);margin-bottom:4px;">🔴 领涨</div><div class="limit-row"><span>SC 原油</span><span style="color:var(--color-up)">+3.8%</span></div><div class="limit-row"><span>AG 沪银</span><span style="color:var(--color-up)">+2.8%</span></div></div>
<div><div style="font-size:10px;color:var(--text-tertiary);margin-bottom:4px;">🟢 领跌</div><div class="limit-row"><span>SA 纯碱</span><span style="color:var(--color-down)">-3.1%</span></div><div class="limit-row"><span>FG 玻璃</span><span style="color:var(--color-down)">-1.9%</span></div></div>
</div>
</div>
<div class="side-panel">
<div class="panel-box">
<div class="panel-box-title">💰 主力资金流向</div>
<div class="flow-row"><div class="flow-code">SC</div><div class="flow-bar-bg"><div class="flow-bar" style="width:80%;background:var(--color-up);"></div></div><div class="flow-val" style="color:var(--color-up);">+5.2亿</div></div>
<div class="flow-row"><div class="flow-code">SA</div><div class="flow-bar-bg"><div class="flow-bar" style="width:70%;background:var(--color-down);"></div></div><div class="flow-val" style="color:var(--color-down);">-4.5亿</div></div>
</div>
<div class="panel-box" style="flex:1;">
<div class="panel-box-title">📅 宏观日历</div>
<div class="calendar-item"><div class="cal-time">23:30</div><div class="cal-content"><div class="cal-event">EIA 原油库存</div><div class="cal-stars">★★★</div></div></div>
<div class="calendar-item"><div class="cal-time">周五</div><div class="cal-content"><div class="cal-event">非农数据</div><div class="cal-stars">★★★★</div></div></div>
</div>
</div>
</div>
<div class="status-bar"><div class="status-left"><div class="status-item">📡 概览数据</div></div><div class="status-right"><span>刷新 ON</span></div></div>
</main>
<!-- 风险预警页 -->
<main class="content-area" id="alerts-view">
<div class="alerts-layout">
<div class="alerts-left">
<div class="rule-header"><span>⚙️ 预警规则</span><button class="btn btn-primary" style="padding:2px 8px;font-size:11px;">+ 新建</button></div>
<input class="search-box" style="width:100%;margin-bottom:8px;max-width:none;" placeholder="搜索规则...">
<div class="rule-category">价格与指标</div>
<div class="rule-item" onclick="toggleSwitch(this)"><span>价格突破 60 日均线</span><div class="rule-switch on"></div></div>
<div class="rule-item" onclick="toggleSwitch(this)"><span>MACD 金叉/死叉</span><div class="rule-switch on"></div></div>
<div class="rule-category">量能与事件</div>
<div class="rule-item" onclick="toggleSwitch(this)"><span>成交量突增 > 3 倍</span><div class="rule-switch on"></div></div>
<div class="rule-item" onclick="toggleSwitch(this)"><span>热点事件关联异动</span><div class="rule-switch on"></div></div>
</div>
<div class="alerts-center">
<div class="alert-filters"><div class="pill active">全部</div><div class="pill" style="border-color:var(--color-up);color:var(--color-up);">紧急 (3)</div><div class="pill">事件驱动 (2)</div></div>
<div class="timeline">
<div class="alert-card urgent event">
<div class="alert-time">14:32 · 关联事件EIA 库存</div>
<div class="alert-title">🔥 [热点] 燃油快速突破 60 日均线</div>
<div class="alert-desc">关联事件EIA 库存超预期下降 | 5 分钟涨幅 1.5%</div>
<div class="alert-actions"><span class="alert-action">查看详情</span><span class="alert-action">AI 分析</span><span class="alert-action">加入自选</span></div>
</div>
<div class="alert-card urgent">
<div class="alert-time">13:15</div>
<div class="alert-title">🔴 沪银成交量异常放大</div>
<div class="alert-desc">当前成交量为过去 20 日均值的 4.2 倍 | 疑似主力入场</div>
<div class="alert-actions"><span class="alert-action">查看详情</span><span class="alert-action">AI 分析</span></div>
</div>
<div class="alert-card normal">
<div class="alert-time">11:20</div>
<div class="alert-title">螺纹 MACD 出现金叉</div>
<div class="alert-desc">15 分钟级别 MACD 快线上穿慢线</div>
<div class="alert-actions"><span class="alert-action">查看详情</span><span class="alert-action">AI 分析</span></div>
</div>
<div class="alert-card normal">
<div class="alert-time">09:30</div>
<div class="alert-title">开盘跳空 5 品种</div>
<div class="alert-desc">原油、沪银、铁矿等高开超 1%</div>
<div class="alert-actions"><span class="alert-action">查看详情</span></div>
</div>
</div>
</div>
<div class="alerts-right">
<div class="panel-box-title" style="font-size:12px;font-weight:600;margin-bottom:12px;">🤖 AI 异动侦测</div>
<div class="ai-scan-card">
<div class="ai-scan-title">✦ 今日风险指数: 72/100</div>
<div class="ai-scan-item">
<div style="font-weight:600;margin-bottom:2px;">疑似洗盘</div>
<div style="color:var(--text-secondary);font-size:10px;">纯碱 10:45 急跌后迅速拉回,量能异常,疑似主力洗盘。</div>
</div>
<div class="ai-scan-item">
<div style="font-weight:600;margin-bottom:2px;">流动性枯竭</div>
<div style="color:var(--text-secondary);font-size:10px;">胶板 2610 连续 3 分钟无成交,注意滑点风险。</div>
</div>
</div>
<div class="panel-box">
<div class="panel-box-title">📊 预警统计</div>
<div style="display:flex;justify-content:space-between;margin-bottom:4px;font-size:11px;"><span>今日触发</span><span style="font-weight:600;">24 次</span></div>
<div style="display:flex;justify-content:space-between;margin-bottom:4px;font-size:11px;"><span>误报率</span><span style="font-weight:600;color:var(--color-down);">12%</span></div>
</div>
</div>
</div>
<div class="status-bar"><div class="status-left"><div class="status-item">📡 预警服务</div></div><div class="status-right"><span>推送 ON</span></div></div>
</main>
</div>
</div>
<!-- 弹窗 -->
<div class="modal-overlay" id="aiModal" onclick="if(event.target===this)hideModal()">
<div class="modal-dialog">
<div class="modal-header"><div class="modal-title"><div class="ai-dot"></div>AI 分析报告</div><button class="modal-close" onclick="hideModal()"></button></div>
<div class="modal-body">
<div class="ai-summary"><div class="label">✦ AI 研判</div><div class="content">SC2606 当前处于 15 分钟级别空头趋势MACD 死叉向下。持仓量持续减少。建议观望。</div></div>
<div class="ai-section">
<div class="ai-metric-row"><div class="ai-metric-card"><div class="ai-metric-val" style="color:var(--color-up);">38</div><div class="ai-metric-label">综合评分</div></div><div class="ai-metric-card"><div class="ai-metric-val" style="color:var(--color-up);">偏空</div><div class="ai-metric-label">方向</div></div></div>
</div>
<div class="ai-section"><div class="ai-section-title">关键价位</div>
<div class="ai-price-list"><div class="ai-price-item"><span>止损位</span><span style="color:var(--color-up)">656.0</span></div><div class="ai-price-item"><span>目标位</span><span style="color:var(--color-down)">628.0</span></div></div>
</div>
<div class="ai-section"><div class="ai-risk"><span>⚠️</span><div class="ai-risk-text">EIA 数据即将公布,建议控制仓位,避免重仓隔夜。</div></div></div>
</div>
<div class="modal-footer"><button class="btn" onclick="hideModal()">关闭</button><button class="btn btn-primary" onclick="hideModal()">确认</button></div>
</div>
</div>
</body>
</html>

@ -0,0 +1,534 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
期货结算单数据处理脚本
读取期货结算单Excel提取期货和期权交易数据计算盈亏和持仓合并输出
"""
import os
import glob
import re
from datetime import datetime
from collections import defaultdict
import xlrd
import pandas as pd
def find_header(sheet, keywords):
"""
动态识别表头行
:param sheet: xlrd sheet对象
:param keywords: 关键词列表["合约", "品种合约"]
:return: 表头行索引
"""
for row_idx in range(min(20, sheet.nrows)): # 只扫描前20行
row_values = [str(sheet.cell_value(row_idx, col_idx)).strip()
for col_idx in range(sheet.ncols)]
row_text = ' '.join(row_values)
# 检查是否包含关键词
has_keyword = any(kw in row_text for kw in keywords)
# 检查是否包含辅助关键词
has_aux = any(aux in row_text for aux in ["买/卖", "成交价", "权利金"])
if has_keyword and has_aux:
return row_idx
return None
def read_sheet(xls_file, sheet_name, trade_type):
"""
读取单个Sheet的数据
:param xls_file: Excel文件路径
:param sheet_name: Sheet名称
:param trade_type: 交易类型期货/期权
:return: DataFrame
"""
try:
wb = xlrd.open_workbook(xls_file)
if sheet_name not in wb.sheet_names():
print(f" 警告: {xls_file} 中未找到 {sheet_name}")
return pd.DataFrame()
sheet = wb.sheet_by_name(sheet_name)
if sheet.nrows == 0:
return pd.DataFrame()
# 根据类型确定关键词
if trade_type == '期货':
keywords = ["合约"]
else: # 期权
keywords = ["品种合约"]
# 查找表头行
header_row = find_header(sheet, keywords)
if header_row is None:
print(f" 警告: {sheet_name} 中未找到表头")
return pd.DataFrame()
# 读取表头
headers = [str(sheet.cell_value(header_row, col_idx)).strip()
for col_idx in range(sheet.ncols)]
# 读取数据行
data = []
for row_idx in range(header_row + 1, sheet.nrows):
row_data = {}
first_cell = str(sheet.cell_value(row_idx, 0)).strip()
# 跳过合计行
if first_cell == '合计' or first_cell == '':
continue
for col_idx, header in enumerate(headers):
if col_idx < sheet.ncols:
value = sheet.cell_value(row_idx, col_idx)
# 处理"--"
if str(value).strip() == '--':
value = None
row_data[header] = value
# 添加类型标签
row_data['类型'] = trade_type
data.append(row_data)
df = pd.DataFrame(data)
print(f" 读取 {sheet_name}: {len(df)} 条记录")
return df
except Exception as e:
print(f" 错误: 读取 {sheet_name} 失败 - {e}")
return pd.DataFrame()
def extract_variety(contract):
"""
提取品种代码所有前导字母
:param contract: 合约代码
:return: 品种代码
"""
if pd.isna(contract) or not contract:
return ''
contract = str(contract).strip()
match = re.match(r'^([A-Za-z]+)', contract)
if match:
return match.group(1).upper()
return contract[:2].upper()
def safe_float(value, default=0.0):
"""
安全转换为float
"""
if pd.isna(value) or value is None or value == '':
return default
try:
return float(value)
except (ValueError, TypeError):
return default
def calc_pnl_futures(df):
"""
计算期货盈亏
"""
if df.empty:
return {'平仓盈亏': 0, '手续费': 0, '净盈亏': 0}
# 平仓盈亏
pnl_close = 0
if '平仓盈亏' in df.columns:
for val in df['平仓盈亏']:
if not pd.isna(val):
pnl_close += safe_float(val)
# 手续费
commission = 0
if '手续费' in df.columns:
for val in df['手续费']:
commission += safe_float(val)
net_pnl = pnl_close - commission
return {
'平仓盈亏': pnl_close,
'手续费': commission,
'净盈亏': net_pnl
}
def calc_pnl_options(df):
"""
计算期权盈亏
"""
if df.empty:
return {'权利金净收入': 0, '手续费': 0, '净盈亏': 0}
# 权利金(带方向)
premium_total = 0
if '权利金' in df.columns:
for val in df['权利金']:
premium_total += safe_float(val)
# 手续费
commission = 0
if '手续费' in df.columns:
for val in df['手续费']:
commission += safe_float(val)
net_pnl = premium_total - commission
return {
'权利金净收入': premium_total,
'手续费': commission,
'净盈亏': net_pnl
}
def calc_position_futures(df):
"""
计算期货持仓
"""
positions = defaultdict(float)
if df.empty:
return positions
for _, row in df.iterrows():
contract = str(row.get('合约', '')).strip()
if not contract or contract == '合计':
continue
bs_flag = str(row.get('买/卖', '')).strip()
oc_flag = str(row.get('开/平', '')).strip()
volume = safe_float(row.get('手数', 0))
# 买+开 → +手数;卖+开 → -手数;买+平 → -手数;卖+平 → +手数
if '' in bs_flag and '' in oc_flag:
positions[contract] += volume
elif '' in bs_flag and '' in oc_flag:
positions[contract] -= volume
elif '' in bs_flag and '' in oc_flag:
positions[contract] -= volume
elif '' in bs_flag and '' in oc_flag:
positions[contract] += volume
# 过滤0持仓
return {k: v for k, v in positions.items() if abs(v) > 0.001}
def calc_position_options(df):
"""
计算期权持仓
"""
positions = defaultdict(float)
if df.empty:
return positions
for _, row in df.iterrows():
contract = str(row.get('品种合约', '')).strip()
if not contract or contract == '合计':
continue
bs_flag = str(row.get('买/卖', '')).strip()
volume = safe_float(row.get('成交量', 0))
if '' in bs_flag:
positions[contract] += volume
elif '' in bs_flag:
positions[contract] -= volume
# 过滤0持仓
return {k: v for k, v in positions.items() if abs(v) > 0.001}
def merge_trades(df_futures, df_options):
"""
合并期货和期权交易记录
"""
# 统一字段
columns_map_futures = {
'合约': '合约代码',
'买/卖': '买卖',
'成交价': '价格/权利金单价',
'手数': '手数/成交量',
'成交额': '金额/权利金',
'实际成交日期': '实际成交日期',
'成交时间': '成交时间'
}
columns_map_options = {
'品种合约': '合约代码',
'买/卖': '买卖',
'权利金单价': '价格/权利金单价',
'成交量': '手数/成交量',
'权利金': '金额/权利金',
'成交日期': '实际成交日期',
'成交时间': '成交时间'
}
# 处理期货数据
if not df_futures.empty:
df_f = df_futures.rename(columns=columns_map_futures).copy()
df_f['品种'] = df_f['合约代码'].apply(extract_variety)
df_f['方向'] = df_f['买卖'].apply(lambda x: '' if '' in str(x) else '')
df_f['开平仓'] = df_f.get('开/平', '')
# 确保必要字段存在
for col in ['平仓盈亏', '手续费']:
if col not in df_f.columns:
df_f[col] = 0
else:
df_f = pd.DataFrame()
# 处理期权数据
if not df_options.empty:
df_o = df_options.rename(columns=columns_map_options).copy()
df_o['品种'] = df_o['合约代码'].apply(extract_variety)
df_o['方向'] = df_o['买卖'].apply(lambda x: '' if '' in str(x) else '')
df_o['开平仓'] = ''
df_o['平仓盈亏'] = 0
# 确保必要字段存在
for col in ['手续费']:
if col not in df_o.columns:
df_o[col] = 0
else:
df_o = pd.DataFrame()
# 合并
common_cols = ['品种', '合约代码', '类型', '买卖', '方向', '成交时间',
'实际成交日期', '价格/权利金单价', '手数/成交量', '金额/权利金',
'手续费', '平仓盈亏', '开平仓']
if not df_f.empty:
df_f = df_f[[c for c in common_cols if c in df_f.columns]]
if not df_o.empty:
df_o = df_o[[c for c in common_cols if c in df_o.columns]]
if not df_f.empty and not df_o.empty:
return pd.concat([df_f, df_o], ignore_index=True)
elif not df_f.empty:
return df_f
elif not df_o.empty:
return df_o
else:
return pd.DataFrame()
def save_output(trades_df, df_futures, df_options, output_dir):
"""
保存输出文件
"""
os.makedirs(output_dir, exist_ok=True)
# 文件1trades_merged.csv
csv_path = os.path.join(output_dir, 'trades_merged.csv')
if not trades_df.empty:
trades_df.to_csv(csv_path, index=False, encoding='utf-8-sig')
print(f"已保存交易明细: {csv_path}")
else:
print("警告: 无交易数据")
# 计算分析数据
analysis_data = {}
# Sheet1: 品种盈亏汇总
variety_summary = []
# 期货品种汇总
if not df_futures.empty:
df_f = df_futures.copy()
df_f['品种'] = df_f['合约'].apply(extract_variety)
for variety, group in df_f.groupby('品种'):
pnl = calc_pnl_futures(group)
total_volume = sum(safe_float(v) for v in group.get('手数', []))
total_amount = sum(safe_float(v) for v in group.get('成交额', []))
buy_count = sum(1 for v in group.get('买/卖', []) if '' in str(v))
sell_count = sum(1 for v in group.get('买/卖', []) if '' in str(v))
variety_summary.append({
'品种': variety,
'类型': '期货',
'总手数': total_volume,
'总成交额': total_amount,
'总手续费': pnl['手续费'],
'平仓盈亏': pnl['平仓盈亏'],
'净盈亏': pnl['净盈亏'],
'买入次数': buy_count,
'卖出次数': sell_count
})
# 期权品种汇总
if not df_options.empty:
df_o = df_options.copy()
df_o['品种'] = df_o['品种合约'].apply(extract_variety)
for variety, group in df_o.groupby('品种'):
pnl = calc_pnl_options(group)
total_volume = sum(safe_float(v) for v in group.get('成交量', []))
total_premium = sum(abs(safe_float(v)) for v in group.get('权利金', []))
buy_count = sum(1 for v in group.get('买/卖', []) if '' in str(v))
sell_count = sum(1 for v in group.get('买/卖', []) if '' in str(v))
variety_summary.append({
'品种': variety,
'类型': '期权',
'总手数': total_volume,
'总成交额/权利金': total_premium,
'总手续费': pnl['手续费'],
'平仓盈亏': 0,
'净盈亏': pnl['净盈亏'],
'买入次数': buy_count,
'卖出次数': sell_count
})
df_variety = pd.DataFrame(variety_summary)
# Sheet2: 期货持仓
futures_positions = calc_position_futures(df_futures)
futures_pos_data = []
for contract, pos in futures_positions.items():
futures_pos_data.append({
'合约': contract,
'净持仓': pos,
'方向': '' if pos > 0 else ''
})
df_futures_pos = pd.DataFrame(futures_pos_data)
# Sheet3: 期权持仓
options_positions = calc_position_options(df_options)
options_pos_data = []
for contract, pos in options_positions.items():
options_pos_data.append({
'品种合约': contract,
'净持仓': pos,
'方向': '权利' if pos > 0 else '义务'
})
df_options_pos = pd.DataFrame(options_pos_data)
# 文件2analysis.xlsx
xlsx_path = os.path.join(output_dir, 'analysis.xlsx')
with pd.ExcelWriter(xlsx_path, engine='openpyxl') as writer:
if not df_variety.empty:
df_variety.to_excel(writer, sheet_name='品种盈亏汇总', index=False)
if not df_futures_pos.empty:
df_futures_pos.to_excel(writer, sheet_name='期货持仓', index=False)
if not df_options_pos.empty:
df_options_pos.to_excel(writer, sheet_name='期权持仓', index=False)
print(f"已保存分析结果: {xlsx_path}")
return {
'variety_summary': variety_summary,
'futures_positions': futures_positions,
'options_positions': options_positions
}
def print_summary(df_futures, df_options, results):
"""
打印摘要
"""
print("\n" + "="*50)
print("=== 交易摘要 ===")
# 期货统计
if not df_futures.empty:
pnl_f = calc_pnl_futures(df_futures)
varieties_f = df_futures['合约'].apply(extract_variety).nunique()
print(f"期货:{len(df_futures)}笔交易,{varieties_f}个品种")
print(f" 总盈亏:{pnl_f['净盈亏']:.2f}元(含手续费{pnl_f['手续费']:.2f}元)")
else:
print("期货:无交易")
# 期权统计
if not df_options.empty:
pnl_o = calc_pnl_options(df_options)
varieties_o = df_options['品种合约'].apply(extract_variety).nunique()
print(f"期权:{len(df_options)}笔交易,{varieties_o}个品种")
print(f" 总盈亏:{pnl_o['净盈亏']:.2f}元(含手续费{pnl_o['手续费']:.2f}元)")
else:
print("期权:无交易")
# 持仓统计
print("\n=== 收盘持仓 ===")
futures_pos = results['futures_positions']
if futures_pos:
for contract, pos in sorted(futures_pos.items()):
direction = '' if pos > 0 else ''
print(f"期货:{contract} {direction} {abs(pos):.0f}")
else:
print("期货:无持仓")
options_pos = results['options_positions']
if options_pos:
for contract, pos in sorted(options_pos.items()):
direction = '权利' if pos > 0 else '义务'
print(f"期权:{contract} {direction} {abs(pos):.0f}")
else:
print("期权:无持仓")
print("="*50 + "\n")
def main():
"""
主函数
"""
# 数据目录
data_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data')
output_dir = os.path.dirname(os.path.abspath(__file__))
print(f"数据目录: {data_dir}")
print(f"输出目录: {output_dir}\n")
# 查找所有xls文件
xls_files = glob.glob(os.path.join(data_dir, '*.xls'))
if not xls_files:
print(f"错误: 在 {data_dir} 中未找到 .xls 文件")
return
print(f"找到 {len(xls_files)} 个结算单文件\n")
# 读取所有文件
all_futures = []
all_options = []
for xls_file in sorted(xls_files):
filename = os.path.basename(xls_file)
print(f"处理: {filename}")
# 读取期货成交明细
df_f = read_sheet(xls_file, '成交明细', '期货')
if not df_f.empty:
all_futures.append(df_f)
# 读取期权成交明细
df_o = read_sheet(xls_file, '期权成交明细', '期权')
if not df_o.empty:
all_options.append(df_o)
# 合并数据
df_futures = pd.concat(all_futures, ignore_index=True) if all_futures else pd.DataFrame()
df_options = pd.concat(all_options, ignore_index=True) if all_options else pd.DataFrame()
print(f"\n汇总: 期货 {len(df_futures)} 条, 期权 {len(df_options)}\n")
# 合并交易记录
trades_merged = merge_trades(df_futures, df_options)
# 保存输出
results = save_output(trades_merged, df_futures, df_options, output_dir)
# 打印摘要
print_summary(df_futures, df_options, results)
if __name__ == '__main__':
main()

File diff suppressed because it is too large Load Diff

@ -0,0 +1,156 @@
# 每日收盘交易计划 - 执行逻辑说明 (v2)
> **任务名称**:每日收盘交易计划 (16:01)
> **任务 ID**`3b960afd-45b3-489a-9029-fdbadbdc4f64`
> **调度时间**`1 8 * * mon-fri`UTC 08:01 = 北京时间 16:01
> **超时限制**1200 秒20 分钟)
> **分发渠道**console + 收件箱
> **状态**:✅ 已启用
---
## 📋 完整执行流程
### 第一步:数据收集
```
数据源share_data/symbols_config.json38个期货品种
采集工具batch_get_market_data (Market Data Collector MCP)
```
| 周期 | 用途 |
|------|------|
| 日线 (daily) | 计算当日振幅、涨跌幅、近20日均振幅 |
| 60分钟 (60min) | 趋势判断MA排列 + MACD |
| 15分钟 (15min) | 趋势判断 + 入场时机 |
| 5分钟 (5min) | 活跃度评估 |
**参数**`include_indicators=True`(包含 MA、MACD 指标)
---
### 第二步:多维度打分排名(全品种扫描)
对每个品种计算 **5 个维度** 并排名:
| 维度 | 权重 | 计算方法 | 排名依据 | 意义 |
|------|------|---------|---------|------|
| **A. 当日振幅** | 20% | (今日最高 - 今日最低) / 今日收盘 × 100% | 从大到小 | 振幅越大,日内操作空间越大 |
| **B. 成交量** | 15% | 今日成交量 / 近5日均量量比 | 从大到小 | 放量品种更值得关注 |
| **C. 涨跌幅** | 15% | (今日收盘 - 昨日收盘) / 昨日收盘 × 100% | 绝对值从大到小 | 大幅波动可能有趋势延续或反转机会 |
| **D. 多周期趋势共振** | 35% | 60m/15m/5m 的 MA排列 + MACD状态 → 三周期共振 | 趋势分 0-100 | 趋势越明确,操作胜率越高 |
| **E. 活跃度** | 15% | 5min K线近10根的量活跃度 + 价格变动率 | 综合评分 0-100 | 活跃品种流动性好、滑点小 |
#### 趋势分计算细则维度D
| 周期 | MA排列方向 | MACD状态 | 单周期趋势分 |
|------|-----------|---------|-------------|
| 60min | 多头排列 +50 / 空头排列 -50 | DIF>DEA +30 / DIF<DEA -30 | -50 ~ +50 |
| 15min | 同上 | 同上 | -50 ~ +50 |
| 5min | 同上 | 同上 | -50 ~ +50 |
**三周期共振判定**
| 条件 | 方向标签 | 趋势分映射 |
|------|---------|-----------|
| 60m/15m/5m 趋势分均 > 20 | 多头共振 | 80-100 |
| 60m/15m/5m 趋势分均 < -20 | | 80-100 |
| 60m>0 且 15m>0 | 偏多震荡 | 50-70 |
| 60m<0 15m<0 | | 50-70 |
| 其他 | 多空交织 | 0-40 |
---
### 第三步:综合评分与筛选
#### 综合评分公式
```
composite = 振幅得分×0.20 + 成交量得分×0.15 + 涨跌幅得分×0.15 + 趋势得分×0.35 + 活跃度×0.15
```
#### 筛选规则
| 标签 | 条件 | 处理 |
|------|------|------|
| 🟢 **交易机会** | 综合≥55 且 趋势明确 | 纳入交易计划 |
| 👀 **重点关注** | 综合45-55 或 形态构筑中 | 列入观察清单 |
| 🔴 **规避** | 综合<45 多空交织无方向 | 不建议参与 |
---
### 第四步:制定具体交易计划
针对 🟢 交易机会品种Top 3-5逐一出具
```
每个品种包含:
├── 品种/合约 & 方向(多/空)
├── 入场区间(具体支撑/阻力位)
├── 止损位(必须明确,证伪点)
├── 目标位(第一目标 / 第二目标)
├── 触发条件突破XX进场、回踩XX不破进场
└── 综合评分及各维度得分明细
```
---
### 第五步:交付
| 交付方式 | 内容 |
|---------|------|
| **文件保存** | `reports/daily_plan/YYYY-MM-DD_交易计划.md` |
| **文件发送** | 通过 `send_file_to_user` 发送完整报告 |
| **聊天窗口** | 精简摘要:今日一句话总结 + Top3 核心机会 + 最大风险提示 |
| **收件箱** | 自动保存一份(`save_result_to_inbox: true` |
#### 完整报告结构
1. **全品种多维度排名表**(振幅/成交量/涨跌幅/趋势/综合评分)
2. **🟢 交易机会品种详细计划**
3. **👀 重点关注品种清单**
4. **🔴 规避品种及原因**
5. **板块热度分析**(按黑色系/贵金属/有色/能源化工/农产品/建材分组)
6. **风险提示**
---
## 🔧 技术配置
| 配置项 | 值 |
|--------|-----|
| 任务类型 | agent |
| 会话 ID | 1779157619683 |
| 用户 ID | default |
| 分发模式 | final仅最终结果 |
| 最大并发 | 1 |
| 超时 | 1200 秒 |
| 错过宽限 | 60 秒 |
| 共享会话 | ✅ |
| 收件箱 | ✅ |
---
## ⏰ 调度说明
| 项目 | 值 |
|------|-----|
| Cron 表达式 | `1 8 * * mon-fri` |
| UTC 时间 | 每周一至周五 08:01 |
| **北京时间** | **每周一至周五 16:01**收盘后1分钟 |
---
## 📊 v1 → v2 变更摘要
| 变更项 | v1旧版 | v2新版 |
|--------|-----------|-----------|
| 数据收集 | 优先本地缓存缺失才调MCP | **直接调用 batch_get_market_data 获取全品种数据** |
| 分析范围 | 仅有机会品种 | **全品种扫描 + 多维度打分排名** |
| 筛选维度 | 多周期共振 + 量价 | **新增:当日振幅排名、成交量排名、涨跌幅排名** |
| 评分体系 | 无量化评分 | **5维度加权综合评分振幅20%+量15%+涨跌幅15%+趋势35%+活跃度15%** |
| 输出格式 | Top3-5计划 | **全品种排名表 + 分级清单(🟢👀🔴) + Top3-5详细计划 + 板块热度** |
---
*文档生成时间2026-06-14*

@ -0,0 +1,25 @@
@echo off
chcp 65001 >nul
echo ========================================
echo 查看期货智析缓冲平台日志
echo ========================================
echo.
echo 请选择日志查看模式:
echo 1. 查看最近100行日志
echo 2. 实时跟踪日志
echo 3. 查看最近1小时日志
echo.
set /p MODE=请输入选项 (1/2/3):
if "%MODE%"=="1" (
docker-compose logs --tail=100
) else if "%MODE%"=="2" (
docker-compose logs -f
) else if "%MODE%"=="3" (
docker-compose logs --since 1h
) else (
echo 无效选项
)
pause
Loading…
Cancel
Save