Author SHA1 Message Date
crypto_predictor 8cdfb2a8e2 fix: 统一时区处理,准确率统计使用CST时间分组
- 增强 convert_to_cst() 函数,新增 source_tz 参数支持 UTC/ET/CST 输入
- 修改 get_monthly_stats() 使用 CONVERT_TZ 按 CST 月份统计
- 修改 get_daily_stats() 使用 CONVERT_TZ 按 CST 日期分组
- 修改 get_hourly_details() 按 CST 日期筛选和小时排序
- 交易记录使用 source_tz='CST' 避免重复转换
- 前端表头添加'(北京)'时区标注
2026-02-07 18:14:37 +08:00
crypto_predictor 6182630203 docs: 更新 AGENTS.md 开发指南,反映最新的架构变更 2026-02-07 16:58:44 +08:00
3 changed files with 87 additions and 107 deletions

No files matched your search

+43 -81
View File
@@ -8,104 +8,66 @@
* **语言要求**:所有回答必须使用**中文**。
* **称呼要求**:每次回答必须加上**“主人”**。
* **最小改动 (Minimal Change)**:只修改完成任务所必须的代码。**禁止**为了“最佳实践”或“代码美观”而进行未被请求的重构。
* **环境约束**:本项目严格使用 **Conda** 环境。禁止使用 venv/poetry/pipenv。
* **复用优先**:在编写新功能前,必须检查 `data/`, `models/`, `utils/` 下是否已有类似实现。
* **环境约束**:本项目严格使用 **Conda** 环境 (`crypto_pred_312`)。禁止使用 venv/poetry/pipenv。
* **复用优先**:在编写新功能前,必须检查 `data/`, `models/`, `api/` 下是否已有类似实现。
## 2. 环境与构建 (Environment & Build)
本项目依赖管理基于 `requirements.txt`, conda环境名称**crypto_pred_312**
* **Python 环境**: `conda activate crypto_pred_312`
* **依赖安装**: `pip install -r requirements.txt`
* **数据库**:
* **仅支持 MySQL**。禁止引入 SQLite 代码。
* 配置:通过 `.env` 文件配置 `MYSQL_HOST`, `MYSQL_USER` 等参数。
* 连接管理:使用 `data/database.py` 中的 `DatabaseManager` 或 `api/models.py` 中的 `get_db_connection`。
* **安装依赖**:
```bash
pip install -r requirements.txt
```
## 3. 项目结构说明 (Project Structure)
## 3. 测试与质量保证 (Testing & QA)
* **`api/`**: 核心服务层
* `main.py`: FastAPI 主应用入口。
* `models.py`: 数据库模型与操作(MySQL),包含时区转换逻辑。
* `prediction_service.py`: 预测业务逻辑。
* `static/`: Vue 3 前端静态资源(Dashboard)。
* **`data/`**: 数据层
* `database.py`: 市场数据 (OHLCV) 数据库管理。
* `trading_db.py`: 交易历史数据库管理。
* **`models/`**: 机器学习模型定义 (DLinear, TTM-R2 等) 及训练产物。
* **`scripts/`**: 辅助工具脚本。
* **根目录**:
* `start_system.py`: **系统统一启动脚本**(同时启动 API 和自动交易)。
* `auto_trade_polymarket.py`: 自动交易主程序。
使用 `pytest` 作为测试框架。在提交修改前,必须确保相关测试通过。
## 4. 开发规范 (Development Standards)
* **运行所有测试**:
```bash
pytest
```
### 4.1 时区处理 (Timezone)
* **存储**: 数据库中时间戳通常为 **UTC** 或 **ET (美东时间)**。
* **展示**: 前端及日志输出必须转换为 **CST (北京时间)**。
* **工具**: 使用 `api.models.convert_to_cst` 进行转换。
* **运行单个测试文件 (常用)**:
```bash
pytest tests/test_example.py
```
### 4.2 代码风格
* **注释**: 必须使用**中文**解释“为什么这样做”。
* **命名**: 变量/函数 `snake_case`,类名 `PascalCase`。
* **类型提示**: 推荐在关键接口使用 Type Hints。
* **运行单个测试用例 (推荐)**:
用于专注调试某一个功能:
```bash
pytest tests/test_example.py::test_specific_function
```
### 4.3 数据库操作
* **禁止**使用 SQLite。
* 使用 `pymysql` 或 `sqlalchemy` 进行操作。
* 确保所有数据库连接都使用上下文管理器 (`with` 语句) 以自动关闭。
* **显示打印输出 (Debug模式)**:
```bash
pytest -s tests/test_example.py
```
## 5. 测试与验证 (Testing & QA)
* **代码格式化与检查**:
```bash
black . # 格式化代码
flake8 . # 代码风格检查
```
* **运行测试**: `pytest`
* **代码检查**: `flake8 .`
* **本地验证**:
* 修改 API 或前端后,运行 `./start_system.py` 并访问 `http://localhost:8000` 验证。
* 修改模型后,运行 `python ensemble_predict.py` 验证预测逻辑。
## 4. 代码风格规范 (Code Style)
* **注释 (Comments)**:
* **必须使用中文**编写所有注释和文档字符串 (Docstrings)。
* Docstring 风格推荐 Google Style 或简单的多行注释。
* 注释应解释“为什么这样做” (Why),而不是“做了什么” (What)。
* **命名约定 (Naming)**:
* 变量/函数: `snake_case` (如 `add_technical_features`)
* 类名: `PascalCase` (如 `BinanceDataFetcher`)
* 常量: `UPPER_CASE` (如 `MAX_RETRIES`)
* 私有成员: `_snake_case` (前缀下划线)
* **导入 (Imports)**:
* 顺序:标准库 -> 第三方库 -> 本地模块。
* 尽量使用绝对导入。
* 注意:项目中部分脚本使用了 `sys.path.insert` hack,修改时需保持兼容,不要随意删除。
* **类型提示 (Type Hints)**:
* 推荐在函数签名中添加类型提示,但不强制要求全覆盖。
* 示例: `def process_data(df: pd.DataFrame) -> np.ndarray:`
* **错误处理 (Error Handling)**:
* 使用具体的异常类型 (如 `ValueError`, `FileNotFoundError`),避免裸 `except:`。
* 在 API 调用或网络请求处必须添加重试或错误捕获逻辑。
## 5. 项目结构说明 (Project Structure)
在修改代码前,请参考以下结构定位文件:
* **`api/`**: FastAPI 接口服务代码。
* **`data/`**: 数据获取 (BinanceFetcher等) 和预处理逻辑。
* **`models/`**: 模型定义 (N-BEATS, DLinear, CatBoost 等) 和 训练好的模型文件。
* **`tests/`**: 单元测试和集成测试。
* **`scripts/`**: 辅助脚本。
* **`training/`** & **`logs/`**: 训练日志和过程文件。
* **根目录脚本**: 包含多个入口脚本 (如 `ensemble_predict.py`, `train_catboost.py`),修改时需格外小心,避免破坏生产流程。
## 6. 开发流程 (Workflow)
1. **理解需求**: 先阅读代码,确认需求范围。如果有歧义,**先询问主人**。
2. **定位代码**: 使用 `grep` 或 `glob` 查找相关文件。
3. **制定计划**: 思考修改方案,优先考虑不破坏现有逻辑的“加法”修改。
4. **实施修改**: 编辑代码,保持风格一致。
5. **验证**:
* 运行 `flake8` 检查语法错误。
* 运行 `pytest` 验证逻辑。
* 如果是脚本,尝试带 `--help` 或测试参数运行以确保无运行时错误。
## 7. 禁止事项 (Constraints)
## 6. 禁止事项 (Constraints)
* ❌ **禁止**猜测业务逻辑。
* ❌ **禁止**删除看似无用但可能被外部脚本调用的代码。
* ❌ **禁止**在未确认的情况下升级核心库版本 (如 torch, pandas)。
* ❌ **禁止**输出英语回复 (代码变量名除外)。
* ❌ **禁止**恢复已删除的 SQLite 相关代码。
---
*遵循此指南,确保高效、安全地协助主人完成任务。*
+42 -24
View File
@@ -17,12 +17,13 @@ TZ_ET = ZoneInfo('America/New_York') # 美东时间 (Polymarket)
TZ_CST = ZoneInfo('Asia/Shanghai') # 中国标准时间 (前端显示)
def convert_to_cst(timestamp_val) -> str:
def convert_to_cst(timestamp_val, source_tz: str = 'UTC') -> str:
"""
将时间戳转换为 CST 时间字符串
Args:
timestamp_val: datetime 对象或 ISO 格式时间戳字符串 (假设是 UTC)
timestamp_val: datetime 对象或 ISO 格式时间戳字符串
source_tz: 输入时区 - 'UTC'(默认,币安数据), 'ET'(Polymarket), 'CST'(已是北京时间)
Returns:
CST 时间字符串 (YYYY-MM-DD HH:MM:SS)
@@ -37,9 +38,19 @@ def convert_to_cst(timestamp_val) -> str:
# 字符串格式
dt = datetime.fromisoformat(str(timestamp_val).replace('Z', '+00:00'))
# 如果没有时区信息,假设是 UTC
# 根据来源时区设置时区信息
if dt.tzinfo is None:
dt = dt.replace(tzinfo=TZ_UTC)
if source_tz == 'ET':
dt = dt.replace(tzinfo=TZ_ET)
elif source_tz == 'CST':
# 已经是 CST,直接格式化返回
return dt.strftime('%Y-%m-%d %H:%M:%S')
else: # 默认 UTC
dt = dt.replace(tzinfo=TZ_UTC)
# 如果来源已经是 CST,直接格式化
if source_tz == 'CST':
return dt.strftime('%Y-%m-%d %H:%M:%S')
# 转换为 CST
dt_cst = dt.astimezone(TZ_CST)
@@ -122,7 +133,8 @@ class TradeModel:
row = cursor.fetchone()
result = _dict_from_row(row)
if result and result.get('timestamp'):
result['timestamp_cst'] = convert_to_cst(result['timestamp'])
# 交易记录时间已经是 CST,直接格式化
result['timestamp_cst'] = convert_to_cst(result['timestamp'], source_tz='CST')
return result
@staticmethod
@@ -173,7 +185,8 @@ class TradeModel:
for row in rows:
item = _dict_from_row(row)
if item and item.get('timestamp'):
item['timestamp_cst'] = convert_to_cst(item['timestamp'])
# 交易记录时间已经是 CST,直接格式化
item['timestamp_cst'] = convert_to_cst(item['timestamp'], source_tz='CST')
data_list.append(item)
return {
@@ -292,7 +305,7 @@ class PredictionModel:
SUM(CASE WHEN is_correct = 1 THEN 1 ELSE 0 END) as correct,
SUM(CASE WHEN is_correct = 0 THEN 1 ELSE 0 END) as incorrect
FROM model_predictions
WHERE DATE_FORMAT(date, '%%Y-%%m') = %s
WHERE DATE_FORMAT(CONVERT_TZ(timestamp, '+00:00', '+08:00'), '%%Y-%%m') = %s
AND actual_result IS NOT NULL
GROUP BY model_name
"""
@@ -333,16 +346,16 @@ class PredictionModel:
sql = """
SELECT
date,
DATE(CONVERT_TZ(timestamp, '+00:00', '+08:00')) as date_cst,
model_name,
COUNT(*) as total,
SUM(CASE WHEN is_correct = 1 THEN 1 ELSE 0 END) as correct,
SUM(CASE WHEN is_correct = 0 THEN 1 ELSE 0 END) as incorrect
FROM model_predictions
WHERE DATE_FORMAT(date, '%%Y-%%m') = %s
WHERE DATE_FORMAT(CONVERT_TZ(timestamp, '+00:00', '+08:00'), '%%Y-%%m') = %s
AND actual_result IS NOT NULL
GROUP BY date, model_name
ORDER BY date DESC
GROUP BY date_cst, model_name
ORDER BY date_cst DESC
"""
cursor.execute(sql, (month,))
@@ -352,7 +365,7 @@ class PredictionModel:
daily_data = {}
for row in rows:
row_dict = _dict_from_row(row)
date = row_dict['date']
date = str(row_dict['date_cst']) # CST 日期
if date not in daily_data:
daily_data[date] = {"date": date}
@@ -391,28 +404,33 @@ class PredictionModel:
获取指定日期的每小时详情
Args:
date: 日期 (YYYY-MM-DD)
date: 日期 (YYYY-MM-DD, CST 北京时间)
"""
with get_db_connection() as conn:
cursor = conn.cursor()
# 按 CST 日期筛选,并获取 CST 小时
cursor.execute("""
SELECT * FROM model_predictions
WHERE date = %s
ORDER BY hour, model_name
SELECT
*,
HOUR(CONVERT_TZ(timestamp, '+00:00', '+08:00')) as hour_cst,
CONVERT_TZ(timestamp, '+00:00', '+08:00') as timestamp_cst_dt
FROM model_predictions
WHERE DATE(CONVERT_TZ(timestamp, '+00:00', '+08:00')) = %s
ORDER BY hour_cst, model_name
""", (date,))
rows = cursor.fetchall()
# 按小时重组数据
# 按 CST 小时重组数据
hourly_data = {}
for row in rows:
row_dict = _dict_from_row(row)
hour = row_dict['hour']
if hour not in hourly_data:
# 将 timestamp 转换为 CST 时间
hour_cst = row_dict['hour_cst'] # 使用 CST 小时
if hour_cst not in hourly_data:
# 将 timestamp 转换为 CST 时间字符串
timestamp_cst = convert_to_cst(row_dict.get('timestamp', ''))
hourly_data[hour] = {
"hour": hour,
hourly_data[hour_cst] = {
"hour": hour_cst, # 返回 CST 小时
"timestamp_et": row_dict['timestamp_et'],
"timestamp_cst": timestamp_cst,
"open_price": row_dict['open_price'],
@@ -422,13 +440,13 @@ class PredictionModel:
}
model = row_dict['model_name']
hourly_data[hour]["models"][model] = {
hourly_data[hour_cst]["models"][model] = {
"prediction": row_dict['prediction'],
"probability": row_dict['probability'],
"is_correct": row_dict['is_correct']
}
# 按小时排序返回
# 按 CST 小时排序返回
return [hourly_data[h] for h in sorted(hourly_data.keys())]
@staticmethod
+2 -2
View File
@@ -259,7 +259,7 @@
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>日期</th>
<th>日期 (北京)</th>
<th>DLinear 预测</th>
<th>DLinear 准确率</th>
<th>TTM-R2 预测</th>
@@ -341,7 +341,7 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="bi bi-clock"></i> {{ hourlyModal.date }} 每小时预测详情
<i class="bi bi-clock"></i> {{ hourlyModal.date }} 每小时预测详情 (北京时间)
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>