This commit is contained in:
zhangkun9038@dingtalk.com 2025-11-27 16:17:22 +08:00
parent 7fba5e94cd
commit 271a98a772

View File

@ -26,11 +26,12 @@ class AdjustmentType(Enum):
@dataclass
class GridLevel:
"""单个网格点记录"""
"""单个网格点的状态"""
price: float # 网格价格
quantity: float # 该价格的持仓数量
status: str # "filled" (已买入) 或 "empty" (未买/已卖出)
quantity: float # 该网格的持仓数量
entry_price: float # 该网格的实际买入价格
entry_time: int # 建仓蜡烛线索引
status: str # "open" 或 "closed"
@dataclass
@ -58,13 +59,16 @@ class OrderFill:
class GridManager:
"""
网格交易管理器
网格交易管理器 - 核心思想维护网格的填充/排空状态
维护单个币对 ETH/USDT的完整网格持仓生命周期
包含
- 网格参数管理
- 持仓状态跟踪
- 加减仓决策
网格逻辑
- 下沿到当前价格 所有网格必须被填满已买入持仓
- 当前价格到上沿 所有网格必须被排空不持有 已卖出
决策规则
1. 如果下方有空白网格 加仓填满它们
2. 如果上方有持仓网格 平仓排空它们
3. 维护一个网格状态数组每个位置记录 "filled" "empty"
"""
def __init__(self,
@ -78,10 +82,10 @@ class GridManager:
Args:
pair: 币对名称 "ETH/USDT"
lower_price: 网格下限价格 1500
upper_price: 网格上限价格 4500
step: 网格间距 50
stake_per_grid: 每个网格的投资额 40 USDT
lower_price: 网格下限价格
upper_price: 网格上限价格
step: 网格间距
stake_per_grid: 每个网格的投资额
"""
self.pair = pair
self.lower_price = lower_price
@ -95,11 +99,20 @@ class GridManager:
# 生成所有网格点价格
self.grid_prices = [lower_price + i * step for i in range(self.total_grid_levels)]
# 持仓状态
self.grid_levels: Dict[float, GridLevel] = {} # 价格 -> GridLevel
self.position_history: List[PositionRecord] = [] # 历史加减仓记录
# 核心数据结构:网格状态数组
# grid_states[i] = GridLevel 对象记录该网格的状态filled/empty
self.grid_states: Dict[float, GridLevel] = {}
for price in self.grid_prices:
self.grid_states[price] = GridLevel(
price=price,
status="empty", # 初始状态:全部为 empty未持有
quantity=0.0, # 当前持仓数量
entry_price=0.0, # 买入价格
entry_time=0
)
# 订单执行历史
# 持仓统计
self.position_history: List[PositionRecord] = [] # 历史加减仓记录
self.order_fills: List[OrderFill] = [] # 所有订单成交记录
self.pending_orders: Dict[str, PositionRecord] = {} # 待成交的订单映射
@ -110,7 +123,6 @@ class GridManager:
self.avg_entry_price: float = 0.0 # 平均建仓价
self.highest_price: float = 0.0 # 持仓期间最高价
self.lowest_price: float = float('inf') # 持仓期间最低价
self.max_positions: int = 0 # 历史最大持仓数
# 调试
self.candle_index = 0 # 当前蜡烛线索引
@ -149,7 +161,9 @@ class GridManager:
def apply_adjustment(self, adjustment: PositionRecord) -> None:
"""
应用一次加减仓操作来自策略的决策
应用一次加减仓操作并更新网格状态
通过更新 grid_states 中相关网格的状态filled/empty来报告内部一致性
Args:
adjustment: PositionRecord 对象包含加减仓的所有信息
@ -158,59 +172,61 @@ class GridManager:
quantity = adjustment.quantity
adj_type = adjustment.type
# 找到该操作所属的网格点
grid_price = self._round_to_grid(price)
grid_state = self.grid_states.get(grid_price)
if adj_type == AdjustmentType.ENTRY or adj_type == AdjustmentType.ADD:
# 建仓或加仓
if price not in self.grid_levels:
self.grid_levels[price] = GridLevel(
price=price,
quantity=quantity,
entry_time=self.candle_index,
status="open"
)
else:
self.grid_levels[price].quantity += quantity
# 建仓或加仓 → 将该网格标记为 FILLED
if grid_state:
grid_state.status = "filled"
grid_state.quantity += quantity
grid_state.entry_price = price # 记录实际买入价
grid_state.entry_time = self.candle_index
# 更新总持仓
old_total = self.total_quantity
# 更新持仓统计
self.total_invested += quantity * price
self.total_quantity += quantity
# 更新平均价
if self.total_quantity > 0:
self.avg_entry_price = self.total_invested / self.total_quantity
# 更新最大持仓数
if len(self.grid_levels) > self.max_positions:
self.max_positions = len(self.grid_levels)
print(f"[GridManager] {self.pair} 加仓 - 价格: {price:.2f}, "
print(f"[GridManager] {self.pair} 加仓 - 网格 {grid_price:.2f} 标记为 FILLED, "
f"数量: {quantity:.6f}, 总持仓: {self.total_quantity:.6f}",
file=sys.stderr, flush=True)
elif adj_type == AdjustmentType.REDUCE:
# 减仓
if price in self.grid_levels:
self.grid_levels[price].quantity -= quantity
if self.grid_levels[price].quantity <= 0:
del self.grid_levels[price]
# 减仓 → 其他网格的故事,不常用(网格交易通常只有 ENTRY/ADD/EXIT
if grid_state:
grid_state.quantity -= quantity
if grid_state.quantity <= 0:
grid_state.quantity = 0
self.total_quantity -= quantity
self.total_invested -= quantity * price
self.total_invested -= quantity * grid_state.entry_price if grid_state else 0
if self.total_quantity > 0:
self.avg_entry_price = self.total_invested / self.total_quantity
print(f"[GridManager] {self.pair} 减仓 - 价格: {price:.2f}, "
f"数量: {quantity:.6f}, 剩余持仓: {self.total_quantity:.6f}",
print(f"[GridManager] {self.pair} 减仓 - 网格 {grid_price:.2f}, "
f"减少: {quantity:.6f}, 剩余持仓: {self.total_quantity:.6f}",
file=sys.stderr, flush=True)
elif adj_type == AdjustmentType.EXIT:
# 全部平仓
print(f"[GridManager] {self.pair} 全部平仓 - 持仓: {self.total_quantity:.6f}, "
# 平仓 → 将所有 FILLED 的网格一次排空(或不排空对象网格)
# 此处只是记录平仓决策,实际的排空是由 decide_adjustment 程序控制
print(f"[GridManager] {self.pair} 平仓 - 总持仓: {self.total_quantity:.6f}, "
f"平均价: {self.avg_entry_price:.2f}, 当前价: {price:.2f}",
file=sys.stderr, flush=True)
self.grid_levels.clear()
# 清空所有 FILLED 网格,下次套需要绍转建仓时重新填满
for gs in self.grid_states.values():
if gs.status == "filled":
gs.status = "empty"
gs.quantity = 0
gs.entry_price = 0.0
# 清空污了持仓计数
self.total_quantity = 0.0
self.total_invested = 0.0
self.avg_entry_price = 0.0
@ -222,13 +238,13 @@ class GridManager:
def decide_adjustment(self) -> Optional[PositionRecord]:
"""
判定是否需要加减仓并返回建议
判定是否需要加减仓基于网格填充/排空逻辑
核心逻辑
1. 如果还没有建仓过且价格在网格范围内 初始建仓
2. 如果已有头寸价格跌入新的更低网格点 加仓
3. 如果已有头寸价格涨超过平均价 全部平仓
4. 如果已有多个头寸价格涨到最高点 可选部分减仓
核心规则
1. 下沿到当前价格 所有网格必须被 FILLED已买入
如果有 EMPTY 的网格 加仓填满它
2. 当前价格到上沿 所有网格必须被 EMPTY不持有
如果有 FILLED 的网格 平仓排空它
Returns:
PositionRecord 如果需要操作否则 None
@ -236,61 +252,50 @@ class GridManager:
if self.current_price is None:
return None
# 情况 1: 还没有持仓,且价格在范围内 → 初始建仓
if self.total_quantity == 0:
if self.lower_price <= self.current_price <= self.upper_price:
# 找到当前价格对应的网格点(向上舍入)
grid_price = (int(self.current_price / self.step) + 1) * self.step
if grid_price > self.upper_price:
grid_price = self.upper_price
print(f"[GridManager] {self.pair} 初始建仓建议 - 价格: {grid_price:.2f}",
file=sys.stderr, flush=True)
return PositionRecord(
level_index=self._price_to_level_index(grid_price),
price=self.current_price,
quantity=1.0, # 1个单位实际金额由策略乘以 stake_per_grid
type=AdjustmentType.ENTRY,
timestamp=self.candle_index
)
# 找到当前价格所在的网格点
current_grid_price = self._round_to_grid(self.current_price)
# 情况 2: 已有持仓,价格涨超过平均价 → 全部平仓
if self.total_quantity > 0 and self.current_price > self.avg_entry_price:
profit_pct = (self.current_price - self.avg_entry_price) / self.avg_entry_price * 100
print(f"[GridManager] {self.pair} 平仓建议 - 利润: {profit_pct:.2f}%",
file=sys.stderr, flush=True)
return PositionRecord(
level_index=0,
price=self.current_price,
quantity=self.total_quantity,
type=AdjustmentType.EXIT,
timestamp=self.candle_index
)
# 情况 3: 已有持仓,价格跌入新的更低网格点 → 加仓
if self.total_quantity > 0:
# 找到当前价格最接近的网格点(向下舍入)
current_grid_level = int(self.current_price / self.step) * self.step
# 关键修复:只有当价格已经跌破平均价时,才考虑加仓
# 并且必须是一个还没加过仓的网格点
if current_grid_level < self.avg_entry_price and current_grid_level >= self.lower_price:
if current_grid_level not in self.grid_levels and len(self.grid_levels) < self.total_grid_levels:
print(f"[GridManager] {self.pair} 加仓建议 - 价格: {current_grid_level:.2f}, "
f"已有网格数: {len(self.grid_levels)}",
# 规则 2: 检查上方是否有需要平仓的网格
# 当前价格到上沿之间的网格都应该是 EMPTY
for grid_price in self.grid_prices:
if grid_price > current_grid_price:
grid_state = self.grid_states.get(grid_price)
if grid_state and grid_state.status == "filled" and grid_state.quantity > 0:
# 上方有持仓,需要平仓
print(f"[GridManager] {self.pair} 平仓信号 - 价格已涨到 {self.current_price:.2f}, "
f"上方网格 {grid_price:.2f} 有持仓需排空",
file=sys.stderr, flush=True)
# 平仓该网格
return PositionRecord(
level_index=self._price_to_level_index(current_grid_level),
level_index=self._price_to_level_index(grid_price),
price=self.current_price,
quantity=1.0,
type=AdjustmentType.ADD,
quantity=grid_state.quantity,
type=AdjustmentType.EXIT,
timestamp=self.candle_index
)
# 没有操作建议
# 规则 1: 检查下方是否有需要加仓的网格
# 下沿到当前价格之间的网格都应该是 FILLED
for grid_price in self.grid_prices:
if grid_price <= current_grid_price:
grid_state = self.grid_states.get(grid_price)
if grid_state and grid_state.status == "empty" and grid_state.quantity == 0:
# 下方有空白网格,需要加仓
print(f"[GridManager] {self.pair} 加仓信号 - 价格已跌到 {self.current_price:.2f}, "
f"下方网格 {grid_price:.2f} 为空需填满",
file=sys.stderr, flush=True)
# 加仓该网格
return PositionRecord(
level_index=self._price_to_level_index(grid_price),
price=self.current_price,
quantity=1.0,
type=AdjustmentType.ADD if self.total_quantity > 0 else AdjustmentType.ENTRY,
timestamp=self.candle_index
)
# 没有操作建议(所有网格状态都符合规则)
return None
def _price_to_level_index(self, price: float) -> int:
@ -298,8 +303,15 @@ class GridManager:
index = int((price - self.lower_price) / self.step)
return max(0, min(index, self.total_grid_levels - 1))
def _round_to_grid(self, price: float) -> float:
"""
将价格舍入到最接近的网格点向下舍入
"""
return int(price / self.step) * self.step
def get_summary(self) -> Dict[str, Any]:
"""获取当前持仓的完整摘要"""
filled_grids = sum(1 for gs in self.grid_states.values() if gs.status == "filled")
return {
"pair": self.pair,
"current_price": self.current_price,
@ -307,9 +319,9 @@ class GridManager:
"total_invested": self.total_invested,
"avg_entry_price": self.avg_entry_price,
"unrealized_profit": (self.current_price - self.avg_entry_price) * self.total_quantity if self.total_quantity > 0 else 0,
"active_grid_levels": len(self.grid_levels),
"max_positions_ever": self.max_positions,
"total_adjustments": len(self.position_history),
"filled_grids": filled_grids, # FILLED 的网格数
"empty_grids": self.total_grid_levels - filled_grids, # EMPTY 的网格数
"total_orders": len(self.order_fills),
"highest_price": self.highest_price if self.total_quantity > 0 else None,
"lowest_price": self.lowest_price if self.total_quantity > 0 else None,
}
@ -383,8 +395,9 @@ class GridManager:
"""
Freqtrade Trade 对象同步状态
当无法直接捕获订单成交时可以通过 Trade 对象反向同步状态
备选方案不如直接记录 OrderFill 准确
由于 Trade 对象提供了实际持仓信息使用其更新网格状态
- 根据平均价找到应该填满的网格
- 全部低于平均价的网格为 FILLED高于平均价的为 EMPTY
Args:
trade: Freqtrade Trade 对象
@ -395,29 +408,37 @@ class GridManager:
self.total_invested = trade.stake_amount
self.avg_entry_price = trade.open_rate
# 重新初始化 grid_levels仅保留当前持仓
self.grid_levels.clear()
if self.total_quantity > 0 and self.avg_entry_price > 0:
# 创建一个体现当前持仓的网格点
# 依据平均价格找最接近的网格位置
grid_price = round(self.avg_entry_price / self.step) * self.step
self.grid_levels[grid_price] = GridLevel(
price=grid_price,
quantity=self.total_quantity,
entry_time=candle_index,
status="open"
)
# 根据 Trade 对象的平均价,更新所有网格的状态
# 下沿 → 平均价:全部 FILLED
# 平均价 → 上沿:全部 EMPTY
avg_grid_price = self._round_to_grid(self.avg_entry_price)
# 如果 trade 有多个 entry加仓的情况
if hasattr(trade, 'trades') and trade.trades:
entries = [t for t in trade.trades if t.entry_side == 'entry']
self.max_positions = len(entries)
for price, grid_state in self.grid_states.items():
if price < avg_grid_price:
# 下沿:应该是 FILLED
if self.total_quantity > 0:
grid_state.status = "filled"
grid_state.entry_price = self.avg_entry_price
grid_state.entry_time = candle_index
# 简化:把所有 filled 网格的持仓信息污盖到了均价
# 实际应用中会根据 trade.trades 进行细上漂歪
else:
grid_state.status = "empty"
grid_state.quantity = 0
elif price > avg_grid_price:
# 上沿:应该是 EMPTY
grid_state.status = "empty"
grid_state.quantity = 0
else:
# 不太可能抹挡,但为了严谨标记为 FILLED
grid_state.status = "filled"
grid_state.entry_price = self.avg_entry_price
grid_state.entry_time = candle_index
print(f"[GridManager] {self.pair} 从 Trade 对象同步状态 - "
print(f"[GridManager] {self.pair} 从 Trade 对象同步 - "
f"持仓: {self.total_quantity:.6f}, "
f"投入: {self.total_invested:.2f} USDT, "
f"平均价: {self.avg_entry_price:.2f}, "
f"网格点数: {len(self.grid_levels)}",
f"下沿到平均价的网格标记为 FILLED",
file=sys.stderr, flush=True)
def record_pending_order(self, order_id: str, adjustment: PositionRecord) -> None: