Exclude trend and roll monitors from position-limit freeze count.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+1
-1
@@ -721,7 +721,7 @@ def apply_position_limit_risk(
|
|||||||
out["status"] = STATUS_FREEZE_POSITION
|
out["status"] = STATUS_FREEZE_POSITION
|
||||||
out["status_label"] = STATUS_LABELS[STATUS_FREEZE_POSITION]
|
out["status_label"] = STATUS_LABELS[STATUS_FREEZE_POSITION]
|
||||||
out["can_trade"] = False
|
out["can_trade"] = False
|
||||||
out["reason"] = f"已达最大持仓数({ac}/{mx}),平仓前不可新开"
|
out["reason"] = f"已达最大持仓数({ac}/{mx},不含趋势回调/顺势加仓),平仓前不可新开"
|
||||||
out["position_limit_frozen"] = True
|
out["position_limit_frozen"] = True
|
||||||
out["freeze_until_ms"] = None
|
out["freeze_until_ms"] = None
|
||||||
out["freeze_remaining_sec"] = 0
|
out["freeze_remaining_sec"] = 0
|
||||||
|
|||||||
@@ -1557,9 +1557,11 @@ def hub_account_risk_status(conn):
|
|||||||
fmt_local_ms=ms_to_app_local_str,
|
fmt_local_ms=ms_to_app_local_str,
|
||||||
)
|
)
|
||||||
st = enrich_risk_status_countdown(st, now=now, daily_reset_hour=TRADING_DAY_RESET_HOUR)
|
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(
|
return apply_position_limit_risk(
|
||||||
st,
|
st,
|
||||||
get_active_position_count(conn),
|
count_position_limit_active_monitors(conn),
|
||||||
max_active_positions=MAX_ACTIVE_POSITIONS,
|
max_active_positions=MAX_ACTIVE_POSITIONS,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1548,9 +1548,11 @@ def hub_account_risk_status(conn):
|
|||||||
fmt_local_ms=ms_to_app_local_str,
|
fmt_local_ms=ms_to_app_local_str,
|
||||||
)
|
)
|
||||||
st = enrich_risk_status_countdown(st, now=now, daily_reset_hour=TRADING_DAY_RESET_HOUR)
|
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(
|
return apply_position_limit_risk(
|
||||||
st,
|
st,
|
||||||
get_active_position_count(conn),
|
count_position_limit_active_monitors(conn),
|
||||||
max_active_positions=MAX_ACTIVE_POSITIONS,
|
max_active_positions=MAX_ACTIVE_POSITIONS,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1548,9 +1548,11 @@ def hub_account_risk_status(conn):
|
|||||||
fmt_local_ms=ms_to_app_local_str,
|
fmt_local_ms=ms_to_app_local_str,
|
||||||
)
|
)
|
||||||
st = enrich_risk_status_countdown(st, now=now, daily_reset_hour=TRADING_DAY_RESET_HOUR)
|
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(
|
return apply_position_limit_risk(
|
||||||
st,
|
st,
|
||||||
get_active_position_count(conn),
|
count_position_limit_active_monitors(conn),
|
||||||
max_active_positions=MAX_ACTIVE_POSITIONS,
|
max_active_positions=MAX_ACTIVE_POSITIONS,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1535,9 +1535,11 @@ def hub_account_risk_status(conn):
|
|||||||
fmt_local_ms=ms_to_app_local_str,
|
fmt_local_ms=ms_to_app_local_str,
|
||||||
)
|
)
|
||||||
st = enrich_risk_status_countdown(st, now=now, daily_reset_hour=TRADING_DAY_RESET_HOUR)
|
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(
|
return apply_position_limit_risk(
|
||||||
st,
|
st,
|
||||||
get_active_position_count(conn),
|
count_position_limit_active_monitors(conn),
|
||||||
max_active_positions=MAX_ACTIVE_POSITIONS,
|
max_active_positions=MAX_ACTIVE_POSITIONS,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ APP_TIMEZONE=Asia/Shanghai
|
|||||||
| `freeze_until_ms` | 倒计时结束时间戳(日冻结为下一交易日切点) |
|
| `freeze_until_ms` | 倒计时结束时间戳(日冻结为下一交易日切点) |
|
||||||
| `freeze_remaining_sec` | 服务端计算的剩余秒数(供调试) |
|
| `freeze_remaining_sec` | 服务端计算的剩余秒数(供调试) |
|
||||||
|
|
||||||
**仓位上限冻结**:当活跃持仓数 ≥ 实例 `.env` 的 `MAX_ACTIVE_POSITIONS`(默认 1)且账户无时间类冻结时,徽章显示 **仓位上限冻结**;平仓后自动恢复 **正常**。时间冻结(1h/4h/日)优先展示。
|
**仓位上限冻结**:当 **计入上限的** 活跃持仓数(不含趋势回调、顺势加仓)≥ 实例 `.env` 的 `MAX_ACTIVE_POSITIONS`(默认 1)且账户无时间类冻结时,徽章显示 **仓位上限冻结**;相关策略单平仓后或仅存在策略持仓时不会触发该冻结。时间冻结(1h/4h/日)优先展示。
|
||||||
|
|
||||||
## 前端倒计时
|
## 前端倒计时
|
||||||
|
|
||||||
|
|||||||
@@ -139,3 +139,40 @@ def entry_reason_for_monitor_type(monitor_type: str | None) -> str:
|
|||||||
if mt == MONITOR_TYPE_ROLL:
|
if mt == MONITOR_TYPE_ROLL:
|
||||||
return ENTRY_REASON_ROLL
|
return ENTRY_REASON_ROLL
|
||||||
return ""
|
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
|
||||||
|
|||||||
@@ -499,6 +499,7 @@ class AccountRiskLibTests(unittest.TestCase):
|
|||||||
self.assertEqual(st["status_label"], "仓位上限冻结")
|
self.assertEqual(st["status_label"], "仓位上限冻结")
|
||||||
self.assertFalse(st["can_trade"])
|
self.assertFalse(st["can_trade"])
|
||||||
self.assertIn("2/2", st["reason"])
|
self.assertIn("2/2", st["reason"])
|
||||||
|
self.assertIn("不含趋势回调", st["reason"])
|
||||||
self.assertEqual(st["max_active_positions"], 2)
|
self.assertEqual(st["max_active_positions"], 2)
|
||||||
|
|
||||||
def test_position_limit_normal_when_under_cap(self):
|
def test_position_limit_normal_when_under_cap(self):
|
||||||
|
|||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user