日志展示优化
This commit is contained in:
192
bitmart/四分之一策略说明-修改版.md
Normal file
192
bitmart/四分之一策略说明-修改版.md
Normal file
@@ -0,0 +1,192 @@
|
||||
# 四分之一策略 · ETHUSDT 5 分钟(修改版)— 详细说明
|
||||
|
||||
## 优化摘要(与初版对比)
|
||||
|
||||
- **触发方式**:由「当前价与阈值比较」改为**穿越触发**(`cross_up` / `cross_down`),即上一轮价在阈值一侧、当前轮在另一侧才触发,避免“已在阈值下方/上方”的追单。
|
||||
- **实体过滤**:由固定 `entity < 0.1` 改为**百分比或 ATR**:`entity_filter_mode` 可选 `"pct"`(entity/close ≥ min_entity_pct)或 `"atr"`(entity ≥ entity_atr_ratio × ATR14)。
|
||||
- **影线反手**:上/下影线阈值由 0.01% 提高到 **0.05%**(可调),且影线反手也要求**穿越**实体边,减少频繁反手。
|
||||
- **EMA20**:拆成 **use_ema20_entry_filter**(开仓/反手方向过滤)与 **use_ema20_force_exit**(持仓强制平仓),解耦便于调参。
|
||||
- **平仓防针洗**:**exit_use_last_close = True** 时用已收盘 K 线的 `last_close` 判断 EMA 破位,减少瞬间插针触发平仓。
|
||||
- **ATR 追踪**:由「本 K 线内最高/最低」改为**自开仓以来**的 `_highest_since_entry` / `_lowest_since_entry`,换线不重置,真正追踪止盈。
|
||||
- **开盘延迟**:可选 **open_trigger_delay_seconds**(默认 0),当前 K 线开始后经过该秒数才允许触发,减轻开盘即触发。
|
||||
|
||||
---
|
||||
|
||||
## 一、策略概述
|
||||
|
||||
- **名称**:四分之一策略(Quarter Strategy)
|
||||
- **标的**:Bitmart 合约 **ETHUSDT**
|
||||
- **周期**:**5 分钟 K 线**
|
||||
- **模式**:全仓(cross)、高杠杆(100x)
|
||||
- **思路**:用「上一根 K 线实体」的 **1/4 位置** 做突破/回落触发,结合 EMA10/EMA20/ATR 止盈与趋势过滤,支持多空开仓与反手。
|
||||
|
||||
---
|
||||
|
||||
## 二、核心价格与 K 线定义
|
||||
|
||||
### 2.1 上一根 K 线实体
|
||||
|
||||
- **实体大小**:`entity = |close - open|`(绝对值)
|
||||
- **实体上边**:`upper = max(open, close)`
|
||||
- **实体下边**:`lower = min(open, close)`
|
||||
|
||||
即:阳线时上边=收盘、下边=开盘;阴线时上边=开盘、下边=收盘。
|
||||
|
||||
### 2.2 四个关键价位(常规情况)
|
||||
|
||||
以**上一根 K 线**的实体为基准:
|
||||
|
||||
| 名称 | 计算公式 | 含义 |
|
||||
|--------------|------------------------------|--------------------------|
|
||||
| 做多触发 | `lower + entity/4` | 实体下方向上 1/4 处 |
|
||||
| 做空触发 | `upper - entity/4` | 实体上边向下 1/4 处 |
|
||||
| 突破做多 | `upper + entity/4` | 实体上边再向上 1/4 处 |
|
||||
| 突破做空 | `lower - entity/4` | 实体下边再向下 1/4 处 |
|
||||
|
||||
- **无仓时**:做多看「突破做多」价,做空看「做空触发」价。
|
||||
- **有仓时**:反手多/空用「做多触发」「做空触发」价。
|
||||
|
||||
### 2.3 跳空时的基准修正
|
||||
|
||||
当出现跳空时,用**当前 K 线开盘价**作为计算基准,使 1/4 区间对称于开盘价:
|
||||
|
||||
- **情况 1**:上一根为**阳线**且**当前开盘价 > 上一根收盘价**(跳空高开)
|
||||
- **情况 2**:上一根为**阴线**且**当前开盘价 < 上一根收盘价**(跳空低开)
|
||||
|
||||
此时:
|
||||
|
||||
- `calc_base = 当前 K 线 open`
|
||||
- 做多触发 = `calc_base + entity/4`
|
||||
- 做空触发 = `calc_base - entity/4`
|
||||
- 突破做多 = `calc_base + entity/4`(与做多触发同)
|
||||
- 突破做空 = `calc_base - entity/4`(与做空触发同)
|
||||
|
||||
即跳空时以当前开盘为轴心,上下各 1/4 实体对称。
|
||||
|
||||
---
|
||||
|
||||
## 三、开仓与反手信号逻辑
|
||||
|
||||
### 3.1 前置过滤(所有信号共用)
|
||||
|
||||
- **实体过小**:按 `entity_filter_mode` 二选一:`entity/close` 不低于 `min_entity_pct`(如 0.02%),或 `entity` 不低于 `entity_atr_ratio × ATR(14)`(如 0.2×ATR)。不满足则本根不产生信号。
|
||||
- **形态过滤**:
|
||||
- **上一根阴线 + 当前阳线**(视为做多形态):不按「做空触发(上 1/4)」做空。
|
||||
- **上一根阳线 + 当前阴线**(视为做空形态):不按「做多触发(下 1/4)」做多。
|
||||
- **穿越触发**:所有开仓/反手均要求**上一轮价格在阈值一侧、当前轮穿越到另一侧**(避免追单)。
|
||||
|
||||
### 3.2 无持仓(start == 0)
|
||||
|
||||
- **做多**:**穿越**突破做多(上一轮 < 突破价 ≤ 当前价),且未被做多形态过滤、且通过 EMA20 入场过滤 → 开多。
|
||||
- **做空**:**穿越**做空触发(上一轮 > 做空触发 ≥ 当前价),且未被做空形态过滤、且通过 EMA20 入场过滤 → 开空。
|
||||
|
||||
### 3.3 持多仓(start == 1)→ 反手做空
|
||||
|
||||
满足其一即反手空:
|
||||
|
||||
1. **穿越**做空触发,且反手价差 ≥ reverse_min_move_pct,且 EMA20 入场过滤通过。
|
||||
2. **上影线反手**:上一根**上影线比例 > min_upper_shadow_pct**(如 0.05%),且**穿越**上一根实体下边。
|
||||
|
||||
### 3.4 持空仓(start == -1)→ 反手做多
|
||||
|
||||
满足其一即反手多:
|
||||
|
||||
1. **穿越**做多触发,且反手价差 ≥ reverse_min_move_pct,且 EMA20 入场过滤通过。
|
||||
2. **下影线反手**:上一根**下影线比例 > min_lower_shadow_pct**(如 0.05%),且**穿越**上一根实体上边。
|
||||
|
||||
---
|
||||
|
||||
## 四、反手前的额外过滤
|
||||
|
||||
- **反手价差过滤**:
|
||||
`reverse_min_move_pct = 0.05%`
|
||||
反手时要求:`|当前价 - 触发价| / 触发价 * 100 >= 0.05%`,否则不执行反手(避免刚触及就反手)。
|
||||
|
||||
---
|
||||
|
||||
## 五、开仓前的过滤
|
||||
|
||||
1. **同 K 线出场不再开仓**:若本根 K 线内已经因 EMA/ATR/EMA20 平过仓(`_last_exit_kline_id == current_kline_time`),本根 K 线内不再开多/开空,等下一根 K 线再判断。
|
||||
2. **EMA20 方向过滤**(`use_ema20_filter = True`):
|
||||
- 开多 / 反手多:仅当 **当前价 > EMA20** 时允许。
|
||||
- 开空 / 反手空:仅当 **当前价 < EMA20** 时允许。
|
||||
即逆势(价在 EMA20 另一侧)时暂停开仓/反手。
|
||||
|
||||
---
|
||||
|
||||
## 六、平仓(止盈/风控)规则
|
||||
|
||||
- **最短持仓时间**:`min_hold_seconds = 90` 秒。开仓或反手后,至少持仓 90 秒才允许下面「技术性平仓」。
|
||||
- **EMA/ATR 平仓开关**:`use_ema_atr_exit = True` 时,下面规则才生效。
|
||||
- **ATR 倍数**:`atr_multiplier = 1.1`,即 1.1 × ATR(14)。
|
||||
- **防针洗**:`exit_use_last_close = True` 时,EMA10/EMA20 破位用**已收盘 K 线的 last_close** 判断,减少插针触发平仓;ATR 仍用当前价与自开仓以来极值比较。
|
||||
- **EMA20 强制平**:由独立开关 **use_ema20_force_exit** 控制,与开仓用的 **use_ema20_entry_filter** 解耦。
|
||||
|
||||
### 6.1 多单平仓(满足其一即平)
|
||||
|
||||
1. **EMA10 快出**:参考价 **< EMA10**(参考价 = last_close 或当前价,见上)→ 平多。
|
||||
2. **EMA20 强制平**(`use_ema20_force_exit = True`):参考价 **< EMA20** → 强制平多。
|
||||
3. **ATR 追踪止盈**:从**自开仓以来最高价**回撤 **≥ 1.1×ATR(14)**(用当前价)→ 平多。极值不随换线重置。
|
||||
|
||||
### 6.2 空单平仓(满足其一即平)
|
||||
|
||||
1. **EMA10 快出**:参考价 **> EMA10** → 平空。
|
||||
2. **EMA20 强制平**:参考价 **> EMA20** → 强制平空。
|
||||
3. **ATR 追踪止盈**:从**自开仓以来最低价**反弹 **≥ 1.1×ATR(14)** → 平空。
|
||||
|
||||
### 6.3 指标计算说明
|
||||
|
||||
- **EMA10 / EMA20 / ATR(14) / last_close**:均基于「已收盘 K 线」计算(最近约 35 根 5m,排除当前未收盘一根);`get_ema_atr_for_exit` 返回 dict 含上述四项。
|
||||
- **自开仓以来最高/最低**:仅在持仓期间用当前价更新,用于 ATR 追踪;开仓/反手时重置为当次入场价,换线不重置。
|
||||
|
||||
---
|
||||
|
||||
## 七、交易执行与风控细节
|
||||
|
||||
- **下单方式**:市价单;开仓/反手统一张数 `default_order_size = 25`(可在代码中修改)。
|
||||
- **反手流程**:先市价平仓 → 轮询确认无持仓(最多约 10 秒)→ 再市价开新方向;反手成功后多等约 20 秒再继续循环,避免界面/接口延迟。
|
||||
- **开仓前校验**:开多/开空前会查持仓,若已有反向或同向持仓则放弃本次开仓,避免双向持仓。
|
||||
- **杠杆与账户**:启动时设置全仓、100x 杠杆;未使用 `risk_percent`,仓位由固定张数控制。
|
||||
|
||||
---
|
||||
|
||||
## 八、主循环流程概览
|
||||
|
||||
1. 拉取最近 2 根 5 分钟 K 线(上一根 + 当前根)及当前价。
|
||||
2. 每次循环通过 API 获取真实持仓状态(避免与交易所不同步)。
|
||||
3. **若已有持仓**:
|
||||
先判断是否满足「最短持仓 90 秒」+ EMA10/EMA20/ATR 平仓条件,满足则平仓并本 K 线内不再开仓。
|
||||
4. **信号检测**:
|
||||
根据当前价与上一根实体 1/4 计算做多/做空/突破价,结合形态过滤与持仓状态,得到开多、开空、反手多、反手空之一或无信号。
|
||||
5. **反手过滤**:若为反手信号,再检查反手价差 ≥ 0.05%。
|
||||
6. **开仓过滤**:若为开多/开空,检查「本 K 线未因技术性平仓」及 EMA20 方向过滤。
|
||||
7. **执行**:有信号则执行对应开仓或反手,成功后刷新页面/状态并短暂等待再进入下一轮。
|
||||
|
||||
---
|
||||
|
||||
## 九、关键参数汇总
|
||||
|
||||
| 参数 | 值 | 说明 |
|
||||
|------------------------------|-----------|----------------------------------------|
|
||||
| contract_symbol | ETHUSDT | 合约品种 |
|
||||
| 周期 | 5 分钟 | K 线步长 |
|
||||
| leverage | 100 | 杠杆倍数 |
|
||||
| open_type | cross | 全仓 |
|
||||
| default_order_size | 25 | 开仓/反手张数 |
|
||||
| reverse_min_move_pct | 0.05 | 反手最小价差(%) |
|
||||
| min_hold_seconds | 90 | 最短持仓时间(秒) |
|
||||
| use_ema_atr_exit | True | 是否启用 EMA/ATR 平仓 |
|
||||
| atr_multiplier | 1.1 | ATR 追踪止盈倍数(自开仓以来极值) |
|
||||
| use_ema20_entry_filter | True | 开仓/反手方向过滤(价在 EMA20 同侧) |
|
||||
| use_ema20_force_exit | True | 持仓强制平仓(价穿越 EMA20 则平) |
|
||||
| exit_use_last_close | True | 用已收盘 last_close 判断 EMA 破位防针洗|
|
||||
| entity_filter_mode | atr | 实体过滤:pct 或 atr |
|
||||
| min_entity_pct | 0.02 | 实体/close ≥ 此%才交易(mode=pct) |
|
||||
| entity_atr_ratio | 0.2 | 实体 ≥ 此倍数×ATR 才交易(mode=atr) |
|
||||
| min_upper_shadow_pct | 0.05 | 上影线比例 > 此值才允许上影线反手 |
|
||||
| min_lower_shadow_pct | 0.05 | 下影线比例 > 此值才允许下影线反手 |
|
||||
| open_trigger_delay_seconds | 0 | 当前 K 线开始后延迟 N 秒再允许触发 |
|
||||
|
||||
---
|
||||
|
||||
以上即为「四分之一,五分钟,反手条件充足修改版」中的完整策略逻辑说明;实际运行以代码为准,本文档便于理解和后续调参。
|
||||
@@ -106,6 +106,17 @@ def build_dashboard_layout(state: dict, logs: list) -> Layout:
|
||||
return layout
|
||||
|
||||
|
||||
# ---------- 穿越触发(避免“已在阈值下方/上方”的追单) ----------
|
||||
def cross_up(prev_price: float, curr_price: float, level: float) -> bool:
|
||||
"""上一轮 < 阈值 且 这一轮 >= 阈值"""
|
||||
return prev_price < level <= curr_price
|
||||
|
||||
|
||||
def cross_down(prev_price: float, curr_price: float, level: float) -> bool:
|
||||
"""上一轮 > 阈值 且 这一轮 <= 阈值"""
|
||||
return prev_price > level >= curr_price
|
||||
|
||||
|
||||
class BitmartFuturesTransaction:
|
||||
def __init__(self, bit_id):
|
||||
|
||||
@@ -134,15 +145,18 @@ class BitmartFuturesTransaction:
|
||||
self.leverage = "100" # 高杠杆(全仓模式下可开更大仓位)
|
||||
self.open_type = "cross" # 全仓模式
|
||||
self.risk_percent = 0 # 未使用;若启用则可为每次开仓占可用余额的百分比
|
||||
# 退出:仅 EMA10(快退出)+ 1.1×ATR(14) 追踪。EMA20 不做先平,只做趋势/方向过滤(见下)
|
||||
# 退出:EMA10 快退出 + EMA20 强制平(可独立开关)+ 自开仓以来 ATR 追踪
|
||||
self.use_ema_atr_exit = True # 是否启用 EMA/ATR 平仓规则(多单+空单)
|
||||
self.atr_multiplier = 1.1 # 追踪止盈:从当前K线极值回撤/反弹 ≥ 此倍数×ATR(14) 则平仓
|
||||
self.use_ema20_filter = True # 是否用 EMA20 做开仓/反手方向过滤 + 持仓强制减仓(趋势过滤)
|
||||
self.atr_multiplier = 1.1 # 追踪止盈:自开仓以来最高/最低回撤/反弹 ≥ 此倍数×ATR(14) 则平仓
|
||||
self.use_ema20_entry_filter = True # 开仓/反手方向过滤:价在 EMA20 同侧才允许
|
||||
self.use_ema20_force_exit = True # 持仓强制平仓:价穿越 EMA20 则平(与入场过滤解耦)
|
||||
self.min_hold_seconds = 90 # 开仓/反手后至少持仓此时长才允许技术性止盈(EMA10/EMA20/ATR)
|
||||
self._candle_high_seen = None # 当前K线内见过的最高价(多头用)
|
||||
self._candle_low_seen = None # 当前K线内见过的最低价(空头用)
|
||||
self._candle_id_for_high_low = None # 记录高低对应的K线 id,换线则重置
|
||||
self.exit_use_last_close = True # True=用已收盘K线 last_close 判断 EMA 破位(防针洗),False=用当前价
|
||||
# 自开仓以来的最高/最低(真正的 ATR 追踪,不随换线重置)
|
||||
self._highest_since_entry = None
|
||||
self._lowest_since_entry = None
|
||||
self._last_exit_kline_id = None # 当前K线出场后,本K线内不再开仓,等下一根K线再判断
|
||||
self._prev_price = None # 上一轮轮询价,用于穿越触发
|
||||
|
||||
self.open_avg_price = None # 开仓价格
|
||||
self.current_amount = None # 持仓量
|
||||
@@ -150,6 +164,16 @@ class BitmartFuturesTransaction:
|
||||
self.bit_id = bit_id
|
||||
self.default_order_size = 25 # 开仓/反手张数,统一在此修改
|
||||
|
||||
# 实体过滤:ETH 上 0.1 几乎无效,改为百分比或 ATR。二选一:entity_pct 或 entity_atr_ratio
|
||||
self.entity_filter_mode = "atr" # "pct" | "atr"
|
||||
self.min_entity_pct = 0.02 # entity/close < 0.02% 视为实体过小(当 mode=pct 时)
|
||||
self.entity_atr_ratio = 0.2 # entity < 0.2*ATR(14) 视为实体过小(当 mode=atr 时)
|
||||
# 上/下影线反手阈值(原 0.01% 太小,几乎根根满足)
|
||||
self.min_upper_shadow_pct = 0.05 # 上影线比例 > 此值才允许“上影线反手”
|
||||
self.min_lower_shadow_pct = 0.05 # 下影线比例 > 此值才允许“下影线反手”
|
||||
# 跳空/开盘后延迟:当前 K 线开始后经过此秒数才允许触发(0=不延迟,可设 30 等防开盘即触发)
|
||||
self.open_trigger_delay_seconds = 0
|
||||
|
||||
# 策略相关变量
|
||||
self.prev_kline = None
|
||||
self.current_kline = None
|
||||
@@ -253,19 +277,20 @@ class BitmartFuturesTransaction:
|
||||
|
||||
def get_ema_atr_for_exit(self, kline_series):
|
||||
"""
|
||||
基于已收盘 K 线计算 EMA10、EMA20、ATR(14)。
|
||||
基于已收盘 K 线计算 EMA10、EMA20、ATR(14)、last_close。
|
||||
kline_series 最后一根可为当前未收盘 K 线,计算时用倒数第 2~N 根作为已收盘。
|
||||
返回 (ema10, ema20, atr14),任一不足则对应为 None。
|
||||
返回 dict: ema10, ema20, atr14, last_close(已收盘的最后一根 close,用于防针洗)。
|
||||
"""
|
||||
if not kline_series or len(kline_series) < 21:
|
||||
return None, None, None
|
||||
return {"ema10": None, "ema20": None, "atr14": None, "last_close": None}
|
||||
# 用除最后一根外的已收盘 K 线(若只有 21 根则用前 20 根)
|
||||
closed = kline_series[:-1] if len(kline_series) >= 21 else kline_series[:20]
|
||||
closes = [k['close'] for k in closed]
|
||||
ema10 = self._ema(closes, 10)
|
||||
ema20 = self._ema(closes, 20)
|
||||
atr14 = self._atr(closed, 14)
|
||||
return ema10, ema20, atr14
|
||||
last_close = closed[-1]['close'] if closed else None
|
||||
return {"ema10": ema10, "ema20": ema20, "atr14": atr14, "last_close": last_close}
|
||||
|
||||
def get_current_price(self):
|
||||
"""获取当前最新价格"""
|
||||
@@ -455,122 +480,189 @@ class BitmartFuturesTransaction:
|
||||
'lower': min(kline['open'], kline['close']) # 实体下边
|
||||
}
|
||||
|
||||
def check_signal(self, current_price, prev_kline, current_kline):
|
||||
@staticmethod
|
||||
def quarter_levels(prev_o, prev_h, prev_l, prev_c, curr_open):
|
||||
"""计算四分之一价位(含跳空修正)。返回 dict: entity, upper, lower, long_trigger, short_trigger, breakout_long, breakout_short"""
|
||||
entity = abs(prev_c - prev_o)
|
||||
upper = max(prev_o, prev_c)
|
||||
lower = min(prev_o, prev_c)
|
||||
gap_up = (prev_c > prev_o) and (curr_open > prev_c)
|
||||
gap_down = (prev_c < prev_o) and (curr_open < prev_c)
|
||||
if gap_up or gap_down:
|
||||
base = curr_open
|
||||
long_trigger = base + entity / 4
|
||||
short_trigger = base - entity / 4
|
||||
breakout_long = long_trigger
|
||||
breakout_short = short_trigger
|
||||
else:
|
||||
long_trigger = lower + entity / 4
|
||||
short_trigger = upper - entity / 4
|
||||
breakout_long = upper + entity / 4
|
||||
breakout_short = lower - entity / 4
|
||||
return {
|
||||
"entity": entity,
|
||||
"upper": upper,
|
||||
"lower": lower,
|
||||
"long_trigger": long_trigger,
|
||||
"short_trigger": short_trigger,
|
||||
"breakout_long": breakout_long,
|
||||
"breakout_short": breakout_short,
|
||||
}
|
||||
|
||||
def _entity_filter_passed(self, entity: float, prev_close: float, atr14: float | None) -> bool:
|
||||
"""实体过滤:False 表示实体过小,不交易。"""
|
||||
if self.entity_filter_mode == "pct":
|
||||
if prev_close <= 0:
|
||||
return False
|
||||
return (entity / prev_close * 100) >= self.min_entity_pct
|
||||
# atr
|
||||
if atr14 is None or atr14 <= 0:
|
||||
return entity >= 0.1 # 回退:无 ATR 时用固定 0.1
|
||||
return entity >= self.entity_atr_ratio * atr14
|
||||
|
||||
def _allow_entry_by_ema20(self, side: int, curr_price: float, ema20: float | None) -> bool:
|
||||
"""开仓/反手方向过滤:多单要求价>EMA20,空单要求价<EMA20"""
|
||||
if not self.use_ema20_entry_filter or ema20 is None:
|
||||
return True
|
||||
return (curr_price > ema20) if side == 1 else (curr_price < ema20)
|
||||
|
||||
def check_signal(self, current_price, prev_kline, current_kline, levels: dict, ind: dict):
|
||||
"""
|
||||
检查交易信号
|
||||
返回: ('long', trigger_price) / ('short', trigger_price) / None
|
||||
检查交易信号(穿越触发 + 形态过滤 + 影线反手阈值提高)。
|
||||
返回: ('long'|'short'|'reverse_long'|'reverse_short', trigger_price) 或 None。
|
||||
"""
|
||||
current_kline_id = current_kline.get('id')
|
||||
should_log_snapshot = self._last_signal_log_kline_id != current_kline_id
|
||||
|
||||
# 计算上一根K线实体
|
||||
prev_entity = self.calculate_entity(prev_kline)
|
||||
|
||||
# 实体过小不交易(实体 < 0.1)
|
||||
if prev_entity < 0.1:
|
||||
if should_log_snapshot:
|
||||
logger.info(LOG_PRICE + f"上一根K线实体过小: {prev_entity:.4f},跳过信号检测")
|
||||
self._last_signal_log_kline_id = current_kline_id
|
||||
prev_price = self._prev_price
|
||||
if prev_price is None:
|
||||
# 第一轮无上一轮价,无法判断穿越,不产生开仓/反手
|
||||
return None
|
||||
|
||||
# 获取上一根K线的实体上下边
|
||||
prev_entity_edge = self.get_entity_edge(prev_kline)
|
||||
prev_entity_upper = prev_entity_edge['upper'] # 实体上边
|
||||
prev_entity_lower = prev_entity_edge['lower'] # 实体下边
|
||||
prev_entity = levels["entity"]
|
||||
prev_entity_upper = levels["upper"]
|
||||
prev_entity_lower = levels["lower"]
|
||||
long_trigger = levels["long_trigger"]
|
||||
short_trigger = levels["short_trigger"]
|
||||
breakout_long = levels["breakout_long"]
|
||||
|
||||
# 优化:以下两种情况以当前这根的开盘价作为计算基准
|
||||
# 1) 上一根阳线 且 当前开盘价 > 上一根收盘价(跳空高开)
|
||||
# 2) 上一根阴线 且 当前开盘价 < 上一根收盘价(跳空低开)
|
||||
prev_is_bullish_for_calc = prev_kline['close'] > prev_kline['open']
|
||||
prev_is_bearish_for_calc = prev_kline['close'] < prev_kline['open']
|
||||
current_open_above_prev_close = current_kline['open'] > prev_kline['close']
|
||||
current_open_below_prev_close = current_kline['open'] < prev_kline['close']
|
||||
use_current_open_as_base = (prev_is_bullish_for_calc and current_open_above_prev_close) or (prev_is_bearish_for_calc and current_open_below_prev_close)
|
||||
|
||||
if use_current_open_as_base:
|
||||
# 以当前K线开盘价为基准计算(跳空时用当前开盘价参与计算)
|
||||
calc_lower = current_kline['open']
|
||||
calc_upper = current_kline['open'] # 同一基准,上下四分之一对称
|
||||
long_trigger = calc_lower + prev_entity / 4
|
||||
short_trigger = calc_upper - prev_entity / 4
|
||||
long_breakout = calc_upper + prev_entity / 4
|
||||
short_breakout = calc_lower - prev_entity / 4
|
||||
else:
|
||||
# 原有计算方式
|
||||
long_trigger = prev_entity_lower + prev_entity / 4 # 做多触发价 = 实体下边 + 实体/4(下四分之一处)
|
||||
short_trigger = prev_entity_upper - prev_entity / 4 # 做空触发价 = 实体上边 - 实体/4(上四分之一处)
|
||||
long_breakout = prev_entity_upper + prev_entity / 4 # 做多突破价 = 实体上边 + 实体/4
|
||||
short_breakout = prev_entity_lower - prev_entity / 4 # 做空突破价 = 实体下边 - 实体/4
|
||||
|
||||
# 上一根阴线 + 当前阳线:做多形态,不按上一根K线上三分之一做空
|
||||
# 形态过滤
|
||||
prev_is_bearish = prev_kline['close'] < prev_kline['open']
|
||||
current_is_bullish = current_kline['close'] > current_kline['open']
|
||||
skip_short_by_upper_third = prev_is_bearish and current_is_bullish
|
||||
# 上一根阳线 + 当前阴线:做空形态,不按上一根K线下三分之一做多
|
||||
prev_is_bullish = prev_kline['close'] > prev_kline['open']
|
||||
current_is_bearish = current_kline['close'] < current_kline['open']
|
||||
skip_long_by_lower_third = prev_is_bullish and current_is_bearish
|
||||
|
||||
if should_log_snapshot:
|
||||
if use_current_open_as_base:
|
||||
if prev_is_bullish_for_calc and current_open_above_prev_close:
|
||||
logger.info(LOG_PRICE + f"上一根阳线且当前开盘价({current_kline['open']:.2f})>上一根收盘价({prev_kline['close']:.2f}),以当前开盘价为基准计算")
|
||||
else:
|
||||
logger.info(LOG_PRICE + f"上一根阴线且当前开盘价({current_kline['open']:.2f})<上一根收盘价({prev_kline['close']:.2f}),以当前开盘价为基准计算")
|
||||
logger.info(LOG_PRICE + f"当前价: {current_price:.2f} | 上一根实体: {prev_entity:.4f} | 实体上边: {prev_entity_upper:.2f} 下边: {prev_entity_lower:.2f}")
|
||||
logger.info(LOG_PRICE + f"做多触发(下1/4): {long_trigger:.2f} | 做空触发(上1/4): {short_trigger:.2f} | 突破做多: {long_breakout:.2f} | 突破做空: {short_breakout:.2f}")
|
||||
logger.info(LOG_PRICE + f"当前价: {current_price:.2f} 上一轮价: {prev_price:.2f} | 实体: {prev_entity:.4f} | 上边: {prev_entity_upper:.2f} 下边: {prev_entity_lower:.2f}")
|
||||
logger.info(LOG_PRICE + f"做多触发: {long_trigger:.2f} | 做空触发: {short_trigger:.2f} | 突破做多: {breakout_long:.2f}")
|
||||
if skip_short_by_upper_third:
|
||||
logger.info(LOG_PRICE + "上一根阴+当前阳(做多形态),不按上1/4做空")
|
||||
if skip_long_by_lower_third:
|
||||
logger.info(LOG_PRICE + "上一根阳+当前阴(做空形态),不按下1/4做多")
|
||||
self._last_signal_log_kline_id = current_kline_id
|
||||
|
||||
self._display_triggers = {"long_trigger": long_trigger, "short_trigger": short_trigger, "long_breakout": long_breakout, "short_breakout": short_breakout}
|
||||
self._display_triggers = {
|
||||
"long_trigger": long_trigger, "short_trigger": short_trigger,
|
||||
"long_breakout": breakout_long, "short_breakout": levels["breakout_short"]
|
||||
}
|
||||
|
||||
# 无持仓时检查开仓信号(做多看突破价,做空看做空触发价,与反手阈值一致便于回落即进空)
|
||||
ema20 = ind.get("ema20")
|
||||
|
||||
# 无持仓:穿越触发
|
||||
if self.start == 0:
|
||||
if current_price >= long_breakout and not skip_long_by_lower_third:
|
||||
logger.info(LOG_SIGNAL + f"触发做多 | 价 {current_price:.2f} >= 突破价 {long_breakout:.2f}")
|
||||
return ('long', long_breakout)
|
||||
elif current_price <= short_trigger and not skip_short_by_upper_third:
|
||||
logger.info(LOG_SIGNAL + f"触发做空 | 价 {current_price:.2f} <= 做空触发价 {short_trigger:.2f}")
|
||||
return ('short', short_trigger)
|
||||
if cross_up(prev_price, current_price, breakout_long) and not skip_long_by_lower_third:
|
||||
if self._allow_entry_by_ema20(1, current_price, ema20):
|
||||
logger.info(LOG_SIGNAL + f"穿越做多 | 上一轮 {prev_price:.2f} < 突破价 {breakout_long:.2f} <= 当前 {current_price:.2f}")
|
||||
return ('long', breakout_long)
|
||||
if cross_down(prev_price, current_price, short_trigger) and not skip_short_by_upper_third:
|
||||
if self._allow_entry_by_ema20(-1, current_price, ema20):
|
||||
logger.info(LOG_SIGNAL + f"穿越做空 | 上一轮 {prev_price:.2f} > 做空触发 {short_trigger:.2f} >= 当前 {current_price:.2f}")
|
||||
return ('short', short_trigger)
|
||||
return None
|
||||
|
||||
# 持仓时检查反手信号
|
||||
elif self.start == 1: # 持多仓
|
||||
if current_price <= short_trigger and not skip_short_by_upper_third:
|
||||
logger.info(LOG_SIGNAL + f"持多→反手做空 | 价 {current_price:.2f} <= 触发价 {short_trigger:.2f}")
|
||||
return ('reverse_short', short_trigger)
|
||||
# 持多 → 反手空
|
||||
if self.start == 1:
|
||||
if cross_down(prev_price, current_price, short_trigger) and not skip_short_by_upper_third:
|
||||
if self._pct_move(current_price, short_trigger) >= self.reverse_min_move_pct and self._allow_entry_by_ema20(-1, current_price, ema20):
|
||||
logger.info(LOG_SIGNAL + f"持多→穿越反手做空 | 触发价 {short_trigger:.2f}")
|
||||
return ('reverse_short', short_trigger)
|
||||
upper_shadow_pct = self.calculate_upper_shadow(prev_kline)
|
||||
if upper_shadow_pct > 0.01 and current_price <= prev_entity_lower:
|
||||
logger.info(LOG_SIGNAL + f"持多→反手做空 | 上阴线 {upper_shadow_pct:.4f}% 价<=实体下边 {prev_entity_lower:.2f}")
|
||||
return ('reverse_short', prev_entity_lower)
|
||||
if upper_shadow_pct > self.min_upper_shadow_pct and cross_down(prev_price, current_price, prev_entity_lower):
|
||||
if self._allow_entry_by_ema20(-1, current_price, ema20):
|
||||
logger.info(LOG_SIGNAL + f"持多→反手做空 | 上影线 {upper_shadow_pct:.2f}% 穿越实体下边 {prev_entity_lower:.2f}")
|
||||
return ('reverse_short', prev_entity_lower)
|
||||
return None
|
||||
|
||||
elif self.start == -1: # 持空仓
|
||||
if current_price >= long_trigger and not skip_long_by_lower_third:
|
||||
logger.info(LOG_SIGNAL + f"持空→反手做多 | 价 {current_price:.2f} >= 触发价 {long_trigger:.2f}")
|
||||
return ('reverse_long', long_trigger)
|
||||
# 持空 → 反手多
|
||||
if self.start == -1:
|
||||
if cross_up(prev_price, current_price, long_trigger) and not skip_long_by_lower_third:
|
||||
if self._pct_move(current_price, long_trigger) >= self.reverse_min_move_pct and self._allow_entry_by_ema20(1, current_price, ema20):
|
||||
logger.info(LOG_SIGNAL + f"持空→穿越反手做多 | 触发价 {long_trigger:.2f}")
|
||||
return ('reverse_long', long_trigger)
|
||||
lower_shadow_pct = self.calculate_lower_shadow(prev_kline)
|
||||
if lower_shadow_pct > 0.01 and current_price >= prev_entity_upper:
|
||||
logger.info(LOG_SIGNAL + f"持空→反手做多 | 下阴线 {lower_shadow_pct:.4f}% 价>=实体上边 {prev_entity_upper:.2f}")
|
||||
return ('reverse_long', prev_entity_upper)
|
||||
if lower_shadow_pct > self.min_lower_shadow_pct and cross_up(prev_price, current_price, prev_entity_upper):
|
||||
if self._allow_entry_by_ema20(1, current_price, ema20):
|
||||
logger.info(LOG_SIGNAL + f"持空→反手做多 | 下影线 {lower_shadow_pct:.2f}% 穿越实体上边 {prev_entity_upper:.2f}")
|
||||
return ('reverse_long', prev_entity_upper)
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
def _pct_move(self, curr_price: float, trigger_price: float) -> float:
|
||||
"""当前价相对触发价的变动百分比"""
|
||||
if not trigger_price or trigger_price <= 0:
|
||||
return 0.0
|
||||
return abs(curr_price - trigger_price) / trigger_price * 100.0
|
||||
|
||||
def can_open(self, current_kline_id):
|
||||
"""开仓前过滤(已删除开仓冷却,保留接口便于后续扩展)。"""
|
||||
return True
|
||||
|
||||
def can_reverse(self, current_price, trigger_price):
|
||||
"""反手前过滤:仅最小价差"""
|
||||
"""反手前过滤:仅最小价差(与 check_signal 内已做重复校验,保留双保险)"""
|
||||
if trigger_price and trigger_price > 0:
|
||||
move_pct = abs(current_price - trigger_price) / trigger_price * 100
|
||||
move_pct = self._pct_move(current_price, trigger_price)
|
||||
if move_pct < self.reverse_min_move_pct:
|
||||
self._log_throttled("reverse_move_small", LOG_SYSTEM + f"反手价差不足: {move_pct:.4f}% < {self.reverse_min_move_pct}%", interval=1.0)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def should_exit(self, current_price: float, kline_id: int, ind: dict, now_ts: float):
|
||||
"""
|
||||
是否应技术性平仓。使用 last_close 防针洗(可选)。
|
||||
返回 (True, reason_str) 或 (False, None)。
|
||||
"""
|
||||
if self.start == 0 or not self.use_ema_atr_exit:
|
||||
return False, None
|
||||
if not self.last_open_time or (now_ts - self.last_open_time) < self.min_hold_seconds:
|
||||
return False, None
|
||||
ema10 = ind.get("ema10")
|
||||
ema20 = ind.get("ema20")
|
||||
atr14 = ind.get("atr14")
|
||||
last_close = ind.get("last_close")
|
||||
ref_price = last_close if (self.exit_use_last_close and last_close is not None) else current_price
|
||||
|
||||
if self.start == 1:
|
||||
if ema10 is not None and ref_price < ema10:
|
||||
return True, "EXIT_LONG_EMA10"
|
||||
if self.use_ema20_force_exit and ema20 is not None and ref_price < ema20:
|
||||
return True, "EXIT_LONG_EMA20"
|
||||
if atr14 is not None and self._highest_since_entry is not None:
|
||||
dd = self._highest_since_entry - current_price
|
||||
if dd >= self.atr_multiplier * atr14:
|
||||
return True, "EXIT_LONG_ATR"
|
||||
elif self.start == -1:
|
||||
if ema10 is not None and ref_price > ema10:
|
||||
return True, "EXIT_SHORT_EMA10"
|
||||
if self.use_ema20_force_exit and ema20 is not None and ref_price > ema20:
|
||||
return True, "EXIT_SHORT_EMA20"
|
||||
if atr14 is not None and self._lowest_since_entry is not None:
|
||||
ru = current_price - self._lowest_since_entry
|
||||
if ru >= self.atr_multiplier * atr14:
|
||||
return True, "EXIT_SHORT_ATR"
|
||||
return False, None
|
||||
|
||||
def verify_no_position(self, max_retries=5, retry_interval=3):
|
||||
"""
|
||||
验证当前无持仓
|
||||
@@ -815,104 +907,69 @@ class BitmartFuturesTransaction:
|
||||
level="debug",
|
||||
)
|
||||
|
||||
kline_series = self.get_klines_series(35)
|
||||
ind = self.get_ema_atr_for_exit(kline_series)
|
||||
# 更新仪表盘左侧数据(供 Rich 展示)
|
||||
try:
|
||||
self._display_state["price"] = current_price
|
||||
self._display_state["position"] = self.start
|
||||
self._display_state["kline_id"] = current_kline_time
|
||||
self._display_state["unrealized_pnl"] = self.get_unrealized_pnl_usd()
|
||||
kline_series = self.get_klines_series(35)
|
||||
ema10, ema20, atr14 = self.get_ema_atr_for_exit(kline_series)
|
||||
self._display_state["ema10"] = ema10
|
||||
self._display_state["ema20"] = ema20
|
||||
self._display_state["atr14"] = atr14
|
||||
self._display_state["ema10"] = ind["ema10"]
|
||||
self._display_state["ema20"] = ind["ema20"]
|
||||
self._display_state["atr14"] = ind["atr14"]
|
||||
if getattr(self, "_display_triggers", None):
|
||||
self._display_state.update(self._display_triggers)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 3.5 平仓:EMA10 快退出、EMA20 趋势过滤强制减仓、1.1×ATR 追踪
|
||||
if self.start != 0:
|
||||
# 换线重置跟踪,有持仓时更新本K线内最高/最低价
|
||||
if self._candle_id_for_high_low != current_kline_time:
|
||||
self._candle_high_seen = None
|
||||
self._candle_low_seen = None
|
||||
self._candle_id_for_high_low = current_kline_time
|
||||
# 多头:先更新本 K 线最高价(供 ATR 追踪与后续回落逻辑用)
|
||||
if self.start == 1:
|
||||
self._candle_high_seen = max(self._candle_high_seen or 0, current_price)
|
||||
elif self.start == -1:
|
||||
self._candle_low_seen = min(self._candle_low_seen or float('inf'), current_price)
|
||||
# 3.5 更新自开仓以来最高/最低(真正 ATR 追踪,不随换线重置)
|
||||
if self.start == 1:
|
||||
self._highest_since_entry = max(self._highest_since_entry or 0, current_price)
|
||||
elif self.start == -1:
|
||||
self._lowest_since_entry = min(self._lowest_since_entry if self._lowest_since_entry is not None else float('inf'), current_price)
|
||||
|
||||
hold_sec = (time.time() - self.last_open_time) if self.last_open_time else 999999
|
||||
allow_technical_exit = hold_sec >= self.min_hold_seconds
|
||||
# 3.6 平仓:EMA10 / EMA20 强制 / ATR 追踪(ref 用 last_close 防针洗)
|
||||
now_ts = time.time()
|
||||
exit_ok, exit_reason = self.should_exit(current_price, current_kline_time, ind, now_ts)
|
||||
if exit_ok:
|
||||
logger.info(LOG_POSITION + f"技术性平仓 | 原因: {exit_reason}")
|
||||
self.平仓()
|
||||
self._last_exit_kline_id = current_kline_time
|
||||
self._highest_since_entry = None
|
||||
self._lowest_since_entry = None
|
||||
time.sleep(3)
|
||||
self._prev_price = current_price
|
||||
continue
|
||||
|
||||
# 多单平仓:EMA10 快退出 / EMA20 趋势过滤强制减仓 / 1.1×ATR 追踪(组合1:能赚点是点)
|
||||
if allow_technical_exit and self.start == 1 and self.use_ema_atr_exit:
|
||||
kline_series = self.get_klines_series(35)
|
||||
ema10, ema20, atr14 = self.get_ema_atr_for_exit(kline_series)
|
||||
# 1) 价格跌破 EMA10 → 先平(能赚点是点)
|
||||
if ema10 is not None and current_price < ema10:
|
||||
logger.info(LOG_POSITION + f"多单 EMA10 平仓 | 价 {current_price:.2f} 跌破 EMA10 {ema10:.2f}")
|
||||
self.平仓()
|
||||
self._last_exit_kline_id = current_kline_time
|
||||
self._candle_high_seen = None
|
||||
time.sleep(3)
|
||||
continue
|
||||
# 2) EMA20 趋势过滤:价格跌破 EMA20 → 强制减仓(方向过滤,不做先平触发)
|
||||
if self.use_ema20_filter and ema20 is not None and current_price < ema20:
|
||||
logger.info(LOG_POSITION + f"多单 EMA20 强制减仓 | 价 {current_price:.2f} 跌破 EMA20 {ema20:.2f}")
|
||||
self.平仓()
|
||||
self._last_exit_kline_id = current_kline_time
|
||||
self._candle_high_seen = None
|
||||
time.sleep(3)
|
||||
continue
|
||||
# 3) 从最高价回撤 ≥ 1.1×ATR(14) → 平仓
|
||||
if atr14 is not None and self._candle_high_seen and (self._candle_high_seen - current_price) >= self.atr_multiplier * atr14:
|
||||
logger.info(LOG_POSITION + f"多单 ATR 追踪止盈 | 最高 {self._candle_high_seen:.2f} 当前 {current_price:.2f} 回撤≥{self.atr_multiplier}×ATR={atr14:.2f}")
|
||||
self.平仓()
|
||||
self._last_exit_kline_id = current_kline_time
|
||||
self._candle_high_seen = None
|
||||
time.sleep(3)
|
||||
continue
|
||||
# 4. 四分之一价位与实体过滤
|
||||
levels = self.quarter_levels(
|
||||
prev_kline['open'], prev_kline['high'], prev_kline['low'], prev_kline['close'],
|
||||
current_kline['open']
|
||||
)
|
||||
if not self._entity_filter_passed(levels["entity"], prev_kline['close'], ind.get("atr14")):
|
||||
self._log_throttled("entity_small", LOG_PRICE + f"实体过小(实体={levels['entity']:.4f}),跳过信号", interval=2.0)
|
||||
self._prev_price = current_price
|
||||
time.sleep(0.1)
|
||||
continue
|
||||
|
||||
# 空单平仓:EMA10 快退出 / EMA20 趋势过滤强制减仓 / 1.1×ATR 追踪
|
||||
if allow_technical_exit and self.start == -1 and self.use_ema_atr_exit:
|
||||
kline_series = self.get_klines_series(35)
|
||||
ema10, ema20, atr14 = self.get_ema_atr_for_exit(kline_series)
|
||||
# 1) 价格涨破 EMA10 → 先平
|
||||
if ema10 is not None and current_price > ema10:
|
||||
logger.info(LOG_POSITION + f"空单 EMA10 平仓 | 价 {current_price:.2f} 涨破 EMA10 {ema10:.2f}")
|
||||
self.平仓()
|
||||
self._last_exit_kline_id = current_kline_time
|
||||
self._candle_low_seen = None
|
||||
time.sleep(3)
|
||||
continue
|
||||
# 2) EMA20 趋势过滤:价格涨破 EMA20 → 强制减仓
|
||||
if self.use_ema20_filter and ema20 is not None and current_price > ema20:
|
||||
logger.info(LOG_POSITION + f"空单 EMA20 强制减仓 | 价 {current_price:.2f} 涨破 EMA20 {ema20:.2f}")
|
||||
self.平仓()
|
||||
self._last_exit_kline_id = current_kline_time
|
||||
self._candle_low_seen = None
|
||||
time.sleep(3)
|
||||
continue
|
||||
# 3) 从最低价反弹 ≥ 1.1×ATR(14) → 平仓
|
||||
if atr14 is not None and self._candle_low_seen and (current_price - self._candle_low_seen) >= self.atr_multiplier * atr14:
|
||||
logger.info(LOG_POSITION + f"空单 ATR 追踪止盈 | 最低 {self._candle_low_seen:.2f} 当前 {current_price:.2f} 反弹≥{self.atr_multiplier}×ATR={atr14:.2f}")
|
||||
self.平仓()
|
||||
self._last_exit_kline_id = current_kline_time
|
||||
self._candle_low_seen = None
|
||||
time.sleep(3)
|
||||
continue
|
||||
# 4. 检查信号
|
||||
signal = self.check_signal(current_price, prev_kline, current_kline)
|
||||
# 开盘后延迟:避免开盘价即触发
|
||||
if self.open_trigger_delay_seconds > 0:
|
||||
kline_elapsed = time.time() - current_kline_time
|
||||
if kline_elapsed < self.open_trigger_delay_seconds:
|
||||
self._prev_price = current_price
|
||||
time.sleep(0.1)
|
||||
continue
|
||||
|
||||
# 5. 反手过滤:最小价差
|
||||
# 5. 检查信号(穿越触发 + EMA20 入场过滤已在 check_signal 内)
|
||||
signal = self.check_signal(current_price, prev_kline, current_kline, levels, ind)
|
||||
|
||||
# 5.5 反手过滤:最小价差(双保险)
|
||||
if signal and signal[0].startswith('reverse_'):
|
||||
if not self.can_reverse(current_price, signal[1]):
|
||||
signal = None
|
||||
|
||||
# 5.5 开仓过滤:当前K线已出场则等下一根K线再开仓
|
||||
# 5.6 开仓过滤:当前K线已出场则等下一根K线再开仓
|
||||
if signal and signal[0] in ('long', 'short'):
|
||||
if self._last_exit_kline_id == current_kline_time:
|
||||
self._log_throttled("same_kline_no_open", LOG_SYSTEM + "当前K线已出场,等下一根K线再开仓", interval=2.0)
|
||||
@@ -920,30 +977,20 @@ class BitmartFuturesTransaction:
|
||||
elif not self.can_open(current_kline_time):
|
||||
signal = None
|
||||
else:
|
||||
self._current_kline_id_for_open = current_kline_time # 供 execute_trade 成功后记录
|
||||
|
||||
# 5.6 EMA20 方向过滤:价在 EMA20 上方才开多,下方才开空(趋势过滤,暂停逆势开仓)
|
||||
if signal and self.use_ema20_filter:
|
||||
kline_series = self.get_klines_series(35)
|
||||
_, ema20, _ = self.get_ema_atr_for_exit(kline_series)
|
||||
if ema20 is not None:
|
||||
if signal[0] in ('long', 'reverse_long') and current_price <= ema20:
|
||||
self._log_throttled("ema20_no_long", LOG_SYSTEM + f"EMA20 方向过滤:价 {current_price:.2f} 未在 EMA20 {ema20:.2f} 上方,暂停开多", interval=2.0)
|
||||
signal = None
|
||||
elif signal[0] in ('short', 'reverse_short') and current_price >= ema20:
|
||||
self._log_throttled("ema20_no_short", LOG_SYSTEM + f"EMA20 方向过滤:价 {current_price:.2f} 未在 EMA20 {ema20:.2f} 下方,暂停开空", interval=2.0)
|
||||
signal = None
|
||||
self._current_kline_id_for_open = current_kline_time
|
||||
|
||||
# 6. 有信号则执行交易
|
||||
if signal:
|
||||
trade_success = self.execute_trade(signal)
|
||||
if trade_success:
|
||||
self._highest_since_entry = current_price
|
||||
self._lowest_since_entry = current_price
|
||||
logger.success(LOG_POSITION + f"交易执行完成: {signal[0]} | 当前持仓: {self.start}")
|
||||
page_start = True
|
||||
else:
|
||||
logger.warning(f"交易执行失败或被阻止: {signal[0]}")
|
||||
|
||||
# 短暂等待后继续循环(同一根K线遇到信号就操作)
|
||||
self._prev_price = current_price
|
||||
time.sleep(0.1)
|
||||
|
||||
if page_start:
|
||||
|
||||
Reference in New Issue
Block a user