Compare commits

..

3 Commits

@ -30,7 +30,7 @@ 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 格式的结算单文件")
@ -43,129 +43,16 @@ async def import_settlement(
result = save_to_db(db, df_futures, df_options, file.filename)
total_skipped = result['futures_skipped'] + result['options_skipped']
total_new = result['futures_count'] + result['options_count']
total_parsed = result['futures_parsed'] + result['options_parsed']
if total_new == 0 and total_skipped > 0:
return {
"success": True,
"message": f"全部跳过:共解析 {total_parsed} 条记录,均已存在(去重规则:交易日+品种+时间+价格),未导入任何新数据",
"data": result,
}
msg_parts = [f"导入成功:期货 {result['futures_count']} 条,期权 {result['options_count']}"]
if total_skipped > 0:
msg_parts.append(f"跳过重复 {total_skipped}")
msg_parts.append(f"交易日期: {result['trade_dates']}")
return {
"success": True,
"message": "".join(msg_parts),
"message": f"导入成功:期货 {result['futures_count']} 条,期权 {result['options_count']}",
"data": result,
}
except HTTPException:
raise
except Exception as e:
logger.exception("导入结算单失败")
raise HTTPException(status_code=500, detail=f"导入失败: {str(e)}")
@router.post("/batch-import")
async def batch_import_settlement(
files: list[UploadFile] = File(...),
db: Session = Depends(get_analysis_db),
):
"""批量上传并解析期货结算单文件,导入交易记录(含逐条去重校验)"""
if not files:
raise HTTPException(status_code=400, detail="请选择至少一个结算单文件")
results = []
total_futures = 0
total_options = 0
total_skipped_all = 0
processed_count = 0
failed_count = 0
for file in files:
if not file.filename.endswith(('.xls', '.xlsx')):
results.append({
"filename": file.filename,
"success": False,
"message": "文件格式不支持,仅支持 .xls/.xlsx",
})
failed_count += 1
continue
try:
content = await file.read()
df_futures, df_options = parse_settlement_file(content, file.filename)
if df_futures.empty and df_options.empty:
results.append({
"filename": file.filename,
"success": False,
"message": "文件中未找到有效的交易记录",
})
failed_count += 1
continue
result = save_to_db(db, df_futures, df_options, file.filename)
file_new = result['futures_count'] + result['options_count']
file_skipped = result['futures_skipped'] + result['options_skipped']
total_futures += result['futures_count']
total_options += result['options_count']
total_skipped_all += file_skipped
processed_count += 1
msg_parts = []
if file_new > 0:
msg_parts.append(f"新导入 {file_new} 条(期货 {result['futures_count']},期权 {result['options_count']}")
if file_skipped > 0:
msg_parts.append(f"跳过重复 {file_skipped}")
if not msg_parts:
msg_parts.append("无有效记录")
results.append({
"filename": file.filename,
"success": True,
"message": "".join(msg_parts),
"new_count": file_new,
"skipped_count": file_skipped,
"trade_dates": result['trade_dates'],
"batch_id": result['batch_id'],
})
except Exception as e:
logger.exception(f"批量导入文件失败: {file.filename}")
results.append({
"filename": file.filename,
"success": False,
"message": f"导入失败: {str(e)}",
})
failed_count += 1
summary = (
f"批量导入完成:共 {len(files)} 个文件,"
f"处理 {processed_count} 个,失败 {failed_count} 个,"
f"合计新导入期货 {total_futures} 条、期权 {total_options} 条,跳过重复 {total_skipped_all}"
)
return {
"success": True,
"message": summary,
"data": {
"total_files": len(files),
"processed_count": processed_count,
"failed_count": failed_count,
"total_futures": total_futures,
"total_options": total_options,
"total_skipped": total_skipped_all,
"details": results,
},
}
# ==================== 交易记录查询 ====================
@router.get("/records")
@ -425,21 +312,6 @@ def get_kline_with_trades(
return {"success": False, "message": f"未找到 {kline_symbol}{period} K线数据"}
candles = json.loads(market_data.candles_json)
# 将字典格式K线转换为前端期望的数组格式 [date, open, close, low, high, volume]
if candles and isinstance(candles[0], dict):
# 根据周期决定是否保留时间部分
is_daily = period == 'daily'
candles = [
[
c.get("time", "")[:10] if is_daily else c.get("time", ""), # 日线只保留日期,分钟线保留完整时间
c.get("open", 0),
c.get("close", 0),
c.get("low", 0),
c.get("high", 0),
c.get("volume", 0),
]
for c in candles
]
kline_symbol = market_data.symbol
logger.info(f"[K线查询] 成功获取K线数据: symbol={kline_symbol}, candles_count={len(candles)}")
finally:
@ -519,6 +391,8 @@ async def analyze_trade(
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
@ -536,13 +410,6 @@ async def analyze_trade(
).first()
if market_data and market_data.candles_json:
candles = json.loads(market_data.candles_json)
# 将字典格式K线转换为数组格式 [date, open, close, low, high, volume]
if candles and isinstance(candles[0], dict):
candles = [
[c.get("time", "")[:10], c.get("open", 0), c.get("close", 0),
c.get("low", 0), c.get("high", 0), c.get("volume", 0)]
for c in candles
]
kline_context[period] = candles[-50:] if len(candles) > 50 else candles
finally:
main_db.close()
@ -578,14 +445,13 @@ async def analyze_trade(
"""
try:
# 使用 AIFuturesAnalyzer 的模型配置和调用能力
from app.services.ai_analysis import AIFuturesAnalyzer
analyzer = AIFuturesAnalyzer(db)
model = analyzer.get_active_model()
# 使用 AIAnalysisService 的模型配置和调用能力
service = AIAnalysisService.__new__(AIAnalysisService)
model = service.get_active_model()
if not model:
return {"success": False, "message": "未配置AI模型或模型未激活请先在AI配置页面设置"}
response = analyzer.call_ai_model(prompt, model)
response = service.call_ai_model(prompt, model)
if not response:
return {"success": False, "message": "AI模型返回空响应请稍后重试"}

@ -136,12 +136,6 @@ def ai_config_page():
return FileResponse(str(STATIC_DIR / "ai_config.html"))
@app.get("/review-plan")
def review_plan_page():
"""复盘计划页面"""
return FileResponse(str(STATIC_DIR / "review_plan.html"))
@app.get("/api/v1/health")
def health():
return {"status": "ok", "service": "market-data-buffer"}

@ -5,7 +5,6 @@
import re
import uuid
import json
import logging
from datetime import datetime
from collections import defaultdict
from pathlib import Path
@ -16,8 +15,6 @@ from sqlalchemy.orm import Session as DBSession
from app.analysis_models import TradeRecord, TradeImportBatch
logger = logging.getLogger(__name__)
# 品种代码 -> 中文名 映射(反向映射:从合约代码前缀查中文名)
_VARIETY_NAME_MAP = {}
@ -196,35 +193,6 @@ def _normalize_time(time_val) -> str:
return s
def _build_dedup_key(trade_date: str, variety: str, trade_time: str, price: float) -> str:
"""构建去重键:交易日+品种+交易时间+价格价格保留2位小数避免浮点精度问题"""
return f"{trade_date}|{variety}|{trade_time}|{round(price, 2)}"
def _load_existing_dedup_keys(db: DBSession, trade_dates: set) -> set:
"""
根据涉及的交易日批量加载已有记录的去重键集合
"""
if not trade_dates:
logger.warning("[去重校验] trade_dates 为空,跳过查询")
return set()
logger.info(f"[去重校验] 查询已有记录,日期范围: {sorted(trade_dates)}")
existing = db.query(
TradeRecord.trade_date,
TradeRecord.variety,
TradeRecord.trade_time,
TradeRecord.price,
).filter(
TradeRecord.trade_date.in_(trade_dates)
).all()
logger.info(f"[去重校验] 从数据库加载到 {len(existing)} 条已有记录")
keys = {_build_dedup_key(r[0], r[1], r[2] or '', r[3] or 0.0) for r in existing}
if keys:
sample = list(keys)[:3]
logger.info(f"[去重校验] 去重键示例: {sample}")
return keys
def save_to_db(
db: DBSession,
df_futures: pd.DataFrame,
@ -232,46 +200,16 @@ def save_to_db(
filename: str,
) -> dict:
"""
将解析后的交易记录保存到数据库含逐条去重校验
去重规则同一交易日 + 同一品种 + 同一交易时间 + 同一价格 => 视为重复跳过
:return: {
"batch_id": str, "futures_count": int, "options_count": int, "trade_dates": str,
"futures_skipped": int, "options_skipped": int, "total_parsed": int
}
将解析后的交易记录保存到数据库
: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
futures_skipped = 0
options_skipped = 0
futures_parsed = 0
options_parsed = 0
# 第一遍:收集所有涉及的交易日,用于批量查询已有记录
if not df_futures.empty:
for _, row in df_futures.iterrows():
contract = str(row.get('合约', '')).strip()
if not contract or contract == '合计':
continue
trade_date = _normalize_date(row.get('实际成交日期', ''))
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
trade_date = _normalize_date(row.get('成交日期', ''))
if trade_date:
all_dates.add(trade_date)
# 批量加载已有记录的去重键
existing_keys = _load_existing_dedup_keys(db, all_dates)
records = []
new_keys_in_batch = set() # 防止本次导入文件内部重复
# 处理期货记录
if not df_futures.empty:
@ -279,24 +217,12 @@ def save_to_db(
contract = str(row.get('合约', '')).strip()
if not contract or contract == '合计':
continue
futures_parsed += 1
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()
price = safe_float(row.get('成交价'))
dedup_key = _build_dedup_key(trade_date, variety, trade_time, price)
if dedup_key in existing_keys or dedup_key in new_keys_in_batch:
futures_skipped += 1
continue
# 记录前几条的去重键用于调试
if futures_parsed <= 3:
logger.info(f"[去重校验] 期货记录 #{futures_parsed}: date={trade_date!r}, variety={variety!r}, time={trade_time!r}, price={price!r} => key={dedup_key!r}")
new_keys_in_batch.add(dedup_key)
rec = TradeRecord(
trade_type='期货',
symbol=contract,
@ -304,7 +230,7 @@ def save_to_db(
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=price,
price=safe_float(row.get('成交价')),
volume=safe_float(row.get('手数')),
amount=safe_float(row.get('成交额')),
close_pnl=safe_float(row.get('平仓盈亏')),
@ -316,6 +242,8 @@ def save_to_db(
)
records.append(rec)
futures_count += 1
if trade_date:
all_dates.add(trade_date)
# 处理期权记录
if not df_options.empty:
@ -323,19 +251,11 @@ def save_to_db(
contract = str(row.get('品种合约', '')).strip()
if not contract or contract == '合计':
continue
options_parsed += 1
variety = extract_variety(contract)
trade_date = _normalize_date(row.get('成交日期', ''))
trade_time = _normalize_time(row.get('成交时间', ''))
bs_flag = str(row.get('买/卖', '')).strip()
price = safe_float(row.get('权利金单价'))
dedup_key = _build_dedup_key(trade_date, variety, trade_time, price)
if dedup_key in existing_keys or dedup_key in new_keys_in_batch:
options_skipped += 1
continue
new_keys_in_batch.add(dedup_key)
rec = TradeRecord(
trade_type='期权',
symbol=contract,
@ -343,7 +263,7 @@ def save_to_db(
symbol_name=_VARIETY_NAME_MAP.get(variety, ''),
direction='' if '' in bs_flag else '',
offset='',
price=price,
price=safe_float(row.get('权利金单价')),
volume=safe_float(row.get('成交量')),
amount=safe_float(row.get('权利金')),
close_pnl=0.0,
@ -355,6 +275,8 @@ def save_to_db(
)
records.append(rec)
options_count += 1
if trade_date:
all_dates.add(trade_date)
if records:
db.add_all(records)
@ -379,22 +301,11 @@ def save_to_db(
db.add(batch)
db.commit()
logger.info(
f"[去重校验] 导入完成: 文件={filename}, "
f"期货 解析={futures_parsed} 新增={futures_count} 跳过={futures_skipped}, "
f"期权 解析={options_parsed} 新增={options_count} 跳过={options_skipped}, "
f"已有去重键数量={len(existing_keys)}"
)
return {
"batch_id": batch_id,
"futures_count": futures_count,
"options_count": options_count,
"trade_dates": trade_dates_str,
"futures_skipped": futures_skipped,
"options_skipped": options_skipped,
"futures_parsed": futures_parsed,
"options_parsed": options_parsed,
}

@ -53,7 +53,7 @@
}
.logo { font-size: 18px; font-weight: 600; letter-spacing: -0.02em; margin-right: 40px; }
.nav-items { display: flex; gap: 24px; flex: 1; justify-content: center; }
.nav-items { display: flex; gap: 24px; flex: 1; }
.nav-item { font-size: 14px; color: var(--text-secondary); cursor: pointer; transition: color 0.2s; text-decoration: none; }
.nav-item:hover { color: var(--text-primary); }
.nav-item.active { color: var(--color-brand); font-weight: 500; }
@ -503,73 +503,82 @@
.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; }
/* 6列统计卡片 */
.tr-stats-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 14px; margin-bottom: 24px; }
.tr-stat-card { background: var(--bg-card); border-radius: var(--radius-card); padding: 18px; text-align: center; box-shadow: var(--shadow-sm); transition: transform 0.2s; }
.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; font-weight: 600; }
.tr-stat-value { font-size: 24px; font-weight: 700; margin-bottom: 4px; }
.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-chart-box { background: var(--bg-card); border-radius: var(--radius-card); padding: 20px; margin-bottom: 24px; box-shadow: var(--shadow-sm); }
.tr-chart-title { font-size: 16px; font-weight: 600; color: var(--text-primary); margin-bottom: 16px; }
.tr-chart-container { width: 100%; height: 350px; }
.tr-bubble-container { width: 100%; height: 400px; }
.tr-sub-page { display: none; }
.tr-sub-page.active { display: block; }
/* 表格 */
.tr-tbl-box { background: var(--bg-card); border-radius: var(--radius-card); padding: 20px; margin-bottom: 24px; box-shadow: var(--shadow-sm); overflow-x: auto; }
.tr-tbl-title { font-size: 16px; font-weight: 600; color: var(--text-primary); margin-bottom: 16px; }
.tr-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.tr-table th { background: #F5F5F7; padding: 12px 8px; text-align: left; font-weight: 600; color: var(--text-tertiary); font-size: 12px; white-space: nowrap; border-bottom: 1px solid rgba(0,0,0,0.06); }
.tr-table td { padding: 12px 8px; border-bottom: 1px solid rgba(0,0,0,0.04); color: var(--text-primary); white-space: nowrap; }
.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-link { color: var(--color-brand); cursor: pointer; text-decoration: none; font-weight: 500; }
.tr-link:hover { text-decoration: underline; }
/* K线周期按钮 */
.tr-kline-tabs { display: flex; gap: 8px; margin-bottom: 16px; align-items: center; }
.tr-kline-tab { padding: 6px 14px; border: 1px solid rgba(0,0,0,0.08); background: #fff; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 500; color: var(--text-secondary); transition: all 0.2s; font-family: inherit; }
.tr-kline-tab:hover { background: var(--color-brand); color: #fff; border-color: var(--color-brand); }
.tr-kline-tab.active { background: var(--color-brand); color: #fff; border-color: var(--color-brand); box-shadow: 0 4px 10px rgba(0,122,255,0.2); }
/* 弹窗通用样式(复用 rv-modal 体系) */
.tr-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); backdrop-filter: blur(8px); z-index: 2000; display: none; align-items: center; justify-content: center; }
.tr-modal-overlay.show { display: flex; }
.tr-modal { background: #fff; border-radius: 20px; width: 90%; max-width: 1000px; max-height: 90vh; overflow-y: auto; box-shadow: 0 25px 50px rgba(0,0,0,0.25); animation: rv-modal-in 0.25s ease; }
.tr-modal-header { display: flex; justify-content: space-between; align-items: center; padding: 20px 24px; border-bottom: 1px solid rgba(0,0,0,0.06); }
.tr-modal-title { font-size: 18px; font-weight: 700; color: var(--text-primary); }
.tr-modal-close { width: 32px; height: 32px; border-radius: 50%; border: none; background: #F5F5F7; font-size: 16px; cursor: pointer; display: flex; align-items: center; justify-content: center; color: var(--text-secondary); transition: all 0.2s; }
.tr-modal-close:hover { background: #E5E5E7; }
.tr-modal-body { padding: 24px; }
/* 弹窗内概览网格 */
.tr-overview-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 24px; }
.tr-overview-item { background: #F5F5F7; border-radius: 12px; padding: 16px; text-align: center; }
.tr-overview-label { font-size: 12px; color: var(--text-tertiary); margin-bottom: 4px; }
.tr-overview-value { font-size: 20px; font-weight: 700; color: var(--text-primary); }
.tr-overview-value.profit { color: var(--color-down); }
.tr-overview-value.loss { color: var(--color-up); }
/* AI诊断框 */
.tr-ai-box { background: linear-gradient(135deg, #f0f7ff 0%, #e6f4ff 100%); border: 1px solid #bae0ff; border-radius: 12px; padding: 20px; margin-bottom: 24px; }
.tr-ai-title { font-size: 16px; font-weight: 600; color: #0958d9; margin-bottom: 12px; display: flex; align-items: center; gap: 8px; }
.tr-ai-content { font-size: 14px; color: var(--text-primary); line-height: 1.8; white-space: pre-wrap; }
.tr-ai-tag { display: inline-block; background: #e6f4ff; color: #0958d9; padding: 2px 8px; border-radius: 4px; font-size: 12px; margin-right: 8px; margin-bottom: 8px; }
/* 品种详情弹窗内K线 */
.tr-kline-chart { width: 100%; height: 400px; }
/* 每日详情 - 品种分区 */
.tr-variety-section { margin-top: 24px; border-top: 1px solid rgba(0,0,0,0.06); padding-top: 20px; }
/* 批次管理 */
.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-detail-info { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; margin-bottom: 20px; padding: 16px; background: #F5F5F7; border-radius: 12px; }
.tr-detail-info-item { display: flex; flex-direction: column; gap: 4px; }
.tr-detail-info-label { font-size: 12px; color: var(--text-tertiary); font-weight: 500; }
.tr-detail-info-value { font-size: 14px; font-weight: 600; color: var(--text-primary); }
.tr-detail-info-value.profit { color: var(--color-down); }
.tr-detail-info-value.loss { color: var(--color-up); }
.tr-detail-section { margin-top: 20px; }
.tr-detail-section-title { font-size: 15px; font-weight: 700; color: var(--text-primary); margin-bottom: 12px; padding-left: 8px; border-left: 3px solid var(--accent-gold); }
.tr-detail-ai-content { padding: 16px; background: #F5F5F7; border-radius: 12px; min-height: 100px; font-size: 14px; line-height: 1.8; color: var(--text-primary); white-space: pre-wrap; }
.tr-pair-symbol-link { cursor: pointer; color: var(--accent-gold); text-decoration: none; font-weight: 700; }
.tr-pair-symbol-link:hover { text-decoration: underline; }
.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; }
@ -577,17 +586,9 @@
.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); }
/* 空状态 */
.tr-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 200px; gap: 12px; }
.tr-empty-icon { font-size: 56px; opacity: 0.3; }
.tr-empty-text { font-size: 15px; color: var(--text-tertiary); }
@media (max-width: 1200px) {
.tr-stats-grid { grid-template-columns: repeat(3, 1fr); }
}
@media (max-width: 768px) {
.tr-stats-grid { grid-template-columns: repeat(2, 1fr); }
.tr-overview-grid { grid-template-columns: repeat(2, 1fr); }
.tr-pairs-grid { grid-template-columns: 1fr; }
}
@media (max-width: 768px) {
@ -605,8 +606,8 @@
<div class="nav-items">
<a href="#" class="nav-item active" data-page="analysis">品种分析</a>
<a href="#" class="nav-item" data-page="watched">自选</a>
<a href="#" class="nav-item" data-page="market" style="display:none">市场概览</a>
<a href="#" class="nav-item" data-page="risk" style="display:none">风险预警</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="review">复盘计划</a>
<a href="#" class="nav-item" data-page="trade-review">交易复盘</a>
</div>
@ -978,13 +979,9 @@
<div class="tr-toolbar">
<div class="tr-toolbar-left">
<input type="file" id="tr-file-input" accept=".xls,.xlsx" style="display:none" />
<input type="file" id="tr-batch-file-input" accept=".xls,.xlsx" multiple style="display:none" />
<button id="tr-btn-import" class="tr-btn tr-btn-primary">
<span>📥</span><span>导入结算单</span>
</button>
<button id="tr-btn-batch-import" class="tr-btn tr-btn-primary" title="同时导入多个结算单文件">
<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="结束日期" />
@ -1000,148 +997,120 @@
</div>
</div>
<!-- 6个统计卡片 -->
<div id="tr-stats-grid" class="tr-stats-grid"></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="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 class="tr-chart-box">
<div class="tr-chart-title">账户权益曲线(累计净盈亏 + 每日盈亏)</div>
<div class="tr-chart-container" id="tr-equity-chart"></div>
</div>
<!-- 统计卡片 -->
<div id="tr-stats-grid" class="tr-stats-grid"></div>
<!-- 品种盈亏分布气泡图 -->
<div class="tr-chart-box">
<div class="tr-chart-title">品种盈亏分布(气泡大小=手续费占比,颜色=净盈亏)</div>
<div class="tr-bubble-container" id="tr-bubble-chart"></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>
<!-- 每日交易详情表 -->
<div class="tr-tbl-box">
<div class="tr-tbl-title">每日交易详情(点击日期查看详情)</div>
<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>
</tr>
</thead>
<tbody id="tr-daily-body"></tbody>
</table>
<!-- 子页面: 每日分析 -->
<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 class="tr-empty-text">暂无每日数据</div>
</div>
</div>
<!-- 品种盈亏排行表 -->
<div class="tr-tbl-box">
<div class="tr-tbl-title">品种盈亏排行(点击查看品种详情)</div>
<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 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>
<!-- 每日详情弹窗 -->
<div id="tr-daily-modal" class="rv-modal-overlay" onclick="if(event.target===this)trCloseDailyModal()">
<div class="rv-modal" style="max-width:1000px;">
<div class="rv-modal-header">
<span class="rv-modal-title" id="tr-daily-modal-title">每日详情</span>
<button class="rv-modal-close" onclick="trCloseDailyModal()"></button>
</div>
<div class="rv-modal-body">
<div id="tr-daily-modal-stats" class="tr-overview-grid"></div>
<div class="tr-ai-box">
<div class="tr-ai-title">🤖 AI 交易诊断报告</div>
<div class="tr-ai-content" id="tr-daily-ai-content"></div>
</div>
<div class="tr-variety-section">
<div class="tr-tbl-title" style="margin-bottom:12px">当日品种盈亏统计</div>
<div class="tr-chart-container" id="tr-daily-variety-chart" style="height:280px;margin-bottom:16px;"></div>
<div class="tr-tbl-box" style="padding:0;box-shadow:none;">
<table class="tr-table">
<thead><tr><th>品种</th><th>笔数</th><th>平仓盈亏</th><th>手续费</th><th>净盈亏</th><th>胜率</th><th>操作</th></tr></thead>
<tbody id="tr-daily-variety-body"></tbody>
</table>
</div>
</div>
<div class="tr-tbl-box" style="margin-top:24px;padding:0;box-shadow:none;">
<div class="tr-tbl-title">当日交易流水</div>
<table class="tr-table">
<thead><tr><th>时间</th><th>品种</th><th>方向</th><th>开仓价</th><th>手数</th><th>盈亏</th><th>手续费</th></tr></thead>
<tbody id="tr-daily-trades-body"></tbody>
</table>
</div>
</div>
</div>
</div>
<!-- 品种详情弹窗 -->
<div id="tr-variety-modal" class="rv-modal-overlay" onclick="if(event.target===this)trCloseVarietyModal()">
<div class="rv-modal" style="max-width:1000px;">
<div class="rv-modal-header">
<span class="rv-modal-title" id="tr-variety-modal-title">品种详情</span>
<button class="rv-modal-close" onclick="trCloseVarietyModal()"></button>
</div>
<div class="rv-modal-body">
<div id="tr-variety-modal-stats" class="tr-overview-grid"></div>
<div class="tr-chart-box" style="margin-bottom:16px;">
<div class="tr-kline-tabs">
<button class="tr-kline-tab active" data-period="daily" onclick="trSwitchVarietyKlinePeriod('daily')">日线</button>
<button class="tr-kline-tab" data-period="60min" onclick="trSwitchVarietyKlinePeriod('60min')">60分钟</button>
<button class="tr-kline-tab" data-period="15min" onclick="trSwitchVarietyKlinePeriod('15min')">15分钟</button>
<button class="tr-kline-tab" data-period="5min" onclick="trSwitchVarietyKlinePeriod('5min')">5分钟</button>
<button id="tr-variety-reload-kline" class="tr-btn tr-btn-secondary" style="margin-left:auto;">刷新K线</button>
</div>
<div class="tr-kline-chart" id="tr-variety-kline-chart"></div>
</div>
<div class="tr-tbl-box" style="padding:0;box-shadow:none;">
<div class="tr-tbl-title">该品种交易记录</div>
<table class="tr-table">
<thead><tr><th>开仓时间</th><th>方向</th><th>开仓价</th><th>平仓价</th><th>盈亏</th><th>手续费</th><th>操作</th></tr></thead>
<tbody id="tr-variety-trades-body"></tbody>
</table>
</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-detail-modal" class="rv-modal-overlay" onclick="if(event.target===this)trCloseDetailModal()">
<div class="rv-modal" style="max-width:700px;">
<div class="rv-modal" style="max-width:1000px;">
<div class="rv-modal-header">
<span class="rv-modal-title" id="tr-detail-title">交易详情</span>
<button class="rv-modal-close" onclick="trCloseDetailModal()"></button>
</div>
<div class="rv-modal-body">
<div id="tr-detail-info" class="tr-overview-grid"></div>
<div class="tr-chart-box" style="margin-top:20px;">
<div class="tr-chart-title">K线走势与买卖点</div>
<div class="tr-kline-tabs">
<button class="tr-kline-tab active" data-period="daily" onclick="trSwitchKlinePeriod('daily')">日线</button>
<button class="tr-kline-tab" data-period="60min" onclick="trSwitchKlinePeriod('60min')">60分钟</button>
<button class="tr-kline-tab" data-period="15min" onclick="trSwitchKlinePeriod('15min')">15分钟</button>
<button class="tr-kline-tab" data-period="5min" onclick="trSwitchKlinePeriod('5min')">5分钟</button>
<button id="tr-detail-reload-kline" class="tr-btn tr-btn-secondary" style="margin-left:auto;">刷新K线</button>
<!-- 交易信息 -->
<div id="tr-detail-info" class="tr-detail-info"></div>
<!-- K线图 -->
<div class="tr-detail-section">
<h4 class="tr-detail-section-title">K线走势与买卖点</h4>
<div class="tr-kline-controls" style="margin-bottom:12px;">
<select id="tr-detail-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-detail-reload-kline" class="tr-btn tr-btn-secondary">刷新K线</button>
</div>
<div id="tr-detail-kline" style="width:100%;height:300px;"></div>
<div id="tr-detail-kline" style="width:100%;height:400px;"></div>
</div>
<div style="margin-top:20px;text-align:center;">
<button id="tr-detail-ai-btn" class="tr-btn tr-btn-primary">开始AI分析</button>
<!-- AI分析 -->
<div class="tr-detail-section">
<h4 class="tr-detail-section-title">AI 交易分析</h4>
<div id="tr-detail-ai-content" class="tr-detail-ai-content">
<button id="tr-detail-ai-btn" class="tr-btn tr-btn-primary">开始AI分析</button>
</div>
</div>
<div id="tr-detail-ai-content" class="tr-ai-box" style="display:none;margin-top:16px;"></div>
</div>
</div>
</div>
@ -1159,16 +1128,15 @@
</div>
</div>
<!-- 批量导入结果弹窗 -->
<div id="tr-batch-import-modal" class="rv-modal-overlay" onclick="if(event.target===this)trCloseBatchImportModal()">
<!-- 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">批量导入结果</span>
<button class="rv-modal-close" onclick="trCloseBatchImportModal()"></button>
<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-batch-import-summary" style="margin-bottom:16px;font-size:14px;font-weight:600;color:var(--text-primary);"></div>
<div id="tr-batch-import-details" style="max-height:400px;overflow-y:auto;"></div>
<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>

File diff suppressed because it is too large Load Diff

@ -1330,14 +1330,10 @@
// Navigation
function navigateTo(page) {
if (!page) return;
const pageEl = document.getElementById(`page-${page}`);
if (!pageEl) return;
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
pageEl.classList.add('active');
document.getElementById(`page-${page}`).classList.add('active');
document.querySelector(`.nav-item[data-page="${page}"]`).classList.add('active');
const titles = {
@ -1351,7 +1347,7 @@
document.getElementById('pageTitle').textContent = titles[page] || page;
}
document.querySelectorAll('.nav-item[data-page]').forEach(item => {
document.querySelectorAll('.nav-item').forEach(item => {
item.addEventListener('click', () => navigateTo(item.dataset.page));
});
@ -2023,15 +2019,15 @@
const jsonStr = JSON.stringify(exportObj, null, 2);
const blob = new Blob([jsonStr], { type: 'application/json' });
const downloadUrl = URL.createObjectURL(blob);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = downloadUrl;
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(downloadUrl);
URL.revokeObjectURL(url);
const periodCount = currentQueryData.timeframes.length;
const totalCandles = currentQueryData.timeframes.reduce((sum, tf) => sum + (tf.candles ? tf.candles.length : 0), 0);
@ -2100,9 +2096,9 @@
}
const queryString = params.toString();
const fetchUrl = `${API}/data/latest/${symbol}${queryString ? '?' + queryString : ''}`;
const url = `${API}/data/latest/${symbol}${queryString ? '?' + queryString : ''}`;
const fetchRes = await fetch(fetchUrl);
const fetchRes = await fetch(url);
if (!fetchRes.ok) {
failedCount++;
failedSymbols.push(symbol);
@ -2137,15 +2133,15 @@
const jsonStr = JSON.stringify(exportObj, null, 2);
const blob = new Blob([jsonStr], { type: 'application/json' });
const downloadUrl = URL.createObjectURL(blob);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = downloadUrl;
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(downloadUrl);
URL.revokeObjectURL(url);
const candleCount = fetchData.timeframes.reduce((sum, tf) => sum + (tf.candles ? tf.candles.length : 0), 0);
successCount++;
@ -2622,6 +2618,51 @@
}
}
// Load Tasks
async function loadTasks() {
try {
const res = await fetch(`${API}/tasks`);
const data = await res.json();
if (!data.tasks.length) {
document.getElementById('taskTable').innerHTML = '<div class="empty-state"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg><p>暂无定时任务</p></div>';
return;
}
let html = `<table>
<thead><tr><th>ID</th><th>品种</th><th>周期</th><th>间隔</th><th>状态</th><th>最后执行</th><th>操作</th></tr></thead>
<tbody>`;
for (const t of data.tasks) {
const statusBadge = t.running
? '<span class="badge badge-success">运行中</span>'
: t.enabled
? '<span class="badge badge-warning">已停止</span>'
: '<span class="badge badge-gray">已禁用</span>';
html += `<tr>
<td>${t.id}</td>
<td><code style="color: var(--primary);">${t.symbol}</code></td>
<td>${t.periods.join(', ')}</td>
<td>${t.interval_seconds}s</td>
<td>${statusBadge}</td>
<td>${t.last_run ? new Date(t.last_run).toLocaleString() : '-'}</td>
<td>
${t.running
? `<button class="btn btn-warning btn-sm" onclick="stopTask(${t.id})">停止</button>`
: `<button class="btn btn-success btn-sm" onclick="startTask(${t.id})">启动</button>`
}
<button class="btn btn-danger btn-sm" onclick="deleteTask(${t.id})">删除</button>
</td>
</tr>`;
}
html += '</tbody></table>';
document.getElementById('taskTable').innerHTML = html;
} catch (e) {
addLog(`加载任务失败: ${e.message}`, 'error');
}
}
async function stopTask(id) {
await fetch(`${API}/tasks/${id}/stop`, { method: 'POST' });
showToast('任务已停止', 'success');

Binary file not shown.

Binary file not shown.

@ -1,409 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>期货智析 - 交易复盘</title>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:#f5f7fa;color:#333;line-height:1.6}
.header{background:#fff;border-bottom:1px solid #e8e8e8;padding:0 24px;height:56px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:100}
.header-left{display:flex;align-items:center;gap:8px}
.logo{font-size:16px;font-weight:600}
.nav-tabs{display:flex;gap:24px}
.nav-tab{padding:16px 0;font-size:14px;color:#666;cursor:pointer;border-bottom:2px solid transparent}
.nav-tab.active{color:#1890ff;border-bottom-color:#1890ff;font-weight:500}
.header-right{font-size:12px;color:#999}
.main{max-width:1400px;margin:0 auto;padding:24px}
.toolbar{display:flex;align-items:center;gap:12px;margin-bottom:20px;flex-wrap:wrap}
.btn{padding:8px 16px;border:none;border-radius:6px;font-size:14px;cursor:pointer;display:flex;align-items:center;gap:6px}
.btn-primary{background:#1890ff;color:#fff}
.btn-danger{background:#fff;color:#ff4d4f;border:1px solid #ffccc7}
.btn-default{background:#fff;color:#666;border:1px solid #d9d9d9}
.date-picker{padding:8px 12px;border:1px solid #d9d9d9;border-radius:6px;font-size:14px}
.date-range{display:flex;align-items:center;gap:8px}
.toolbar-right{margin-left:auto;display:flex;gap:12px}
.cards{display:grid;grid-template-columns:repeat(6,1fr);gap:16px;margin-bottom:24px}
.card{background:#fff;border-radius:8px;padding:16px;text-align:center;box-shadow:0 1px 2px rgba(0,0,0,0.05)}
.card-label{font-size:12px;color:#999;margin-bottom:8px}
.card-value{font-size:24px;font-weight:600;margin-bottom:4px}
.card-sub{font-size:12px;color:#999}
.green{color:#52c41a}
.red{color:#ff4d4f}
.box{background:#fff;border-radius:8px;padding:20px;margin-bottom:24px;box-shadow:0 1px 2px rgba(0,0,0,0.05)}
.box-title{font-size:16px;font-weight:500;margin-bottom:16px}
.chart{width:100%;height:350px}
.tbl-box{background:#fff;border-radius:8px;padding:20px;margin-bottom:24px;box-shadow:0 1px 2px rgba(0,0,0,0.05);overflow-x:auto}
table{width:100%;border-collapse:collapse;font-size:13px}
th{background:#fafafa;padding:12px 8px;text-align:left;font-weight:500;color:#666;border-bottom:1px solid #e8e8e8;white-space:nowrap}
td{padding:12px 8px;border-bottom:1px solid #f0f0f0;white-space:nowrap}
tr:hover{background:#f5f7fa}
.link{color:#1890ff;cursor:pointer;text-decoration:none}
.modal-bg{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:1000;justify-content:center;align-items:center}
.modal-bg.show{display:flex}
#vmod,#tmod{z-index:1001}
.modal{background:#fff;border-radius:12px;width:90%;max-width:1000px;max-height:90vh;overflow-y:auto}
.modal-hd{padding:20px 24px;border-bottom:1px solid #e8e8e8;display:flex;justify-content:space-between;align-items:center}
.modal-close{width:32px;height:32px;border:none;background:#f5f5f5;border-radius:50%;cursor:pointer;font-size:18px}
.modal-bd{padding:24px}
.ov{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-bottom:24px}
.ov-item{background:#f5f7fa;border-radius:8px;padding:16px;text-align:center}
.ov-label{font-size:12px;color:#999}
.ov-val{font-size:20px;font-weight:600}
.ktb{display:flex;gap:8px;margin-bottom:16px}
.kbtn{padding:6px 12px;border:1px solid #d9d9d9;background:#fff;border-radius:4px;cursor:pointer;font-size:13px}
.kbtn.active{background:#1890ff;color:#fff;border-color:#1890ff}
.bubble{width:100%;height:400px}
.ai-box{background:linear-gradient(135deg, #f0f7ff 0%, #e6f4ff 100%);border:1px solid #bae0ff;border-radius:8px;padding:20px;margin-bottom:24px;position:relative}
.ai-title{font-size:16px;font-weight:600;color:#0958d9;margin-bottom:12px;display:flex;align-items:center;gap:8px}
.ai-content{font-size:14px;color:#333;line-height:1.8}
.ai-tag{display:inline-block;background:#e6f4ff;color:#0958d9;padding:2px 8px;border-radius:4px;font-size:12px;margin-right:8px;margin-bottom:8px}
.variety-sec{margin-top:24px;border-top:1px solid #e8e8e8;padding-top:20px}
@media(max-width:1200px){.cards{grid-template-columns:repeat(3,1fr)}}
@media(max-width:768px){.cards{grid-template-columns:repeat(2,1fr)}}
</style>
</head>
<body>
<div class="header">
<div class="header-left"><div class="logo">&#9670; 期货智析</div></div>
<div class="nav-tabs"><div class="nav-tab">品种分析</div><div class="nav-tab">自选</div><div class="nav-tab">复盘计划</div><div class="nav-tab active">交易复盘</div></div>
<div class="header-right"><span style="color:#52c41a">&#9679;</span> LIVE &nbsp; 2026-06-21 22:41:23</div>
</div>
<div class="main">
<div class="toolbar">
<button class="btn btn-primary">导入结算单</button><button class="btn btn-primary">批量导入</button>
<div class="date-range"><input type="date" class="date-picker" value="2026-06-01"><span>~</span><input type="date" class="date-picker" value="2026-06-12"></div>
<button class="btn btn-default">查询</button>
<div class="toolbar-right"><button class="btn btn-danger">删除当日</button><button class="btn btn-default">批次管理</button></div>
</div>
<div class="cards">
<div class="card"><div class="card-label">总盈亏</div><div class="card-value red">-656.43</div><div class="card-sub">平仓盈亏</div></div>
<div class="card"><div class="card-label">净盈亏</div><div class="card-value red">-3502.86</div><div class="card-sub">扣除手续费后</div></div>
<div class="card"><div class="card-label">胜率</div><div class="card-value">14.2%</div><div class="card-sub">盈亏比: 5.72</div></div>
<div class="card"><div class="card-label">交易笔数</div><div class="card-value">398</div><div class="card-sub">10个交易日</div></div>
<div class="card"><div class="card-label">最大回撤</div><div class="card-value red">-1768.27</div><div class="card-sub">2026-06-03</div></div>
<div class="card"><div class="card-label">日均盈亏</div><div class="card-value red">-65.64</div><div class="card-sub">最大连盈5/连亏5</div></div>
</div>
<div class="box"><div class="box-title">账户权益曲线(累计净盈亏 + 每日盈亏)</div><div class="chart" id="eq"></div></div>
<div class="box"><div class="box-title">品种盈亏分布(气泡大小=手续费占比,颜色=净盈亏)</div><div class="bubble" id="bb"></div></div>
<div class="tbl-box"><div class="box-title">每日交易详情(点击日期查看详情)</div>
<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></tr></thead><tbody id="dtb"></tbody></table></div>
<div class="tbl-box"><div class="box-title">品种盈亏排行(点击查看品种详情)</div>
<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="vtb"></tbody></table></div>
</div>
<div class="modal-bg" id="vmod">
<div class="modal">
<div class="modal-hd"><div class="box-title" id="vtitle">品种详情</div><button class="modal-close" onclick="cm('vmod')">&times;</button></div>
<div class="modal-bd">
<div class="ov">
<div class="ov-item"><div class="ov-label">净盈亏</div><div class="ov-val red" id="vn">-</div></div>
<div class="ov-item"><div class="ov-label">胜率</div><div class="ov-val" id="vw">-</div></div>
<div class="ov-item"><div class="ov-label">盈亏比</div><div class="ov-val" id="vr">-</div></div>
<div class="ov-item"><div class="ov-label">交易笔数</div><div class="ov-val" id="vc">-</div></div>
<div class="ov-item"><div class="ov-label">手续费</div><div class="ov-val" id="vf">-</div></div>
<div class="ov-item"><div class="ov-label">最大单笔盈利</div><div class="ov-val green" id="vm">-</div></div>
</div>
<div class="box" style="margin-bottom:16px">
<div class="ktb"><button class="kbtn active">日线</button><button class="kbtn">60分钟</button><button class="kbtn">15分钟</button><button class="kbtn">5分钟</button><button class="btn btn-default" style="margin-left:auto">刷新K线</button></div>
<div class="chart" id="kc" style="height:400px"></div>
</div>
<div class="tbl-box"><div class="box-title">该品种交易记录</div>
<table><thead><tr><th>开仓时间</th><th>方向</th><th>开仓价</th><th>平仓价</th><th>盈亏</th><th>手续费</th><th>操作</th></tr></thead><tbody id="vttb"></tbody></table></div>
</div></div></div>
<div class="modal-bg" id="tmod">
<div class="modal" style="max-width:700px">
<div class="modal-hd"><div class="box-title" id="ttitle">交易详情</div><button class="modal-close" onclick="cm('tmod')">&times;</button></div>
<div class="modal-bd">
<div class="ov">
<div class="ov-item"><div class="ov-label">品种</div><div class="ov-val" id="tv">-</div></div>
<div class="ov-item"><div class="ov-label">方向</div><div class="ov-val red" id="td">-</div></div>
<div class="ov-item"><div class="ov-label">开仓日期</div><div class="ov-val" id="tod">-</div></div>
<div class="ov-item"><div class="ov-label">开仓时间</div><div class="ov-val" id="tot">-</div></div>
<div class="ov-item"><div class="ov-label">开仓价</div><div class="ov-val" id="top">-</div></div>
<div class="ov-item"><div class="ov-label">平仓价</div><div class="ov-val" id="tcp">-</div></div>
<div class="ov-item"><div class="ov-label">手数</div><div class="ov-val" id="tvol">-</div></div>
<div class="ov-item"><div class="ov-label">手续费</div><div class="ov-val" id="tfee">-</div></div>
<div class="ov-item"><div class="ov-label">净盈亏</div><div class="ov-val red" id="tpnl">-</div></div>
</div>
<div class="box" style="margin-top:20px"><div class="box-title">K线走势与买卖点</div><div class="chart" id="tkc" style="height:300px"></div></div>
<div style="margin-top:20px;text-align:center"><button class="btn btn-primary">开始AI分析</button></div>
</div></div></div>
<div class="modal-bg" id="dmod">
<div class="modal" style="max-width:1000px">
<div class="modal-hd"><div class="box-title" id="dtitle">每日详情</div><button class="modal-close" onclick="cm('dmod')">&times;</button></div>
<div class="modal-bd">
<div class="ov">
<div class="ov-item"><div class="ov-label">平仓盈亏</div><div class="ov-val red" id="dpnl">-</div></div>
<div class="ov-item"><div class="ov-label">净盈亏</div><div class="ov-val red" id="dnpnl">-</div></div>
<div class="ov-item"><div class="ov-label">胜率</div><div class="ov-val" id="dwr">-</div></div>
<div class="ov-item"><div class="ov-label">手续费</div><div class="ov-val" id="dfee">-</div></div>
<div class="ov-item"><div class="ov-label">最大盈利</div><div class="ov-val green" id="dmp">-</div></div>
<div class="ov-item"><div class="ov-label">最大亏损</div><div class="ov-val red" id="dml">-</div></div>
</div>
<div class="ai-box">
<div class="ai-title">&#129302; AI 交易诊断报告</div>
<div class="ai-content" id="dai"></div>
</div>
<div class="variety-sec">
<div class="box-title" style="margin-bottom:12px">当日品种盈亏统计(点击品种名查看详情)</div>
<div class="chart" id="dv_chart" style="height:280px;margin-bottom:16px"></div>
<div class="tbl-box"><table><thead><tr><th>品种</th><th>笔数</th><th>平仓盈亏</th><th>手续费</th><th>净盈亏</th><th>胜率</th><th>操作</th></tr></thead><tbody id="dvtb"></tbody></table></div>
</div>
<div class="tbl-box" style="margin-top:24px"><div class="box-title">当日交易流水</div>
<table><thead><tr><th>时间</th><th>品种</th><th>方向</th><th>开仓价</th><th>平仓价</th><th>盈亏</th><th>手续费</th></tr></thead><tbody id="dttb"></tbody></table></div>
</div></div></div>
<script>
var D=[
{d:"06-01",fd:"2026-06-01",t:33,p:-1042.18,c:982.18,w:7,l:25,wr:"21.9%",mp:1271.67,ml:-1128.42,v:8},
{d:"06-02",fd:"2026-06-02",t:46,p:-14.49,c:239.49,w:5,l:32,wr:"13.5%",mp:487.48,ml:-465.71,v:8},
{d:"06-03",fd:"2026-06-03",t:40,p:-1768.27,c:113.27,w:5,l:30,wr:"14.3%",mp:149.99,ml:-1512.02,v:8},
{d:"06-04",fd:"2026-06-04",t:64,p:-797.55,c:367.55,w:5,l:42,wr:"10.6%",mp:387.98,ml:-1128.78,v:8},
{d:"06-05",fd:"2026-06-05",t:44,p:-749.88,c:249.88,w:1,l:31,wr:"3.1%",mp:221.09,ml:-778.98,v:7},
{d:"06-08",fd:"2026-06-08",t:37,p:512.71,c:207.29,w:1,l:27,wr:"3.6%",mp:711.09,ml:-45.6,v:5},
{d:"06-09",fd:"2026-06-09",t:46,p:544.93,c:175.07,w:9,l:29,wr:"23.7%",mp:320,ml:-270,v:11},
{d:"06-10",fd:"2026-06-10",t:33,p:1497.85,c:212.15,w:6,l:21,wr:"22.2%",mp:468.04,ml:-45.6,v:9},
{d:"06-11",fd:"2026-06-11",t:34,p:960.64,c:169.36,w:4,l:24,wr:"14.3%",mp:1315.44,ml:-370,v:9},
{d:"06-12",fd:"2026-06-12",t:21,p:199.81,c:130.19,w:3,l:18,wr:"14.3%",mp:154.11,ml:-30.4,v:7}
];
var V=[
{c:"BU",n:"沥青",np:2495.94,p:2530,f:34.06,wr:"41.7%",pr:3.1,t:24},
{c:"MA",n:"甲醇",np:997.28,p:1090,f:92.72,wr:"50%",pr:2.5,t:8},
{c:"P",n:"棕榈油",np:419.52,p:480,f:60.48,wr:"37.5%",pr:1.8,t:24},
{c:"RU",n:"橡胶",np:46.97,p:50,f:3.03,wr:"50%",pr:1.2,t:2},
{c:"PS",n:"多晶硅",np:-4.1,p:0,f:4.1,wr:"0%",pr:0,t:1},
{c:"LC",n:"碳酸锂",np:-12.2,p:0,f:12.2,wr:"0%",pr:0,t:6},
{c:"SH",n:"烧碱",np:-11.52,p:0,f:11.52,wr:"25%",pr:0,t:4},
{c:"AG",n:"沪银",np:-32.8,p:0,f:32.8,wr:"0%",pr:0,t:28},
{c:"NI",n:"沪镍",np:-29.45,p:0,f:29.45,wr:"0%",pr:0,t:13},
{c:"AO",n:"氧化铝",np:-26.79,p:20,f:46.79,wr:"37.5%",pr:0.5,t:8},
{c:"CF",n:"棉花",np:-209.63,p:-175,f:34.63,wr:"33.3%",pr:0.7,t:15},
{c:"SC",n:"原油",np:-161.6,p:0,f:161.6,wr:"0%",pr:0,t:31},
{c:"EC",n:"集运欧线",np:-127.42,p:575,f:702.42,wr:"25%",pr:1.5,t:4},
{c:"CU",n:"沪铜",np:-95.95,p:0,f:95.95,wr:"0%",pr:0,t:26},
{c:"SN",n:"沪锡",np:-71.3,p:0,f:71.3,wr:"0%",pr:0,t:78},
{c:"OI",n:"菜油",np:-752.62,p:-690,f:62.62,wr:"19.4%",pr:0.6,t:31},
{c:"MO",n:"锰硅",np:-820.8,p:0,f:820.8,wr:"0%",pr:0,t:47},
{c:"TA",n:"PTA",np:-276.02,p:-270,f:6.02,wr:"0%",pr:0,t:3},
{c:"AP",n:"苹果",np:-315.04,p:-290,f:25.04,wr:"0%",pr:0,t:2},
{c:"JM",n:"焦煤",np:-2457.42,p:-2000,f:457.42,wr:"18.8%",pr:0.82,t:16}
];
V.sort(function(a,b){return b.np-a.np});
// Seeded RNG for consistent mock data
function mulberry32(a) {
return function() {
var t = a += 0x6D2B79F5;
t = Math.imul(t ^ t >>> 15, t | 1);
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
return ((t ^ t >>> 14) >>> 0) / 4294967296;
}
}
function hashDate(str) {
var h = 0;
for (var i = 0; i < str.length; i++) {
h = Math.imul(31, h) + str.charCodeAt(i) | 0;
}
return h;
}
function genDailyVariety(date) {
var seed = hashDate(date);
var rand = mulberry32(seed);
var dayData = D.filter(function(d){return d.fd===date})[0];
var total_p = dayData.p;
var total_c = dayData.c;
var result = [];
V.forEach(function(v) {
if (rand() > 0.3) {
var factor = (rand() - 0.5) * 2;
var pnl = total_p * factor * (rand() * 2);
var fee = Math.max(0, total_c * (rand() * 0.1));
var trades = Math.floor(rand() * 5) + 1;
var wins = Math.floor(rand() * trades);
result.push({ c: v.c, n: v.n, pnl: pnl, fee: fee, trades: trades, wins: wins, winRate: Math.round((wins / trades) * 100) + '%' });
}
});
result.sort(function(a,b){return b.pnl-a.pnl});
return result;
}
function renderDailyVariety(date) {
var data = genDailyVariety(date);
var tbody = document.getElementById("dvtb");
tbody.innerHTML = "";
var categories = data.map(function(x){return x.n});
var values = data.map(function(x){return x.pnl});
if(data.length===0){tbody.innerHTML="<tr><td colspan='7' style='text-align:center'>当日无品种交易数据</td></tr>";return;}
data.forEach(function(item) {
var pnlClass = item.pnl >= 0 ? 'green' : 'red';
var pnlStr = item.pnl >= 0 ? '+' + item.pnl.toFixed(2) : item.pnl.toFixed(2);
tbody.innerHTML += "<tr>" +
"<td><span class='link' onclick='ov(V.findIndex(function(v){return v.c==\""+item.c+"\"}), "+JSON.stringify(item.c)+", "+JSON.stringify(item.n)+")'>"+item.c+"</span></td>" +
"<td>"+item.trades+"</td><td class='"+pnlClass+"'>"+pnlStr+"</td><td>"+item.fee.toFixed(2)+"</td><td class='"+pnlClass+"'>"+pnlStr+"</td><td>"+item.winRate+"</td>" +
"<td><span class='link' onclick='ot("+JSON.stringify(item.n)+", \"做多\")'>详情</span></td></tr>";
});
var chartDom = document.getElementById("dv_chart");
if (dvc) dvc.dispose();
dvc = echarts.init(chartDom);
var barData = values.map(function(v){return{value:v,itemStyle:{color:v>=0?'#52c41a':'#ff4d4f'}}});
dvc.setOption({
tooltip: { trigger: 'axis', formatter: '{b}: {c}元' },
grid: {left: '3%', right: '4%', bottom: '12%', containLabel: true},
xAxis: { type: 'category', data: categories, axisLabel: {interval: 0, rotate: 30} },
yAxis: {type: 'value', name: '盈亏'},
series: [{ data: barData, type: 'bar', label: {show: true, position: 'top', fontSize: 10, formatter: function(p){return p.value>=0?'+':''+p.value.toFixed(0)}} }]
});
}
function rd(){
document.getElementById("dtb").innerHTML=D.map(function(x){
var np=x.p-x.c;
return "<tr style='cursor:pointer' onclick='od("+JSON.stringify(x.fd)+")'><td class='link'>"+x.fd+"</td><td>"+x.t+"</td><td class='"+(x.p>=0?"green":"red")+"'>"+(x.p>=0?"+":"")+x.p.toFixed(2)+"</td><td>"+x.c.toFixed(2)+"</td><td class='"+(np>=0?"green":"red")+"'>"+(np>=0?"+":"")+np.toFixed(2)+"</td><td>"+x.w+"</td><td>"+x.l+"</td><td>"+x.wr+"</td><td class='green'>+"+x.mp.toFixed(2)+"</td><td class='red'>"+x.ml.toFixed(2)+"</td><td>"+x.v+"</td></tr>";
}).join("");
}
function rv(){
document.getElementById("vtb").innerHTML=V.map(function(x,i){
var ic=i<3?["","&#129352;","&#129353;"][i]:(i+1);
var pr=x.np>=0;
return "<tr><td>"+ic+"</td><td><span class='link' onclick='ov("+i+", "+JSON.stringify(x.c)+", "+JSON.stringify(x.n)+")'>"+x.c+"</span></td><td>"+x.n+"</td><td class='"+(pr?"green":"red")+"'>"+(pr?"+":"")+x.np.toFixed(2)+"</td><td class='"+(x.p>=0?"green":"red")+"'>"+(x.p>=0?"+":"")+x.p.toFixed(2)+"</td><td>"+x.f.toFixed(2)+"</td><td>"+x.wr+"</td><td>"+x.pr.toFixed(2)+"</td><td>"+x.t+"</td><td><span class='link' onclick='ov("+i+", "+JSON.stringify(x.c)+", "+JSON.stringify(x.n)+")'>详情</span></td></tr>";
}).join("");
}
var ec,bc,kc,tkc,dvc;
function ie(){
ec=echarts.init(document.getElementById("eq"));
var cum=0;var cd=D.map(function(x){cum+=(x.p-x.c);return cum});
var dp=D.map(function(x){return x.p-x.c});
var dt=D.map(function(x){return x.fd});
ec.setOption({
tooltip:{trigger:"axis",formatter:function(p){var s=p[0].name+"<br/>";p.forEach(function(q){s+=q.marker+q.seriesName+": "+(q.value>=0?"+":"")+q.value.toFixed(2)+"<br/>"});return s}},
legend:{data:["累计净盈亏","每日净盈亏"],top:0},
grid:{left:"3%",right:"4%",bottom:"3%",containLabel:true},
xAxis:{type:"category",data:dt,axisLabel:{rotate:30}},
yAxis:[{type:"value",name:"累计净盈亏"},{type:"value",name:"每日净盈亏",splitLine:{show:false}}],
series:[
{name:"累计净盈亏",type:"line",data:cd,smooth:true,lineStyle:{width:3},areaStyle:{opacity:0.1},itemStyle:{color:"#1890ff"}},
{name:"每日净盈亏",type:"bar",yAxisIndex:1,data:dp.map(function(v){return{value:v,itemStyle:{color:v>=0?"#52c41a":"#ff4d4f"}}}),barWidth:20}
]
});
}
function ib(){
bc=echarts.init(document.getElementById("bb"));
var d=V.map(function(x,i){return{name:x.n,value:[x.t,x.np,Math.sqrt(Math.abs(x.f))*5+5,i],itemStyle:{color:x.np>=0?"#52c41a":"#ff4d4f"}}});
bc.setOption({
tooltip:{formatter:function(p){return p.name+"<br/>笔数: "+p.value[0]+"<br/>净盈亏: "+p.value[1].toFixed(2)+"<br/>手续费: "+V[p.value[3]].f.toFixed(2)}},
xAxis:{name:"交易笔数",nameLocation:"middle",nameGap:30},
yAxis:{name:"净盈亏",nameLocation:"middle",nameGap:50},
series:[{type:"scatter",data:d,symbolSize:function(a){return a[2]},label:{show:true,formatter:function(a){return a.name},position:"top",fontSize:11}}]
});
}
function gk(bp,days,vol){
var dates=[],data=[];var price=bp;var sd=new Date("2026-04-01");
for(var i=0;i<days;i++){var dd=new Date(sd);dd.setDate(dd.getDate()+i);var ds=dd.getFullYear()+"-"+(dd.getMonth()+1<10?"0":"")+(dd.getMonth()+1)+"-"+(dd.getDate()<10?"0":"")+dd.getDate();dates.push(ds);var o=price;var cc=o+(Math.random()-0.5)*vol;var h=Math.max(o,cc)+Math.random()*vol*0.5;var l=Math.min(o,cc)-Math.random()*vol*0.5;data.push([o,cc,l,h]);price=cc}
return{dates:dates,data:data}
}
function ik(cid,bp,vol,bp2,sp2){
var ch=echarts.init(document.getElementById(cid));
var kl=gk(bp,60,vol);
var op={
tooltip:{trigger:"axis",formatter:function(p){var d=kl.dates[p[0].dataIndex];return d+"<br/>开: "+p[0].value[1].toFixed(2)+"<br/>收: "+p[0].value[2].toFixed(2)+"<br/>低: "+p[0].value[3].toFixed(2)+"<br/>高: "+p[0].value[4].toFixed(2)}},
grid:{left:"3%",right:"3%",bottom:"15%",containLabel:true},
xAxis:{type:"category",data:kl.dates,axisLabel:{rotate:45,fontSize:10},splitLine:{show:false}},
yAxis:{type:"value",scale:true,splitLine:{lineStyle:{type:"dashed"}}},
dataZoom:[{type:"inside",start:60,end:100},{type:"slider",show:true,start:60,end:100,height:20,bottom:0}],
series:[{type:"candlestick",data:kl.data,itemStyle:{color:"#52c41a",color0:"#ff4d4f",borderColor:"#52c41a",borderColor0:"#ff4d4f"}}]
};
if(bp2&&bp2.length)op.series.push({type:"scatter",data:bp2.map(function(b){return[b.di,kl.data[b.di][3]]}),symbol:"triangle",symbolSize:12,itemStyle:{color:"#52c41a"},label:{show:true,formatter:"买",position:"bottom",fontSize:10,color:"#52c41a"}});
if(sp2&&sp2.length)op.series.push({type:"scatter",data:sp2.map(function(s){return[s.di,kl.data[s.di][4]]}),symbol:"pin",symbolSize:12,itemStyle:{color:"#ff4d4f"},label:{show:true,formatter:"卖",position:"top",fontSize:10,color:"#ff4d4f"}});
ch.setOption(op);return ch;
}
function cm(id){document.getElementById(id).classList.remove("show")}
function ov(idx,code,name){
var x=V[idx];if(!x)return;
document.getElementById("vtitle").textContent="品种详情:"+name+" ("+code+")";
document.getElementById("vn").textContent=(x.np>=0?"+":"")+x.np.toFixed(2);
document.getElementById("vn").className="ov-val "+(x.np>=0?"green":"red");
document.getElementById("vw").textContent=x.wr;
document.getElementById("vr").textContent=x.pr.toFixed(2);
document.getElementById("vc").textContent=x.t;
document.getElementById("vf").textContent=x.f.toFixed(2);
document.getElementById("vm").textContent="+"+Math.abs(x.p*0.5).toFixed(2);
document.getElementById("vmod").classList.add("show");
if(kc)kc.dispose();
var bp={"JM":2000,"BU":3500,"MA":2500,"P":9000,"CF":16000};
kc=ik("kc",bp[code]||5000,code==="JM"?50:code==="CF"?200:30,[{di:15},{di:45}],[{di:30}]);
var rows=[];
for(var i=0;i<Math.min(x.t,8);i++){
var dir=Math.random()>0.5?"做多":"做空";
var op2=bp[code]||5000;var cp=op2+(Math.random()-0.5)*200;
var pnl=(dir==="做多"?cp-op2:op2-cp)*1;
rows.push("<tr><td>2026-06-"+(i<9?"0":"")+(i+1)+" 10:30</td><td class='"+(dir==="做多"?"green":"red")+"'>"+dir+"</td><td>"+op2.toFixed(2)+"</td><td>"+cp.toFixed(2)+"</td><td class='"+(pnl>=0?"green":"red")+"'>"+(pnl>=0?"+":"")+pnl.toFixed(2)+"</td><td>"+x.f.toFixed(2)+"</td><td><span class='link' onclick='ot("+JSON.stringify(name)+", "+JSON.stringify(dir)+")'>查看</span></td></tr>");
}
document.getElementById("vttb").innerHTML=rows.join("");
}
function ot(name,dir){
document.getElementById("ttitle").textContent=name+" "+dir+" 交易详情";
document.getElementById("tv").textContent=name;
document.getElementById("td").textContent=dir;
document.getElementById("tod").textContent="2026-06-01";
document.getElementById("tot").textContent="11:02:11";
document.getElementById("top").textContent="16300.00";
document.getElementById("tcp").textContent="16375.00";
document.getElementById("tvol").textContent="1";
document.getElementById("tfee").textContent="4.33";
document.getElementById("tpnl").textContent="-379.33";
document.getElementById("tmod").classList.add("show");
if(tkc)tkc.dispose();
tkc=ik("tkc",16000,200,[{di:20}],[{di:35}]);
}
function genAI(x){
var tags=[], txt="";
var wr = parseFloat(x.wr);
var np = x.p - x.c;
if(np<0) tags.push(""); else tags.push("");
if(wr<10) tags.push(""); else if(wr>30) tags.push("高胜率");
if(x.t>40) tags.push("频繁交易");
if(x.c>Math.abs(x.p)) tags.push("高手续费");
tags.forEach(function(t){txt+='<span class="ai-tag">'+t+'</span>'});
txt+="<br/><strong>1. 交易质量诊断:</strong><br/>";
if(wr<15) txt+=""+x.wr+"<br/>";
else txt+="胜率尚可,但需关注盈亏比是否合理。<br/>";
txt+="<br/><strong>2. 手续费分析:</strong><br/>";
if(x.c>Math.abs(x.p)*0.5) txt+="手续费("+x.c.toFixed(2)+")占比过高,侵蚀了利润。建议优化开仓频率或申请低费率。<br/>";
else txt+="手续费控制合理。<br/>";
txt+="<br/><strong>3. 改进建议:</strong><br/>";
if(x.l>x.w) txt+="亏损笔数较多,建议设置严格止损,避免扛单。<br/>";
txt+="建议复盘前3笔亏损交易寻找共性问题。";
return txt;
}
function od(date){
var x=D.find(function(d){return d.fd===date});
if(!x) return;
document.getElementById("dtitle").textContent="每日详情:"+date;
document.getElementById("dpnl").textContent=(x.p>=0?"+":"")+x.p.toFixed(2);
document.getElementById("dpnl").className="ov-val "+(x.p>=0?"green":"red");
var np=x.p-x.c;
document.getElementById("dnpnl").textContent=(np>=0?"+":"")+np.toFixed(2);
document.getElementById("dnpnl").className="ov-val "+(np>=0?"green":"red");
document.getElementById("dwr").textContent=x.wr;
document.getElementById("dfee").textContent=x.c.toFixed(2);
document.getElementById("dmp").textContent="+"+x.mp.toFixed(2);
document.getElementById("dml").textContent=x.ml.toFixed(2);
document.getElementById("dai").innerHTML=genAI(x);
var rows=[];
for(var i=0;i<x.t;i++){
var time="09:"+(30+Math.floor(i*1.2)%30)+":"+(Math.random()<0.5?"0":"")+Math.floor(Math.random()*60);
var varName=["JM","BU","MA","P","CF"][Math.floor(Math.random()*5)];
var dir=Math.random()>0.5?"做多":"做空";
var op=1000+Math.random()*1000;
var cp=op+(Math.random()-0.5)*200;
var pnl=(dir==="做多"?cp-op:op-cp);
var fee=Math.random()*10;
rows.push("<tr><td>"+time+"</td><td>"+varName+"</td><td>"+dir+"</td><td>"+op.toFixed(2)+"</td><td>"+cp.toFixed(2)+"</td><td class='"+(pnl>=0?"green":"red")+"'>"+(pnl>=0?"+":"")+pnl.toFixed(2)+"</td><td>"+fee.toFixed(2)+"</td></tr>");
}
document.getElementById("dttb").innerHTML=rows.join("");
document.getElementById("dmod").classList.add("show");
setTimeout(function(){renderDailyVariety(date)},100);
}
window.onload=function(){rd();rv();ie();ib()};
</script>
</body>
</html>
Loading…
Cancel
Save