Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ ENV/

# UV
.uv/
uv.lock

# IDE
.vscode/
Expand Down
28 changes: 25 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,10 @@ uv run python backtest_comparison.py --symbol BTC --compare all
uv run python backtest_comparison.py --symbol BTC --compare debate
uv run python backtest_comparison.py --symbol BTC --compare regime

# 分别查看各程序日志(日志文件通过 tee 写入 logs/ 目录)
tail -f logs/main.log # 主交易日志
tail -f logs/grid.log # 网格交易日志
# 查看日志(统一入口后所有运行模式均写 main.log;grid.log 为历史遗留文件已停更)
tail -f logs/main.log # 主交易/网格日志(RUN_MODE=main/grid/all 均在此)
tail -f logs/trades/trades_$(date +%Y%m%d).jsonl # 结构化交易记录(含 pnl/reason 归因)
tail -f logs/equity/equity_$(date +%Y%m%d).jsonl # 净值快照(需开启 grid_equity_snapshot_enabled)
```

## 核心架构
Expand Down Expand Up @@ -628,13 +629,17 @@ regime_adaptive:
protections:
- name: max_drawdown
max_drawdown_pct: 0.10 # 最大回撤 10%
min_drawdown_usd: 0 # 回撤绝对额下限(>0 时须同时达标才触发;防小账户上
# 纯百分比线成为噪声级触发器,默认 0=关闭)
pause_hours: 4
- name: daily_loss
max_daily_loss_pct: 0.05 # 单日亏损 5%
pause_hours: 4
- name: consecutive_loss
max_consecutive_losses: 5
per_symbol: true # true=只锁该交易对
forced_close_no_reset: false # true=风控强平的净盈利不重置连亏计数(只有主动止盈
# 才算打破连亏;默认 false=历史行为)
pause_hours: 4
- name: position_timeout
max_position_hours: 48
Expand Down Expand Up @@ -683,6 +688,23 @@ trading:
grid_limit_order_stop_loss_enabled: true # 网格成交后是否补止损单
grid_reduce_only_exit_orders_enabled: true # 是否启用分层减仓单

# ── 网格防护体系(全部默认关闭 = 历史行为)────────────────────────────
grid_force_neutral_mode: true # 强制中性网格(忽略 AI 方向)
grid_max_position_notional_usd: 40 # 库存硬上限(USD 净持仓名义额,0=关闭)
grid_inventory_cap_strict: true # 严格模式:名义额计入同向挂单 + 取价失败 fail-closed
grid_trend_filter_enabled: true # 多周期强势一致时暂停加仓
grid_trend_filter_min_votes: 3 # 强势周期票数阈值
grid_trend_filter_timeframes: ["15m","1h","4h","1d"] # 计票周期白名单(排除 1m 噪声;缺省=全部)
grid_trend_filter_confirm_cycles: 2 # 连续 N 周期同向确认才生效(迟滞去抖,缺省 1=立即)
grid_trend_filter_flatten_adverse: true # 强趋势里减掉逆势库存
grid_trend_filter_flatten_min_cycles: 3 # 平逆势库存需更多连续确认(暂停先行、平仓靠后)
grid_trend_filter_surgical_flatten: true # 手术式减仓:只平超限逆势层级(缺省 false=全量拆网)
grid_keep_grid_reconcile: true # KEEP_GRID 周期对账撤无主残单
grid_adaptive_sizing_enabled: true # 单格金额与净值挂钩:钱不够减格数而非抬单格金额
grid_min_grid_num: 3 # 自适应仓位最少格数(低于则 INSUFFICIENT_CAPITAL 拒绝布单)
grid_halt_below_usd: 15 # 净值停机线:低于此值且无持仓时跳过整个周期(0=关闭)
grid_equity_snapshot_enabled: true # 每周期净值快照写 logs/equity/*.jsonl

agent:
grid_width:
min_pct: 0.02 # 最小网格宽度 2%
Expand Down
8 changes: 5 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
COPY --from=ghcr.io/astral-sh/uv:0.10.5 /uv /uvx /bin/

# Copy dependency files
COPY pyproject.toml .python-version README.md ./
COPY pyproject.toml uv.lock .python-version README.md ./

# Install Python dependencies via uv(不使用 lock 文件,因本地 uv 版本较旧)
RUN uv sync --no-dev --no-install-project
# Install Python dependencies via uv(--frozen 按 lock 文件精确复现依赖版本,
# 防止镜像构建时解析到不兼容的新版依赖——曾因未锁定装到删除了
# pydantic_ai.models.gemini 的新版 pydantic-ai 导致容器循环崩溃)
RUN uv sync --frozen --no-dev --no-install-project

# Final stage
FROM python:3.12-slim
Expand Down
185 changes: 175 additions & 10 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from src.agent.summary_agent_v2 import DecisionHistory, SummaryAgentV2
from src.config import DEFAULT_PERP_FEE_RATES, get_config
from src.data.data_enricher import MarketDataEnricher
from src.data.indicators import TechnicalIndicators
from src.data.indicators import TechnicalIndicators, TrendConfirmTracker, detect_strong_trend
from src.data.market_data import MarketDataFetcher
from src.data.market_monitor import MarketMonitor, MonitorConfig, VolatilityAlert
from src.llm import LLMClientManager
Expand Down Expand Up @@ -481,6 +481,9 @@ def _initialize_components(self):
# 13. 初始化网格交易组件
self.grid_manager = None
self.grid_agent = None
# 趋势过滤迟滞确认器(按 symbol 独立):连续 N 周期同向强趋势才动作。
# 无条件初始化,避免网格组件构造失败时 grid_cycle 引用缺失属性。
self._grid_trend_trackers: dict[str, TrendConfirmTracker] = {}
if self.config.grid_enabled:
self.logger.print_info("初始化网格交易组件...")
try:
Expand All @@ -496,6 +499,11 @@ def _initialize_components(self):
grid_limit_order_stop_loss_enabled=self.config.grid_limit_order_stop_loss_enabled,
grid_reduce_only_exit_orders_enabled=self.config.grid_reduce_only_exit_orders_enabled,
barrier_config=barrier_config,
on_round_trip_close=self._on_grid_round_trip_close,
max_position_notional_usd=self.config.grid_max_position_notional_usd,
trend_flatten_surgical=self.config.grid_trend_filter_surgical_flatten,
inventory_cap_strict=self.config.grid_inventory_cap_strict,
keep_grid_reconcile=self.config.grid_keep_grid_reconcile,
)

self.grid_agent = GridAgent(
Expand All @@ -509,6 +517,9 @@ def _initialize_components(self):
width_pct_fallback=self.config.grid_width_fallback_pct,
ai_width_blend_weight=self.config.grid_ai_blend_weight,
force_neutral_mode=self.config.grid_force_neutral_mode,
max_leverage=self.config.max_leverage,
adaptive_sizing=self.config.grid_adaptive_sizing_enabled,
min_grid_num=self.config.grid_min_grid_num,
)
self.logger.print_info("✅ 网格交易组件初始化完成")
except Exception as e:
Expand Down Expand Up @@ -1622,13 +1633,51 @@ def _send_shutdown_notification(self, reason: str = "正常关闭"):
except Exception as e:
self.logger.print_error(f"发送关闭通知失败: {e}")

def _on_grid_round_trip_close(self, symbol: str, pnl: float, forced: bool = False) -> None:
"""网格每轮 round-trip 平仓回调:把逐轮盈亏喂给连亏熔断插件。

连亏保护(``consecutive_loss``)完全由 ``on_trade_close`` 事件驱动;网格此前
从不上报逐笔盈亏,导致该插件在纯网格模式下形同虚设。此回调由 GridManager 在
``_record_round_trip``(forced=False,主动止盈)与紧急平仓/手术式减仓
(forced=True,风控强平)中触发,补齐这条数据通路。

注意:仅上报 ``on_trade_close``(盈亏事件),不上报 ``on_trade_open``——网格
持续滚动库存、没有干净的「单笔开仓」语义,position_timeout 不适用网格,故不接线。
"""
if not self.protection_manager:
return
try:
self.protection_manager.on_trade_close(symbol=symbol, pnl=float(pnl), forced=forced)
except Exception as e:
self.logger.print_warning(f"[网格风控] round-trip 盈亏上报失败: {e}")

def _detect_strong_trend(self, trends: dict[str, str]) -> int:
"""从多周期趋势判断是否存在「一致强势」趋势(委托纯函数,便于单测)。

计票范围受 ``grid_trend_filter_timeframes`` 白名单约束(None=全部周期,
历史行为);票数阈值 ``grid_trend_filter_min_votes``。

Returns:
+1=强势上涨, -1=强势下跌, 0=无一致强趋势。
"""
return detect_strong_trend(
trends,
min_votes=self.config.grid_trend_filter_min_votes,
allowed_timeframes=self.config.grid_trend_filter_timeframes,
)

def _grid_protection_triggered(self, symbol: str) -> bool:
"""
网格周期的账户级风控熔断检查。

网格此前仅有 per-grid Triple Barrier(单网格止盈止损),缺少账户级回撤 /
连亏 / 单日亏损熔断。此处复用永续同款 ``ProtectionManager.check_all``,在
布单前拦截:
网格风控分两条通路:
- 账户级(本方法,``ProtectionManager.check_all``,按权益快照判定):回撤
(max_drawdown)与单日亏损(daily_loss)熔断,布单前拦截。
- 逐轮级(``_on_grid_round_trip_close`` → ``on_trade_close``):连亏熔断
(consecutive_loss),由每轮 round-trip 盈亏驱动。
position_timeout 依赖逐笔开仓事件,不适用持续滚动库存的网格,故未接入。

本方法处理 check_all 的拦截动作:

- ``CLOSE_ALL_POSITIONS``:平掉全部持仓 + 撤销该 symbol 的网格挂单(含
trigger)并清理网格状态,跳过本轮布单。
Expand Down Expand Up @@ -1721,8 +1770,8 @@ def grid_cycle(self):
if cloud:
cloud.send_cycle_event(symbol=symbol, phase="start")

# 账户级风控熔断(回撤/连亏/单日亏损):网格此前只有 per-grid Triple Barrier,
# 此处复用永续同款 ProtectionManager,在布单前拦截。熔断时已平仓/撤单并跳过布单
# 账户级风控熔断(回撤/单日亏损,按权益判定):在布单前拦截,熔断时已平仓/撤单并跳过布单。
# 连亏熔断走另一条逐轮通路(_on_grid_round_trip_close → on_trade_close),不在此处
if self.protection_manager and self._grid_protection_triggered(symbol):
if cloud:
cloud.send_cycle_event(
Expand All @@ -1733,6 +1782,80 @@ def grid_cycle(self):
)
return

# 净值快照 + 停机线短路(均为可选开关,默认关闭保持历史行为)。
# 停机线:净值低于阈值且无持仓时跳过整个周期(不拉 5 档 K 线、不调 LLM)——
# 线上账户 $7.71 熔断后仍每 5 分钟烧一次 LLM 调用即源于缺此闸。
if self.config.grid_equity_snapshot_enabled or self.config.grid_halt_below_usd > 0:
try:
balance_info = self.order_manager.get_available_balance_info()
except Exception as e:
self.logger.print_warning(f"[Grid] 获取余额失败,跳过快照/停机检查: {e}")
balance_info = None
if balance_info and balance_info.get("status") == "ok":
equity = float(
balance_info.get("equity", balance_info.get("total", 0)) or 0
)
if self.config.grid_equity_snapshot_enabled:
# 持仓名义额单独取(balance_info 里没有此字段,此前一直记 0);
# 取失败不阻断快照,记 0 并由 uPnL 字段暴露持仓存在
position_notional = 0.0
try:
for pos in self.order_manager.get_current_positions() or []:
if pos.get("coin") == symbol:
position_notional = abs(
float(pos.get("positionValue", 0) or 0)
)
break
except Exception as pos_err:
self.logger.print_warning(
f"[Grid] 快照取持仓名义额失败: {pos_err}"
)
self.logger.log_equity_snapshot(
equity=equity,
available=float(balance_info.get("available", 0) or 0),
unrealized_pnl=float(balance_info.get("unrealized_pnl", 0) or 0),
position_notional=position_notional,
symbol=symbol,
)
halt_line = self.config.grid_halt_below_usd
if 0 < equity < halt_line:
try:
positions = self.order_manager.get_current_positions() or []
has_position = any(
p.get("coin") == symbol or p.get("symbol") == symbol
for p in positions
)
except Exception:
has_position = True # 查询失败按有持仓处理,不敢停
if not has_position:
self.logger.print_warning(
f"[Grid] 💤 净值 ${equity:.2f} 低于停机线 ${halt_line:.2f} "
f"且无持仓,跳过本轮网格周期(不拉行情/不调 LLM)"
)
if cloud:
cloud.send_cycle_event(
symbol=symbol,
phase="skip",
details={"reason": "低于停机线", "equity": equity},
level="warn",
)
return

# Triple Barrier 每轮必查:独立于 AI action 分支,KEEP_GRID/ERROR 周期也兜底止损。
# 触发即已紧急平仓(撤单+市价平仓+上报连亏熔断),本轮跳过布单。
if self.grid_manager.check_barrier(symbol):
self.logger.print_warning(
"[网格风控] Triple Barrier 触发,已紧急平仓,跳过本轮布单"
)
if cloud:
cloud.send_cycle_event(
symbol=symbol,
phase="skip",
details={"reason": "Triple Barrier 触发"},
level="warn",
)
return

df = self.market_fetcher.fetch_ohlcv(
symbol=symbol,
timeframe=self.config.timeframe,
Expand Down Expand Up @@ -1761,11 +1884,53 @@ def grid_cycle(self):
)
current_grid_summary = self.grid_manager.get_grid_summary(symbol)

ai_decision = self.grid_agent.make_decision(
market_data,
multi_timeframe_trends,
current_grid_summary,
# 趋势过滤:多周期一致强势时,本轮暂停网格加仓(合成 KEEP_GRID,仅维持
# reduce_only 减仓保护单),可选平掉逆势库存。直接阻断「单边趋势里持续
# 逆势做市」这一最大亏损源——中性网格在上涨里不断开空、空头无上限累积。
# 迟滞确认(confirm_cycles/flatten_min_cycles,默认 1=历史行为):单周期
# 瞬时误判是线上 12.5 天 145 次强平的主要来源,连续同向确认后才动作,
# 且「暂停加仓」先行、「平逆势库存」靠后。
raw_trend_dir = (
self._detect_strong_trend(multi_timeframe_trends)
if self.config.grid_trend_filter_enabled
else 0
)
trend_tracker = self._grid_trend_trackers.setdefault(
symbol,
TrendConfirmTracker(
confirm_cycles=self.config.grid_trend_filter_confirm_cycles,
flatten_min_cycles=self.config.grid_trend_filter_flatten_min_cycles,
),
)
trend_dir, flatten_allowed = trend_tracker.update(raw_trend_dir)
if trend_dir != 0:
arrow = "上涨" if trend_dir > 0 else "下跌"
_, streak_count = trend_tracker.streak
self.logger.print_warning(
f"[网格风控] 检测到强趋势({arrow},连续 {streak_count} 周期确认),"
f"本轮暂停网格加仓,仅维持减仓保护单"
)
if self.config.grid_trend_filter_flatten_adverse and flatten_allowed:
self.grid_manager.flatten_adverse_inventory(symbol, trend_dir)
ai_decision = {
"action": "KEEP_GRID",
"mode": "NEUTRAL",
"confidence": 0.0,
"reason": f"强趋势({arrow})暂停加仓",
}
else:
if raw_trend_dir != 0:
_, streak_count = trend_tracker.streak
self.logger.print_info(
f"[网格风控] 检测到强趋势信号但未达连续确认周期 "
f"({streak_count}/{self.config.grid_trend_filter_confirm_cycles}),"
f"本轮暂不动作"
)
ai_decision = self.grid_agent.make_decision(
market_data,
multi_timeframe_trends,
current_grid_summary,
)

# 记录网格决策到本地日志 + 云端
action = ai_decision.get("action", "UNKNOWN")
Expand Down
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ description = "基于 Pydantic AI 的 AI 驱动加密货币自动交易机器人
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
# Pydantic AI Core
"pydantic-ai>=1.0.0",
# Pydantic AI Core(钉在 1.107.0:1.108+ 移除 pydantic_ai.models.gemini 模块路径,
# llm_client 的 GeminiModel 导入会崩;升级需先适配新导入路径)
"pydantic-ai==1.107.0",

# 交易所 SDK
"hyperliquid-python-sdk>=0.21.0",
Expand Down
Loading