From 9d1986d7710ed2f3f17b570135b4429d6b567513 Mon Sep 17 00:00:00 2001 From: dekun Date: Wed, 24 Jun 2026 02:04:09 +0800 Subject: [PATCH] Exclude trend and roll monitors from position-limit freeze count. Co-authored-by: Cursor --- account_risk_lib.py | 2 +- crypto_monitor_binance/app.py | 4 +- crypto_monitor_gate/app.py | 4 +- crypto_monitor_gate_bot/app.py | 4 +- crypto_monitor_okx/app.py | 4 +- docs/account-risk-cooldown.md | 2 +- strategy_trade_labels.py | 37 +++++++++++++ tests/test_account_risk_lib.py | 1 + tests/test_position_limit_count.py | 88 ++++++++++++++++++++++++++++++ 9 files changed, 140 insertions(+), 6 deletions(-) create mode 100644 tests/test_position_limit_count.py diff --git a/account_risk_lib.py b/account_risk_lib.py index f474c3c..58feff4 100644 --- a/account_risk_lib.py +++ b/account_risk_lib.py @@ -721,7 +721,7 @@ def apply_position_limit_risk( out["status"] = STATUS_FREEZE_POSITION out["status_label"] = STATUS_LABELS[STATUS_FREEZE_POSITION] out["can_trade"] = False - out["reason"] = f"已达最大持仓数({ac}/{mx}),平仓前不可新开" + out["reason"] = f"已达最大持仓数({ac}/{mx},不含趋势回调/顺势加仓),平仓前不可新开" out["position_limit_frozen"] = True out["freeze_until_ms"] = None out["freeze_remaining_sec"] = 0 diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py index 1f1c47a..732ca0c 100644 --- a/crypto_monitor_binance/app.py +++ b/crypto_monitor_binance/app.py @@ -1557,9 +1557,11 @@ def hub_account_risk_status(conn): fmt_local_ms=ms_to_app_local_str, ) st = enrich_risk_status_countdown(st, now=now, daily_reset_hour=TRADING_DAY_RESET_HOUR) + from strategy_trade_labels import count_position_limit_active_monitors + return apply_position_limit_risk( st, - get_active_position_count(conn), + count_position_limit_active_monitors(conn), max_active_positions=MAX_ACTIVE_POSITIONS, ) diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index abf3b1f..7dff966 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -1548,9 +1548,11 @@ def hub_account_risk_status(conn): fmt_local_ms=ms_to_app_local_str, ) st = enrich_risk_status_countdown(st, now=now, daily_reset_hour=TRADING_DAY_RESET_HOUR) + from strategy_trade_labels import count_position_limit_active_monitors + return apply_position_limit_risk( st, - get_active_position_count(conn), + count_position_limit_active_monitors(conn), max_active_positions=MAX_ACTIVE_POSITIONS, ) diff --git a/crypto_monitor_gate_bot/app.py b/crypto_monitor_gate_bot/app.py index 706a19c..dcb0ed2 100644 --- a/crypto_monitor_gate_bot/app.py +++ b/crypto_monitor_gate_bot/app.py @@ -1548,9 +1548,11 @@ def hub_account_risk_status(conn): fmt_local_ms=ms_to_app_local_str, ) st = enrich_risk_status_countdown(st, now=now, daily_reset_hour=TRADING_DAY_RESET_HOUR) + from strategy_trade_labels import count_position_limit_active_monitors + return apply_position_limit_risk( st, - get_active_position_count(conn), + count_position_limit_active_monitors(conn), max_active_positions=MAX_ACTIVE_POSITIONS, ) diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index 7d9c277..c4a65e1 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -1535,9 +1535,11 @@ def hub_account_risk_status(conn): fmt_local_ms=ms_to_app_local_str, ) st = enrich_risk_status_countdown(st, now=now, daily_reset_hour=TRADING_DAY_RESET_HOUR) + from strategy_trade_labels import count_position_limit_active_monitors + return apply_position_limit_risk( st, - get_active_position_count(conn), + count_position_limit_active_monitors(conn), max_active_positions=MAX_ACTIVE_POSITIONS, ) diff --git a/docs/account-risk-cooldown.md b/docs/account-risk-cooldown.md index 4f0e93b..ddfaca8 100644 --- a/docs/account-risk-cooldown.md +++ b/docs/account-risk-cooldown.md @@ -106,7 +106,7 @@ APP_TIMEZONE=Asia/Shanghai | `freeze_until_ms` | 倒计时结束时间戳(日冻结为下一交易日切点) | | `freeze_remaining_sec` | 服务端计算的剩余秒数(供调试) | -**仓位上限冻结**:当活跃持仓数 ≥ 实例 `.env` 的 `MAX_ACTIVE_POSITIONS`(默认 1)且账户无时间类冻结时,徽章显示 **仓位上限冻结**;平仓后自动恢复 **正常**。时间冻结(1h/4h/日)优先展示。 +**仓位上限冻结**:当 **计入上限的** 活跃持仓数(不含趋势回调、顺势加仓)≥ 实例 `.env` 的 `MAX_ACTIVE_POSITIONS`(默认 1)且账户无时间类冻结时,徽章显示 **仓位上限冻结**;相关策略单平仓后或仅存在策略持仓时不会触发该冻结。时间冻结(1h/4h/日)优先展示。 ## 前端倒计时 diff --git a/strategy_trade_labels.py b/strategy_trade_labels.py index b510f76..f80bbb3 100644 --- a/strategy_trade_labels.py +++ b/strategy_trade_labels.py @@ -139,3 +139,40 @@ def entry_reason_for_monitor_type(monitor_type: str | None) -> str: if mt == MONITOR_TYPE_ROLL: return ENTRY_REASON_ROLL return "" + + +def order_monitor_excluded_from_position_limit(conn, row) -> bool: + """趋势回调 / 顺势加仓不计入 MAX_ACTIVE_POSITIONS 与仓位上限冻结。""" + if order_monitor_source_type(row) in (MONITOR_TYPE_TREND_PULLBACK, MONITOR_TYPE_ROLL): + return True + oid = None + try: + keys = row.keys() if hasattr(row, "keys") else [] + if "id" in keys and row["id"] is not None: + oid = int(row["id"]) + except Exception: + oid = None + if oid and oid > 0: + try: + hit = conn.execute( + "SELECT 1 FROM roll_groups WHERE order_monitor_id=? AND status='active' LIMIT 1", + (oid,), + ).fetchone() + if hit is not None: + return True + except Exception: + pass + return False + + +def count_position_limit_active_monitors(conn) -> int: + """计入仓位上限冻结的活跃监控数(不含趋势回调、顺势加仓)。""" + try: + rows = conn.execute("SELECT * FROM order_monitors WHERE status='active'").fetchall() + except Exception: + return 0 + n = 0 + for row in rows: + if not order_monitor_excluded_from_position_limit(conn, row): + n += 1 + return n diff --git a/tests/test_account_risk_lib.py b/tests/test_account_risk_lib.py index 430b8af..746002d 100644 --- a/tests/test_account_risk_lib.py +++ b/tests/test_account_risk_lib.py @@ -499,6 +499,7 @@ class AccountRiskLibTests(unittest.TestCase): self.assertEqual(st["status_label"], "仓位上限冻结") self.assertFalse(st["can_trade"]) self.assertIn("2/2", st["reason"]) + self.assertIn("不含趋势回调", st["reason"]) self.assertEqual(st["max_active_positions"], 2) def test_position_limit_normal_when_under_cap(self): diff --git a/tests/test_position_limit_count.py b/tests/test_position_limit_count.py new file mode 100644 index 0000000..38e315c --- /dev/null +++ b/tests/test_position_limit_count.py @@ -0,0 +1,88 @@ +import sqlite3 +import unittest + +from strategy_db import init_strategy_tables +from strategy_trade_labels import ( + MONITOR_TYPE_ROLL, + MONITOR_TYPE_TREND_PULLBACK, + count_position_limit_active_monitors, +) + + +def _mem_conn(): + conn = sqlite3.connect(":memory:") + conn.row_factory = sqlite3.Row + conn.execute( + """CREATE TABLE order_monitors ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + symbol TEXT, + direction TEXT, + status TEXT, + monitor_type TEXT, + key_signal_type TEXT, + trend_plan_id INTEGER + )""" + ) + init_strategy_tables(conn) + return conn + + +class PositionLimitCountTests(unittest.TestCase): + def test_regular_monitor_counts(self): + conn = _mem_conn() + conn.execute( + "INSERT INTO order_monitors (symbol, status, monitor_type) VALUES ('ETH/USDT', 'active', '下单监控')" + ) + conn.commit() + self.assertEqual(count_position_limit_active_monitors(conn), 1) + + def test_trend_pullback_excluded(self): + conn = _mem_conn() + conn.execute( + """INSERT INTO order_monitors + (symbol, status, monitor_type, trend_plan_id) + VALUES ('ETH/USDT', 'active', ?, 12)""", + (MONITOR_TYPE_TREND_PULLBACK,), + ) + conn.commit() + self.assertEqual(count_position_limit_active_monitors(conn), 0) + + def test_roll_monitor_type_excluded(self): + conn = _mem_conn() + conn.execute( + "INSERT INTO order_monitors (symbol, status, monitor_type) VALUES ('ETH/USDT', 'active', ?)", + (MONITOR_TYPE_ROLL,), + ) + conn.commit() + self.assertEqual(count_position_limit_active_monitors(conn), 0) + + def test_active_roll_group_excludes_monitor(self): + conn = _mem_conn() + conn.execute( + "INSERT INTO order_monitors (id, symbol, status, monitor_type) VALUES (1, 'ETH/USDT', 'active', '下单监控')" + ) + conn.execute( + """INSERT INTO roll_groups + (order_monitor_id, symbol, direction, status) + VALUES (1, 'ETH/USDT', 'long', 'active')""" + ) + conn.commit() + self.assertEqual(count_position_limit_active_monitors(conn), 0) + + def test_mixed_monitors(self): + conn = _mem_conn() + conn.execute( + "INSERT INTO order_monitors (symbol, status, monitor_type) VALUES ('BTC/USDT', 'active', '下单监控')" + ) + conn.execute( + """INSERT INTO order_monitors + (symbol, status, monitor_type, trend_plan_id) + VALUES ('ETH/USDT', 'active', ?, 3)""", + (MONITOR_TYPE_TREND_PULLBACK,), + ) + conn.commit() + self.assertEqual(count_position_limit_active_monitors(conn), 1) + + +if __name__ == "__main__": + unittest.main()