Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4bd5cb44e4 | ||
|
|
bd722242ad | ||
|
|
db937fdcf9 |
No files matched your search
@@ -0,0 +1,111 @@
|
|||||||
|
# AGENTS.md - 智能代理开发指南
|
||||||
|
|
||||||
|
本文件定义了在该项目中进行开发的 AI 代理(Agent)的行为准则、技术规范和操作流程。请严格遵守。
|
||||||
|
|
||||||
|
## 1. 核心行为准则 (Core Mandates)
|
||||||
|
|
||||||
|
**最高优先级原则:**
|
||||||
|
* **语言要求**:所有回答必须使用**中文**。
|
||||||
|
* **称呼要求**:每次回答必须加上**“主人”**。
|
||||||
|
* **最小改动 (Minimal Change)**:只修改完成任务所必须的代码。**禁止**为了“最佳实践”或“代码美观”而进行未被请求的重构。
|
||||||
|
* **环境约束**:本项目严格使用 **Conda** 环境。禁止使用 venv/poetry/pipenv。
|
||||||
|
* **复用优先**:在编写新功能前,必须检查 `data/`, `models/`, `utils/` 下是否已有类似实现。
|
||||||
|
|
||||||
|
## 2. 环境与构建 (Environment & Build)
|
||||||
|
|
||||||
|
本项目依赖管理基于 `requirements.txt`, conda环境名称**crypto_pred_312**
|
||||||
|
|
||||||
|
* **安装依赖**:
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 测试与质量保证 (Testing & QA)
|
||||||
|
|
||||||
|
使用 `pytest` 作为测试框架。在提交修改前,必须确保相关测试通过。
|
||||||
|
|
||||||
|
* **运行所有测试**:
|
||||||
|
```bash
|
||||||
|
pytest
|
||||||
|
```
|
||||||
|
|
||||||
|
* **运行单个测试文件 (常用)**:
|
||||||
|
```bash
|
||||||
|
pytest tests/test_example.py
|
||||||
|
```
|
||||||
|
|
||||||
|
* **运行单个测试用例 (推荐)**:
|
||||||
|
用于专注调试某一个功能:
|
||||||
|
```bash
|
||||||
|
pytest tests/test_example.py::test_specific_function
|
||||||
|
```
|
||||||
|
|
||||||
|
* **显示打印输出 (Debug模式)**:
|
||||||
|
```bash
|
||||||
|
pytest -s tests/test_example.py
|
||||||
|
```
|
||||||
|
|
||||||
|
* **代码格式化与检查**:
|
||||||
|
```bash
|
||||||
|
black . # 格式化代码
|
||||||
|
flake8 . # 代码风格检查
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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)
|
||||||
|
|
||||||
|
* ❌ **禁止**猜测业务逻辑。
|
||||||
|
* ❌ **禁止**删除看似无用但可能被外部脚本调用的代码。
|
||||||
|
* ❌ **禁止**在未确认的情况下升级核心库版本 (如 torch, pandas)。
|
||||||
|
* ❌ **禁止**输出英语回复 (代码变量名除外)。
|
||||||
|
|
||||||
|
---
|
||||||
|
*遵循此指南,确保高效、安全地协助主人完成任务。*
|
||||||
+106
-10
@@ -34,6 +34,7 @@ import httpx
|
|||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
from data.fetcher import BinanceDataFetcher
|
from data.fetcher import BinanceDataFetcher
|
||||||
|
from data.trading_db import TradingDatabase
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
@@ -186,6 +187,9 @@ class PolymarketBTCTrader:
|
|||||||
# 数据获取器
|
# 数据获取器
|
||||||
self.fetcher = BinanceDataFetcher()
|
self.fetcher = BinanceDataFetcher()
|
||||||
|
|
||||||
|
# 数据库
|
||||||
|
self.db = TradingDatabase()
|
||||||
|
|
||||||
# CLOB客户端(用于下单)
|
# CLOB客户端(用于下单)
|
||||||
self.clob_client = None
|
self.clob_client = None
|
||||||
if self.private_key and not self.dry_run:
|
if self.private_key and not self.dry_run:
|
||||||
@@ -229,6 +233,42 @@ class PolymarketBTCTrader:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"⚠️ CLOB客户端初始化失败: {e}")
|
logger.warning(f"⚠️ CLOB客户端初始化失败: {e}")
|
||||||
self.clob_client = None
|
self.clob_client = None
|
||||||
|
|
||||||
|
def _log_to_db(self, result: Dict, market: Optional[Dict]):
|
||||||
|
"""记录交易结果到数据库"""
|
||||||
|
try:
|
||||||
|
# 确定状态
|
||||||
|
status = 'failure'
|
||||||
|
if self.dry_run:
|
||||||
|
status = 'test'
|
||||||
|
elif result.get('success'):
|
||||||
|
status = 'success'
|
||||||
|
elif result.get('error'):
|
||||||
|
status = 'failure'
|
||||||
|
elif result.get('prediction'): # 有预测但未交易(如市场未找到)
|
||||||
|
status = 'in_progress' # 或者 failure? 未找到市场算 failure
|
||||||
|
if result.get('error'):
|
||||||
|
status = 'failure'
|
||||||
|
|
||||||
|
log_data = {
|
||||||
|
'timestamp': result.get('timestamp'),
|
||||||
|
'market_title': result.get('market'),
|
||||||
|
'market_slug': market.get('slug') if market else result.get('market_slug'),
|
||||||
|
'model_name': 'DLinear',
|
||||||
|
'prediction': result.get('prediction'),
|
||||||
|
'probability': result.get('probability'),
|
||||||
|
'action': result.get('side'),
|
||||||
|
'price': result.get('price'),
|
||||||
|
'amount': result.get('amount'),
|
||||||
|
'size': result.get('size'),
|
||||||
|
'status': status,
|
||||||
|
'error_message': result.get('error'),
|
||||||
|
'raw_data': result
|
||||||
|
}
|
||||||
|
self.db.log_trade(log_data)
|
||||||
|
logger.info(f"💾 交易记录已保存到数据库")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ 保存数据库失败: {e}")
|
||||||
|
|
||||||
def generate_market_slug(self, target_time: Optional[datetime] = None) -> str:
|
def generate_market_slug(self, target_time: Optional[datetime] = None) -> str:
|
||||||
"""
|
"""
|
||||||
@@ -598,7 +638,12 @@ class PolymarketBTCTrader:
|
|||||||
|
|
||||||
return prediction, probability
|
return prediction, probability
|
||||||
|
|
||||||
def run_at_hour_start(self, delay_seconds: float = 2.0) -> Dict:
|
def run_once(self) -> Dict:
|
||||||
|
"""运行一次交易流程(不等待整点)"""
|
||||||
|
logger.info("运行单次交易...")
|
||||||
|
return self.run_at_hour_start(delay_seconds=0)
|
||||||
|
|
||||||
|
def run_at_hour_start(self, delay_seconds: float = 2.0, timeout_minutes: int = 50) -> Dict:
|
||||||
"""
|
"""
|
||||||
整点时执行的交易流程
|
整点时执行的交易流程
|
||||||
|
|
||||||
@@ -606,11 +651,13 @@ class PolymarketBTCTrader:
|
|||||||
2. 获取最新K线
|
2. 获取最新K线
|
||||||
3. DLinear预测当前整点的收盘涨跌
|
3. DLinear预测当前整点的收盘涨跌
|
||||||
4. 获取对应的Polymarket市场
|
4. 获取对应的Polymarket市场
|
||||||
5. 检查价格是否≤max_price
|
5. 循环检查价格:
|
||||||
6. 下单
|
- 如果价格 <= max_price: 下单并结束
|
||||||
|
- 如果价格 > max_price: 等待并重试(直到超时)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
delay_seconds: 整点后延迟秒数
|
delay_seconds: 整点后延迟秒数
|
||||||
|
timeout_minutes: 价格等待超时时间(分钟)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
执行结果
|
执行结果
|
||||||
@@ -621,6 +668,7 @@ class PolymarketBTCTrader:
|
|||||||
logger.info(f" 模式: {'🔸 模拟' if self.dry_run else '💰 真实交易'}")
|
logger.info(f" 模式: {'🔸 模拟' if self.dry_run else '💰 真实交易'}")
|
||||||
logger.info(f" 最大下注: ${self.max_bet:.2f} USDC")
|
logger.info(f" 最大下注: ${self.max_bet:.2f} USDC")
|
||||||
logger.info(f" 最高价格: ${self.max_price:.2f}")
|
logger.info(f" 最高价格: ${self.max_price:.2f}")
|
||||||
|
logger.info(f" 等待超时: {timeout_minutes}分钟")
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
'timestamp': datetime.now().isoformat(),
|
'timestamp': datetime.now().isoformat(),
|
||||||
@@ -633,24 +681,75 @@ class PolymarketBTCTrader:
|
|||||||
result['prediction'] = 'UP' if prediction == 1 else 'DOWN'
|
result['prediction'] = 'UP' if prediction == 1 else 'DOWN'
|
||||||
result['probability'] = probability if prediction == 1 else 1 - probability
|
result['probability'] = probability if prediction == 1 else 1 - probability
|
||||||
|
|
||||||
# 2. 获取当前整点的市场(不是下一个整点)
|
# 2. 获取初始市场
|
||||||
market = self.get_current_hourly_market()
|
market = self.get_current_hourly_market()
|
||||||
if not market:
|
if not market:
|
||||||
result['error'] = '未找到市场'
|
result['error'] = '未找到市场'
|
||||||
logger.error(f"❌ {result['error']}")
|
logger.error(f"❌ {result['error']}")
|
||||||
|
self._log_to_db(result, None)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
result['market'] = market['title']
|
result['market'] = market['title']
|
||||||
|
result['market_slug'] = market['slug']
|
||||||
|
|
||||||
if market['closed']:
|
if market['closed']:
|
||||||
result['error'] = '市场已关闭'
|
result['error'] = '市场已关闭'
|
||||||
logger.error(f"❌ {result['error']}")
|
logger.error(f"❌ {result['error']}")
|
||||||
|
self._log_to_db(result, market)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
# 检查是否已交易过该市场
|
||||||
|
check_status = 'test' if self.dry_run else 'success'
|
||||||
|
if self.db.check_existing_trade(market['slug'], status=check_status):
|
||||||
|
result['error'] = '该市场已存在成功交易记录'
|
||||||
|
logger.warning(f"⚠️ {result['error']}, 跳过交易")
|
||||||
|
return result
|
||||||
|
|
||||||
|
# 3. 循环检查价格并交易
|
||||||
|
start_time = datetime.now()
|
||||||
|
end_time = start_time + timedelta(minutes=timeout_minutes)
|
||||||
|
|
||||||
# 3. 执行交易
|
logger.info(f"🔄 开始价格监控 (预测: {'UP' if prediction == 1 else 'DOWN'})...")
|
||||||
trade_result = self.execute_trade(prediction, market)
|
|
||||||
result.update(trade_result)
|
|
||||||
|
|
||||||
|
while datetime.now() < end_time:
|
||||||
|
# 刷新价格
|
||||||
|
market = self.refresh_prices()
|
||||||
|
if not market:
|
||||||
|
logger.warning("无法获取市场价格,重试中...")
|
||||||
|
time.sleep(10)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 检查价格
|
||||||
|
price_idx = 0 if prediction == 1 else 1
|
||||||
|
current_price = market['outcome_prices'][price_idx]
|
||||||
|
|
||||||
|
logger.info(f" 当前价格: ${current_price:.3f} (目标: <= ${self.max_price:.2f})")
|
||||||
|
|
||||||
|
if current_price <= self.max_price:
|
||||||
|
# 价格符合,执行交易
|
||||||
|
logger.info("✅ 价格满足条件,正在下单...")
|
||||||
|
trade_result = self.execute_trade(prediction, market)
|
||||||
|
result.update(trade_result)
|
||||||
|
|
||||||
|
# 记录数据库
|
||||||
|
self._log_to_db(result, market)
|
||||||
|
|
||||||
|
logger.info("=" * 60)
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
# 价格过高,等待
|
||||||
|
wait_time = 30 # 等待30秒
|
||||||
|
elapsed = int((datetime.now() - start_time).total_seconds())
|
||||||
|
logger.info(f"⏳ 价格过高,继续等待... (已耗时: {elapsed}s)")
|
||||||
|
time.sleep(wait_time)
|
||||||
|
|
||||||
|
# 超时
|
||||||
|
result['error'] = f'等待价格超时 ({timeout_minutes}分钟)'
|
||||||
|
result['price'] = market['outcome_prices'][0] if prediction == 1 else market['outcome_prices'][1]
|
||||||
|
result['side'] = 'UP' if prediction == 1 else 'DOWN'
|
||||||
|
logger.error(f"❌ {result['error']}")
|
||||||
|
|
||||||
|
self._log_to_db(result, market)
|
||||||
logger.info("=" * 60)
|
logger.info("=" * 60)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
@@ -758,9 +857,6 @@ class PolymarketBTCTrader:
|
|||||||
# 启动时进行一次预测(不下单,仅显示)
|
# 启动时进行一次预测(不下单,仅显示)
|
||||||
self._show_initial_prediction()
|
self._show_initial_prediction()
|
||||||
|
|
||||||
# 定时刷新价格
|
|
||||||
schedule.every(interval_minutes).minutes.do(self.refresh_prices)
|
|
||||||
|
|
||||||
# 每小时整点后执行交易
|
# 每小时整点后执行交易
|
||||||
def hourly_trade():
|
def hourly_trade():
|
||||||
self.run_at_hour_start(delay_seconds=delay_seconds)
|
self.run_at_hour_start(delay_seconds=delay_seconds)
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,109 @@
|
|||||||
|
import sqlite3
|
||||||
|
from datetime import datetime
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
class TradingDatabase:
|
||||||
|
"""交易数据库管理类 - 用于记录交易历史和预测结果"""
|
||||||
|
|
||||||
|
def __init__(self, db_path="trading_history.db"):
|
||||||
|
self.db_path = db_path
|
||||||
|
self._init_db()
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def get_connection(self):
|
||||||
|
"""数据库连接上下文管理器"""
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
try:
|
||||||
|
yield conn
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def _init_db(self):
|
||||||
|
"""初始化数据库表结构"""
|
||||||
|
with self.get_connection() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
# 创建交易记录表
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS trades (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
timestamp TEXT,
|
||||||
|
market_title TEXT,
|
||||||
|
market_slug TEXT,
|
||||||
|
model_name TEXT,
|
||||||
|
prediction TEXT,
|
||||||
|
probability REAL,
|
||||||
|
action TEXT,
|
||||||
|
price REAL,
|
||||||
|
amount REAL,
|
||||||
|
size REAL,
|
||||||
|
status TEXT,
|
||||||
|
error_message TEXT,
|
||||||
|
raw_data TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
def log_trade(self, trade_data: dict):
|
||||||
|
"""
|
||||||
|
记录交易信息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
trade_data: 包含交易信息的字典,支持字段:
|
||||||
|
timestamp, market_title, market_slug, model_name,
|
||||||
|
prediction, probability, action, price, amount, size,
|
||||||
|
status, error_message, raw_data
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with self.get_connection() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO trades (
|
||||||
|
timestamp, market_title, market_slug, model_name,
|
||||||
|
prediction, probability, action, price, amount, size,
|
||||||
|
status, error_message, raw_data
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
''', (
|
||||||
|
trade_data.get('timestamp', datetime.now().isoformat()),
|
||||||
|
trade_data.get('market_title'),
|
||||||
|
trade_data.get('market_slug'),
|
||||||
|
trade_data.get('model_name'),
|
||||||
|
trade_data.get('prediction'),
|
||||||
|
trade_data.get('probability'),
|
||||||
|
trade_data.get('action'),
|
||||||
|
trade_data.get('price'),
|
||||||
|
trade_data.get('amount'),
|
||||||
|
trade_data.get('size'),
|
||||||
|
trade_data.get('status'),
|
||||||
|
trade_data.get('error_message'),
|
||||||
|
json.dumps(trade_data.get('raw_data', {}), ensure_ascii=False)
|
||||||
|
))
|
||||||
|
conn.commit()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"写入数据库失败: {e}")
|
||||||
|
|
||||||
|
def check_existing_trade(self, market_slug: str, status: str = 'success') -> bool:
|
||||||
|
"""
|
||||||
|
检查指定市场是否已有成功交易记录
|
||||||
|
|
||||||
|
Args:
|
||||||
|
market_slug: 市场slug
|
||||||
|
status: 交易状态 (默认 'success', 也可以是 'test')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True 如果已有成功交易, 否则 False
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with self.get_connection() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('''
|
||||||
|
SELECT COUNT(*) FROM trades
|
||||||
|
WHERE market_slug = ? AND status = ?
|
||||||
|
''', (market_slug, status))
|
||||||
|
count = cursor.fetchone()[0]
|
||||||
|
return count > 0
|
||||||
|
except Exception as e:
|
||||||
|
print(f"检查已存在交易失败: {e}")
|
||||||
|
return False
|
||||||
Binary file not shown.
Reference in new issue
Block a user