Compare commits

..

9 Commits

@ -30,7 +30,7 @@ async def import_settlement(
file: UploadFile = File(...), file: UploadFile = File(...),
db: Session = Depends(get_analysis_db), db: Session = Depends(get_analysis_db),
): ):
"""上传并解析期货结算单文件,导入交易记录""" """上传并解析期货结算单文件,导入交易记录(含逐条去重校验)"""
if not file.filename.endswith(('.xls', '.xlsx')): if not file.filename.endswith(('.xls', '.xlsx')):
raise HTTPException(status_code=400, detail="仅支持 .xls/.xlsx 格式的结算单文件") raise HTTPException(status_code=400, detail="仅支持 .xls/.xlsx 格式的结算单文件")
@ -43,16 +43,129 @@ async def import_settlement(
result = save_to_db(db, df_futures, df_options, file.filename) 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 { return {
"success": True, "success": True,
"message": f"导入成功:期货 {result['futures_count']} 条,期权 {result['options_count']}", "message": f"全部跳过:共解析 {total_parsed} 条记录,均已存在(去重规则:交易日+品种+时间+价格),未导入任何新数据",
"data": result, "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),
"data": result,
}
except HTTPException:
raise
except Exception as e: except Exception as e:
logger.exception("导入结算单失败") logger.exception("导入结算单失败")
raise HTTPException(status_code=500, detail=f"导入失败: {str(e)}") 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") @router.get("/records")
@ -312,6 +425,21 @@ def get_kline_with_trades(
return {"success": False, "message": f"未找到 {kline_symbol}{period} K线数据"} return {"success": False, "message": f"未找到 {kline_symbol}{period} K线数据"}
candles = json.loads(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):
# 根据周期决定是否保留时间部分
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 kline_symbol = market_data.symbol
logger.info(f"[K线查询] 成功获取K线数据: symbol={kline_symbol}, candles_count={len(candles)}") logger.info(f"[K线查询] 成功获取K线数据: symbol={kline_symbol}, candles_count={len(candles)}")
finally: finally:
@ -391,8 +519,6 @@ async def analyze_trade(
db: Session = Depends(get_analysis_db), db: Session = Depends(get_analysis_db),
): ):
"""AI 分析单笔交易结合多周期K线数据""" """AI 分析单笔交易结合多周期K线数据"""
from app.services.ai_analysis import AIAnalysisService
# 收集多周期K线数据 # 收集多周期K线数据
from app.database import SessionLocal as MainSessionLocal from app.database import SessionLocal as MainSessionLocal
from app.models import MarketData from app.models import MarketData
@ -410,6 +536,13 @@ async def analyze_trade(
).first() ).first()
if market_data and market_data.candles_json: if market_data and market_data.candles_json:
candles = json.loads(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 kline_context[period] = candles[-50:] if len(candles) > 50 else candles
finally: finally:
main_db.close() main_db.close()
@ -445,13 +578,14 @@ async def analyze_trade(
""" """
try: try:
# 使用 AIAnalysisService 的模型配置和调用能力 # 使用 AIFuturesAnalyzer 的模型配置和调用能力
service = AIAnalysisService.__new__(AIAnalysisService) from app.services.ai_analysis import AIFuturesAnalyzer
model = service.get_active_model() analyzer = AIFuturesAnalyzer(db)
model = analyzer.get_active_model()
if not model: if not model:
return {"success": False, "message": "未配置AI模型或模型未激活请先在AI配置页面设置"} return {"success": False, "message": "未配置AI模型或模型未激活请先在AI配置页面设置"}
response = service.call_ai_model(prompt, model) response = analyzer.call_ai_model(prompt, model)
if not response: if not response:
return {"success": False, "message": "AI模型返回空响应请稍后重试"} return {"success": False, "message": "AI模型返回空响应请稍后重试"}

@ -136,6 +136,12 @@ def ai_config_page():
return FileResponse(str(STATIC_DIR / "ai_config.html")) 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") @app.get("/api/v1/health")
def health(): def health():
return {"status": "ok", "service": "market-data-buffer"} return {"status": "ok", "service": "market-data-buffer"}

@ -5,6 +5,7 @@
import re import re
import uuid import uuid
import json import json
import logging
from datetime import datetime from datetime import datetime
from collections import defaultdict from collections import defaultdict
from pathlib import Path from pathlib import Path
@ -15,6 +16,8 @@ from sqlalchemy.orm import Session as DBSession
from app.analysis_models import TradeRecord, TradeImportBatch from app.analysis_models import TradeRecord, TradeImportBatch
logger = logging.getLogger(__name__)
# 品种代码 -> 中文名 映射(反向映射:从合约代码前缀查中文名) # 品种代码 -> 中文名 映射(反向映射:从合约代码前缀查中文名)
_VARIETY_NAME_MAP = {} _VARIETY_NAME_MAP = {}
@ -193,6 +196,35 @@ def _normalize_time(time_val) -> str:
return s 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( def save_to_db(
db: DBSession, db: DBSession,
df_futures: pd.DataFrame, df_futures: pd.DataFrame,
@ -200,16 +232,46 @@ def save_to_db(
filename: str, filename: str,
) -> dict: ) -> dict:
""" """
将解析后的交易记录保存到数据库 将解析后的交易记录保存到数据库含逐条去重校验
:return: {"batch_id": str, "futures_count": int, "options_count": int, "trade_dates": str} 去重规则同一交易日 + 同一品种 + 同一交易时间 + 同一价格 => 视为重复跳过
:return: {
"batch_id": str, "futures_count": int, "options_count": int, "trade_dates": str,
"futures_skipped": int, "options_skipped": int, "total_parsed": int
}
""" """
_load_variety_name_map() _load_variety_name_map()
batch_id = str(uuid.uuid4()) batch_id = str(uuid.uuid4())
all_dates = set() all_dates = set()
futures_count = 0 futures_count = 0
options_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 = [] records = []
new_keys_in_batch = set() # 防止本次导入文件内部重复
# 处理期货记录 # 处理期货记录
if not df_futures.empty: if not df_futures.empty:
@ -217,12 +279,24 @@ def save_to_db(
contract = str(row.get('合约', '')).strip() contract = str(row.get('合约', '')).strip()
if not contract or contract == '合计': if not contract or contract == '合计':
continue continue
futures_parsed += 1
variety = extract_variety(contract) variety = extract_variety(contract)
trade_date = _normalize_date(row.get('实际成交日期', '')) trade_date = _normalize_date(row.get('实际成交日期', ''))
trade_time = _normalize_time(row.get('成交时间', '')) trade_time = _normalize_time(row.get('成交时间', ''))
bs_flag = str(row.get('买/卖', '')).strip() bs_flag = str(row.get('买/卖', '')).strip()
oc_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( rec = TradeRecord(
trade_type='期货', trade_type='期货',
symbol=contract, symbol=contract,
@ -230,7 +304,7 @@ def save_to_db(
symbol_name=_VARIETY_NAME_MAP.get(variety, ''), symbol_name=_VARIETY_NAME_MAP.get(variety, ''),
direction='' if '' in bs_flag else '', direction='' if '' in bs_flag else '',
offset='' if '' in oc_flag else ('' if '' in oc_flag else ''), offset='' if '' in oc_flag else ('' if '' in oc_flag else ''),
price=safe_float(row.get('成交价')), price=price,
volume=safe_float(row.get('手数')), volume=safe_float(row.get('手数')),
amount=safe_float(row.get('成交额')), amount=safe_float(row.get('成交额')),
close_pnl=safe_float(row.get('平仓盈亏')), close_pnl=safe_float(row.get('平仓盈亏')),
@ -242,8 +316,6 @@ def save_to_db(
) )
records.append(rec) records.append(rec)
futures_count += 1 futures_count += 1
if trade_date:
all_dates.add(trade_date)
# 处理期权记录 # 处理期权记录
if not df_options.empty: if not df_options.empty:
@ -251,11 +323,19 @@ def save_to_db(
contract = str(row.get('品种合约', '')).strip() contract = str(row.get('品种合约', '')).strip()
if not contract or contract == '合计': if not contract or contract == '合计':
continue continue
options_parsed += 1
variety = extract_variety(contract) variety = extract_variety(contract)
trade_date = _normalize_date(row.get('成交日期', '')) trade_date = _normalize_date(row.get('成交日期', ''))
trade_time = _normalize_time(row.get('成交时间', '')) trade_time = _normalize_time(row.get('成交时间', ''))
bs_flag = str(row.get('买/卖', '')).strip() 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( rec = TradeRecord(
trade_type='期权', trade_type='期权',
symbol=contract, symbol=contract,
@ -263,7 +343,7 @@ def save_to_db(
symbol_name=_VARIETY_NAME_MAP.get(variety, ''), symbol_name=_VARIETY_NAME_MAP.get(variety, ''),
direction='' if '' in bs_flag else '', direction='' if '' in bs_flag else '',
offset='', offset='',
price=safe_float(row.get('权利金单价')), price=price,
volume=safe_float(row.get('成交量')), volume=safe_float(row.get('成交量')),
amount=safe_float(row.get('权利金')), amount=safe_float(row.get('权利金')),
close_pnl=0.0, close_pnl=0.0,
@ -275,8 +355,6 @@ def save_to_db(
) )
records.append(rec) records.append(rec)
options_count += 1 options_count += 1
if trade_date:
all_dates.add(trade_date)
if records: if records:
db.add_all(records) db.add_all(records)
@ -301,11 +379,22 @@ def save_to_db(
db.add(batch) db.add(batch)
db.commit() 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 { return {
"batch_id": batch_id, "batch_id": batch_id,
"futures_count": futures_count, "futures_count": futures_count,
"options_count": options_count, "options_count": options_count,
"trade_dates": trade_dates_str, "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; } .logo { font-size: 18px; font-weight: 600; letter-spacing: -0.02em; margin-right: 40px; }
.nav-items { display: flex; gap: 24px; flex: 1; } .nav-items { display: flex; gap: 24px; flex: 1; justify-content: center; }
.nav-item { font-size: 14px; color: var(--text-secondary); cursor: pointer; transition: color 0.2s; text-decoration: none; } .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:hover { color: var(--text-primary); }
.nav-item.active { color: var(--color-brand); font-weight: 500; } .nav-item.active { color: var(--color-brand); font-weight: 500; }
@ -503,82 +503,73 @@
.tr-btn-danger:hover { background: 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 { 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-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; } /* 6列统计卡片 */
.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-stats-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 14px; margin-bottom: 24px; }
.tr-sub-nav-item:hover { background: var(--color-brand); color: #fff; } .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-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-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-label { font-size: 11px; color: var(--text-tertiary); margin-bottom: 6px; font-weight: 600; }
.tr-stat-value { font-size: 22px; font-weight: 700; } .tr-stat-value { font-size: 24px; font-weight: 700; margin-bottom: 4px; }
.tr-stat-value.profit { color: var(--color-down); } .tr-stat-value.profit { color: var(--color-down); }
.tr-stat-value.loss { color: var(--color-up); } .tr-stat-value.loss { color: var(--color-up); }
.tr-stat-sub { font-size: 12px; color: var(--text-tertiary); margin-top: 4px; } .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-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-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 { 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 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: 9px 12px; border-bottom: 1px solid rgba(0,0,0,0.04); color: var(--text-primary); white-space: nowrap; } .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 tbody tr { transition: background 0.15s; } .tr-table tbody tr { transition: background 0.15s; }
.tr-table tbody tr:hover { background: rgba(0,122,255,0.03); } .tr-table tbody tr:hover { background: rgba(0,122,255,0.03); }
.tr-pnl-positive { color: var(--color-down); font-weight: 600; } .tr-pnl-positive { color: var(--color-down); font-weight: 600; }
.tr-pnl-negative { color: var(--color-up); font-weight: 600; } .tr-pnl-negative { color: var(--color-up); font-weight: 600; }
.tr-dir-buy { color: var(--color-down); font-weight: 500; } .tr-link { color: var(--color-brand); cursor: pointer; text-decoration: none; font-weight: 500; }
.tr-dir-sell { color: var(--color-up); font-weight: 500; } .tr-link:hover { text-decoration: underline; }
.tr-pagination { display: flex; justify-content: center; align-items: center; gap: 8px; margin-top: 16px; } /* K线周期按钮 */
.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-kline-tabs { display: flex; gap: 8px; margin-bottom: 16px; align-items: center; }
.tr-pagination button:hover { background: var(--color-brand); color: #fff; border-color: var(--color-brand); } .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-pagination button:disabled { opacity: 0.4; cursor: default; } .tr-kline-tab:hover { background: var(--color-brand); color: #fff; border-color: var(--color-brand); }
.tr-pagination span { font-size: 12px; color: var(--text-tertiary); } .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); }
.tr-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 300px; gap: 12px; } /* 弹窗通用样式(复用 rv-modal 体系) */
.tr-empty-icon { font-size: 56px; opacity: 0.3; } .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-empty-text { font-size: 15px; color: var(--text-tertiary); } .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-kline-controls { display: flex; gap: 10px; margin-bottom: 16px; align-items: center; } .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-kline-chart { width: 100%; height: 500px; background: var(--bg-card); border-radius: 16px; box-shadow: var(--shadow-sm); } .tr-modal-title { font-size: 18px; font-weight: 700; color: var(--text-primary); }
.tr-chart-container { width: 100%; height: 300px; margin-bottom: 16px; background: var(--bg-card); border-radius: 16px; box-shadow: var(--shadow-sm); } .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-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-overview-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 24px; }
.tr-detail-info-value { font-size: 14px; font-weight: 600; color: var(--text-primary); } .tr-overview-item { background: #F5F5F7; border-radius: 12px; padding: 16px; text-align: center; }
.tr-detail-info-value.profit { color: var(--color-down); } .tr-overview-label { font-size: 12px; color: var(--text-tertiary); margin-bottom: 4px; }
.tr-detail-info-value.loss { color: var(--color-up); } .tr-overview-value { font-size: 20px; font-weight: 700; color: var(--text-primary); }
.tr-detail-section { margin-top: 20px; } .tr-overview-value.profit { color: var(--color-down); }
.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-overview-value.loss { color: var(--color-up); }
.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; } /* AI诊断框 */
.tr-pair-symbol-link:hover { text-decoration: underline; } .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-pairs-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 16px; } .tr-ai-content { font-size: 14px; color: var(--text-primary); line-height: 1.8; white-space: pre-wrap; }
.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-ai-tag { display: inline-block; background: #e6f4ff; color: #0958d9; padding: 2px 8px; border-radius: 4px; font-size: 12px; margin-right: 8px; margin-bottom: 8px; }
.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; } /* 品种详情弹窗内K线 */
.tr-pair-card.profit::before { background: var(--color-down); } .tr-kline-chart { width: 100%; height: 400px; }
.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-variety-section { margin-top: 24px; border-top: 1px solid rgba(0,0,0,0.06); padding-top: 20px; }
.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-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-info { flex: 1; }
.tr-batch-file { font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 4px; } .tr-batch-file { font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 4px; }
@ -586,9 +577,17 @@
.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 { 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-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) { @media (max-width: 768px) {
.tr-stats-grid { grid-template-columns: repeat(2, 1fr); } .tr-stats-grid { grid-template-columns: repeat(2, 1fr); }
.tr-pairs-grid { grid-template-columns: 1fr; } .tr-overview-grid { grid-template-columns: repeat(2, 1fr); }
} }
@media (max-width: 768px) { @media (max-width: 768px) {
@ -606,8 +605,8 @@
<div class="nav-items"> <div class="nav-items">
<a href="#" class="nav-item active" data-page="analysis">品种分析</a> <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="watched">自选</a>
<a href="#" class="nav-item" data-page="market">市场概览</a> <a href="#" class="nav-item" data-page="market" style="display:none">市场概览</a>
<a href="#" class="nav-item" data-page="risk">风险预警</a> <a href="#" class="nav-item" data-page="risk" style="display:none">风险预警</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> <a href="#" class="nav-item" data-page="trade-review">交易复盘</a>
</div> </div>
@ -979,9 +978,13 @@
<div class="tr-toolbar"> <div class="tr-toolbar">
<div class="tr-toolbar-left"> <div class="tr-toolbar-left">
<input type="file" id="tr-file-input" accept=".xls,.xlsx" style="display:none" /> <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"> <button id="tr-btn-import" class="tr-btn tr-btn-primary">
<span>📥</span><span>导入结算单</span> <span>📥</span><span>导入结算单</span>
</button> </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="开始日期" /> <input type="date" id="tr-start-date" class="tr-date-input" title="开始日期" />
<span style="color:var(--text-tertiary);font-size:13px;">~</span> <span style="color:var(--text-tertiary);font-size:13px;">~</span>
<input type="date" id="tr-end-date" class="tr-date-input" title="结束日期" /> <input type="date" id="tr-end-date" class="tr-date-input" title="结束日期" />
@ -997,120 +1000,148 @@
</div> </div>
</div> </div>
<!-- 子导航 --> <!-- 6个统计卡片 -->
<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 id="tr-stats-grid" class="tr-stats-grid"></div> <div id="tr-stats-grid" class="tr-stats-grid"></div>
<!-- 子页面: 交易记录 --> <!-- 账户权益曲线 -->
<div id="tr-sub-records" class="tr-sub-page active"> <div class="tr-chart-box">
<div id="tr-records-table-wrap"> <div class="tr-chart-title">账户权益曲线(累计净盈亏 + 每日盈亏)</div>
<table class="tr-table"> <div class="tr-chart-container" id="tr-equity-chart"></div>
<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-chart-box">
<div class="tr-chart-title">品种盈亏分布(气泡大小=手续费占比,颜色=净盈亏)</div>
<div class="tr-bubble-container" id="tr-bubble-chart"></div>
</div> </div>
<!-- 子页面: 每日分析 --> <!-- 每日交易详情表 -->
<div id="tr-sub-daily" class="tr-sub-page"> <div class="tr-tbl-box">
<div id="tr-daily-chart" class="tr-chart-container"></div> <div class="tr-tbl-title">每日交易详情(点击日期查看详情)</div>
<div style="overflow-x:auto;">
<table class="tr-table"> <table class="tr-table">
<thead> <thead>
<tr> <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><th>亏损笔</th><th>胜率</th>
<th>最大盈利</th><th>最大亏损</th><th>品种数</th> <th>最大盈利</th><th>最大亏损</th><th>品种数</th>
</tr> </tr>
</thead> </thead>
<tbody id="tr-daily-body"></tbody> <tbody id="tr-daily-body"></tbody>
</table> </table>
</div>
<div id="tr-daily-empty" class="tr-empty" style="display:none;"> <div id="tr-daily-empty" class="tr-empty" style="display:none;">
<div class="tr-empty-icon">📊</div> <div class="tr-empty-icon">📊</div>
<div class="tr-empty-text">暂无每日数据</div> <div class="tr-empty-text">暂无交易数据,请先导入结算单</div>
</div> </div>
</div> </div>
<!-- 子页面: 品种汇总 --> <!-- 品种盈亏排行表 -->
<div id="tr-sub-variety" class="tr-sub-page"> <div class="tr-tbl-box">
<div style="overflow-x:auto;"> <div class="tr-tbl-title">品种盈亏排行(点击查看品种详情)</div>
<table class="tr-table"> <table class="tr-table">
<thead> <thead>
<tr> <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><th>胜率</th>
<th>买入</th><th>卖出</th><th>胜率</th> <th>盈亏比</th><th>笔数</th><th>操作</th>
</tr> </tr>
</thead> </thead>
<tbody id="tr-variety-body"></tbody> <tbody id="tr-variety-body"></tbody>
</table> </table>
</div>
<div id="tr-variety-empty" class="tr-empty" style="display:none;"> <div id="tr-variety-empty" class="tr-empty" style="display:none;">
<div class="tr-empty-icon">📋</div> <div class="tr-empty-icon">📋</div>
<div class="tr-empty-text">暂无品种数据</div> <div class="tr-empty-text">暂无品种数据</div>
</div> </div>
</div> </div>
</div>
<!-- 子页面: 逐笔分析 --> <!-- 每日详情弹窗 -->
<div id="tr-sub-pairs" class="tr-sub-page"> <div id="tr-daily-modal" class="rv-modal-overlay" onclick="if(event.target===this)trCloseDailyModal()">
<div id="tr-pairs-grid" class="tr-pairs-grid"></div> <div class="rv-modal" style="max-width:1000px;">
<div id="tr-pairs-empty" class="tr-empty" style="display:none;"> <div class="rv-modal-header">
<div class="tr-empty-icon">🔍</div> <span class="rv-modal-title" id="tr-daily-modal-title">每日详情</span>
<div class="tr-empty-text">暂无配对交易数据</div> <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> </div>
</div> </div>
</div> </div>
<!-- 交易详情弹窗 --> <!-- 交易详情弹窗 -->
<div id="tr-detail-modal" class="rv-modal-overlay" onclick="if(event.target===this)trCloseDetailModal()"> <div id="tr-detail-modal" class="rv-modal-overlay" onclick="if(event.target===this)trCloseDetailModal()">
<div class="rv-modal" style="max-width:1000px;"> <div class="rv-modal" style="max-width:700px;">
<div class="rv-modal-header"> <div class="rv-modal-header">
<span class="rv-modal-title" id="tr-detail-title">交易详情</span> <span class="rv-modal-title" id="tr-detail-title">交易详情</span>
<button class="rv-modal-close" onclick="trCloseDetailModal()"></button> <button class="rv-modal-close" onclick="trCloseDetailModal()"></button>
</div> </div>
<div class="rv-modal-body"> <div class="rv-modal-body">
<!-- 交易信息 --> <div id="tr-detail-info" class="tr-overview-grid"></div>
<div id="tr-detail-info" class="tr-detail-info"></div> <div class="tr-chart-box" style="margin-top:20px;">
<!-- K线图 --> <div class="tr-chart-title">K线走势与买卖点</div>
<div class="tr-detail-section"> <div class="tr-kline-tabs">
<h4 class="tr-detail-section-title">K线走势与买卖点</h4> <button class="tr-kline-tab active" data-period="daily" onclick="trSwitchKlinePeriod('daily')">日线</button>
<div class="tr-kline-controls" style="margin-bottom:12px;"> <button class="tr-kline-tab" data-period="60min" onclick="trSwitchKlinePeriod('60min')">60分钟</button>
<select id="tr-detail-period" class="tr-select"> <button class="tr-kline-tab" data-period="15min" onclick="trSwitchKlinePeriod('15min')">15分钟</button>
<option value="daily">日线</option> <button class="tr-kline-tab" data-period="5min" onclick="trSwitchKlinePeriod('5min')">5分钟</button>
<option value="60min">60分钟</option> <button id="tr-detail-reload-kline" class="tr-btn tr-btn-secondary" style="margin-left:auto;">刷新K线</button>
<option value="15min">15分钟</option> </div>
<option value="5min">5分钟</option> <div id="tr-detail-kline" style="width:100%;height:300px;"></div>
</select> </div>
<button id="tr-detail-reload-kline" class="tr-btn tr-btn-secondary">刷新K线</button> <div style="margin-top:20px;text-align:center;">
</div>
<div id="tr-detail-kline" style="width:100%;height:400px;"></div>
</div>
<!-- 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> <button id="tr-detail-ai-btn" class="tr-btn tr-btn-primary">开始AI分析</button>
</div> </div>
</div> <div id="tr-detail-ai-content" class="tr-ai-box" style="display:none;margin-top:16px;"></div>
</div> </div>
</div> </div>
</div> </div>
@ -1128,15 +1159,16 @@
</div> </div>
</div> </div>
<!-- AI分析结果弹窗 --> <!-- 批量导入结果弹窗 -->
<div id="tr-analysis-modal" class="rv-modal-overlay" onclick="if(event.target===this)trCloseAnalysisModal()"> <div id="tr-batch-import-modal" class="rv-modal-overlay" onclick="if(event.target===this)trCloseBatchImportModal()">
<div class="rv-modal" style="max-width:700px;"> <div class="rv-modal" style="max-width:700px;">
<div class="rv-modal-header"> <div class="rv-modal-header">
<span class="rv-modal-title" id="tr-analysis-title">AI 交易分析</span> <span class="rv-modal-title">批量导入结果</span>
<button class="rv-modal-close" onclick="trCloseAnalysisModal()"></button> <button class="rv-modal-close" onclick="trCloseBatchImportModal()"></button>
</div> </div>
<div class="rv-modal-body"> <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 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> </div>
</div> </div>
</div> </div>

File diff suppressed because it is too large Load Diff

@ -1330,10 +1330,14 @@
// Navigation // Navigation
function navigateTo(page) { 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('.page').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active')); document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
document.getElementById(`page-${page}`).classList.add('active'); pageEl.classList.add('active');
document.querySelector(`.nav-item[data-page="${page}"]`).classList.add('active'); document.querySelector(`.nav-item[data-page="${page}"]`).classList.add('active');
const titles = { const titles = {
@ -1347,7 +1351,7 @@
document.getElementById('pageTitle').textContent = titles[page] || page; document.getElementById('pageTitle').textContent = titles[page] || page;
} }
document.querySelectorAll('.nav-item').forEach(item => { document.querySelectorAll('.nav-item[data-page]').forEach(item => {
item.addEventListener('click', () => navigateTo(item.dataset.page)); item.addEventListener('click', () => navigateTo(item.dataset.page));
}); });
@ -2019,15 +2023,15 @@
const jsonStr = JSON.stringify(exportObj, null, 2); const jsonStr = JSON.stringify(exportObj, null, 2);
const blob = new Blob([jsonStr], { type: 'application/json' }); const blob = new Blob([jsonStr], { type: 'application/json' });
const url = URL.createObjectURL(blob); const downloadUrl = URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; a.href = downloadUrl;
a.download = filename; a.download = filename;
document.body.appendChild(a); document.body.appendChild(a);
a.click(); a.click();
document.body.removeChild(a); document.body.removeChild(a);
URL.revokeObjectURL(url); URL.revokeObjectURL(downloadUrl);
const periodCount = currentQueryData.timeframes.length; const periodCount = currentQueryData.timeframes.length;
const totalCandles = currentQueryData.timeframes.reduce((sum, tf) => sum + (tf.candles ? tf.candles.length : 0), 0); const totalCandles = currentQueryData.timeframes.reduce((sum, tf) => sum + (tf.candles ? tf.candles.length : 0), 0);
@ -2096,9 +2100,9 @@
} }
const queryString = params.toString(); const queryString = params.toString();
const url = `${API}/data/latest/${symbol}${queryString ? '?' + queryString : ''}`; const fetchUrl = `${API}/data/latest/${symbol}${queryString ? '?' + queryString : ''}`;
const fetchRes = await fetch(url); const fetchRes = await fetch(fetchUrl);
if (!fetchRes.ok) { if (!fetchRes.ok) {
failedCount++; failedCount++;
failedSymbols.push(symbol); failedSymbols.push(symbol);
@ -2133,15 +2137,15 @@
const jsonStr = JSON.stringify(exportObj, null, 2); const jsonStr = JSON.stringify(exportObj, null, 2);
const blob = new Blob([jsonStr], { type: 'application/json' }); const blob = new Blob([jsonStr], { type: 'application/json' });
const url = URL.createObjectURL(blob); const downloadUrl = URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; a.href = downloadUrl;
a.download = filename; a.download = filename;
document.body.appendChild(a); document.body.appendChild(a);
a.click(); a.click();
document.body.removeChild(a); document.body.removeChild(a);
URL.revokeObjectURL(url); URL.revokeObjectURL(downloadUrl);
const candleCount = fetchData.timeframes.reduce((sum, tf) => sum + (tf.candles ? tf.candles.length : 0), 0); const candleCount = fetchData.timeframes.reduce((sum, tf) => sum + (tf.candles ? tf.candles.length : 0), 0);
successCount++; successCount++;
@ -2618,51 +2622,6 @@
} }
} }
// 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) { async function stopTask(id) {
await fetch(`${API}/tasks/${id}/stop`, { method: 'POST' }); await fetch(`${API}/tasks/${id}/stop`, { method: 'POST' });
showToast('任务已停止', 'success'); showToast('任务已停止', 'success');

Binary file not shown.

Binary file not shown.

@ -0,0 +1,409 @@
<!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