fix(risk): trigger cooldown only on user-initiated closes
Remove external-close risk hooks; register user_instance, user_hub, and user_trend_stop via hub API and trend stop; update docs and tests. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5,6 +5,9 @@ from datetime import datetime, timezone
|
||||
from unittest import mock
|
||||
|
||||
from account_risk_lib import (
|
||||
CLOSE_SOURCE_USER_HUB,
|
||||
CLOSE_SOURCE_USER_INSTANCE,
|
||||
CLOSE_SOURCE_USER_TREND_STOP,
|
||||
STATUS_DAILY,
|
||||
STATUS_FREEZE_1H,
|
||||
STATUS_FREEZE_4H,
|
||||
@@ -12,11 +15,10 @@ from account_risk_lib import (
|
||||
account_risk_blocks_trading,
|
||||
compute_account_risk_status,
|
||||
ensure_account_risk_schema,
|
||||
on_external_close,
|
||||
on_journal_saved,
|
||||
on_manual_close,
|
||||
on_user_initiated_close,
|
||||
parse_mood_issues,
|
||||
should_apply_external_close_risk,
|
||||
)
|
||||
|
||||
|
||||
@@ -33,7 +35,6 @@ class AccountRiskLibTests(unittest.TestCase):
|
||||
self.env_patch.start()
|
||||
os.environ["RISK_CONTROL_ENABLED"] = "1"
|
||||
os.environ["RISK_COOLING_HOURS_MANUAL"] = "4"
|
||||
os.environ["RISK_COOLING_HOURS_EXTERNAL"] = "4"
|
||||
os.environ["RISK_COOLING_HOURS_MANUAL_JOURNAL"] = "1"
|
||||
os.environ["RISK_MANUAL_CLOSE_DAILY_LIMIT"] = "2"
|
||||
os.environ["RISK_MOOD_ISSUES_DAILY_FREEZE"] = "1"
|
||||
@@ -41,17 +42,13 @@ class AccountRiskLibTests(unittest.TestCase):
|
||||
def tearDown(self):
|
||||
self.env_patch.stop()
|
||||
|
||||
def test_should_apply_external_close_risk_only_external(self):
|
||||
self.assertTrue(should_apply_external_close_risk("外部平仓"))
|
||||
self.assertFalse(should_apply_external_close_risk("止盈"))
|
||||
self.assertFalse(should_apply_external_close_risk("手动平仓"))
|
||||
|
||||
def test_manual_close_sets_4h_cooloff(self):
|
||||
def test_user_instance_sets_4h_cooloff(self):
|
||||
conn = _mem_conn()
|
||||
now = datetime(2026, 6, 14, 12, 0, 0)
|
||||
close_ms = int(now.replace(tzinfo=timezone.utc).timestamp() * 1000)
|
||||
on_manual_close(
|
||||
on_user_initiated_close(
|
||||
conn,
|
||||
source=CLOSE_SOURCE_USER_INSTANCE,
|
||||
trade_record_id=101,
|
||||
closed_at_ms=close_ms,
|
||||
trading_day="2026-06-14",
|
||||
@@ -60,20 +57,60 @@ class AccountRiskLibTests(unittest.TestCase):
|
||||
st = compute_account_risk_status(conn, trading_day="2026-06-14", now=now)
|
||||
self.assertEqual(st["status"], STATUS_FREEZE_4H)
|
||||
self.assertFalse(st["can_trade"])
|
||||
self.assertEqual(st["manual_close_count"], 1)
|
||||
ok, reason = account_risk_blocks_trading(conn, trading_day="2026-06-14", now=now)
|
||||
self.assertFalse(ok)
|
||||
self.assertIn("冻结", reason)
|
||||
|
||||
def test_second_manual_close_daily_freeze(self):
|
||||
def test_invalid_source_ignored(self):
|
||||
conn = _mem_conn()
|
||||
now = datetime(2026, 6, 14, 12, 0, 0)
|
||||
on_user_initiated_close(
|
||||
conn,
|
||||
source="exchange_tpsl",
|
||||
trading_day="2026-06-14",
|
||||
now=now,
|
||||
)
|
||||
st = compute_account_risk_status(conn, trading_day="2026-06-14", now=now)
|
||||
self.assertEqual(st["status"], STATUS_NORMAL)
|
||||
|
||||
def test_second_user_close_daily_freeze(self):
|
||||
conn = _mem_conn()
|
||||
now = datetime(2026, 6, 14, 12, 0, 0)
|
||||
close_ms = int(now.replace(tzinfo=timezone.utc).timestamp() * 1000)
|
||||
on_manual_close(conn, trade_record_id=1, closed_at_ms=close_ms, trading_day="2026-06-14", now=now)
|
||||
on_manual_close(conn, trade_record_id=2, closed_at_ms=close_ms + 1000, trading_day="2026-06-14", now=now)
|
||||
on_user_initiated_close(
|
||||
conn, source=CLOSE_SOURCE_USER_HUB, closed_at_ms=close_ms, trading_day="2026-06-14", now=now
|
||||
)
|
||||
on_user_initiated_close(
|
||||
conn, source=CLOSE_SOURCE_USER_HUB, closed_at_ms=close_ms + 1000, trading_day="2026-06-14", now=now
|
||||
)
|
||||
st = compute_account_risk_status(conn, trading_day="2026-06-14", now=now)
|
||||
self.assertEqual(st["status"], STATUS_DAILY)
|
||||
self.assertTrue(st["daily_frozen"])
|
||||
|
||||
def test_hub_close_all_count(self):
|
||||
conn = _mem_conn()
|
||||
now = datetime(2026, 6, 14, 12, 0, 0)
|
||||
close_ms = int(now.replace(tzinfo=timezone.utc).timestamp() * 1000)
|
||||
on_user_initiated_close(
|
||||
conn,
|
||||
source=CLOSE_SOURCE_USER_HUB,
|
||||
closed_at_ms=close_ms,
|
||||
trading_day="2026-06-14",
|
||||
now=now,
|
||||
count=2,
|
||||
)
|
||||
st = compute_account_risk_status(conn, trading_day="2026-06-14", now=now)
|
||||
self.assertEqual(st["manual_close_count"], 2)
|
||||
self.assertEqual(st["status"], STATUS_DAILY)
|
||||
|
||||
def test_trend_stop_counts_as_manual(self):
|
||||
conn = _mem_conn()
|
||||
now = datetime(2026, 6, 14, 12, 0, 0)
|
||||
on_user_initiated_close(
|
||||
conn,
|
||||
source=CLOSE_SOURCE_USER_TREND_STOP,
|
||||
trading_day="2026-06-14",
|
||||
now=now,
|
||||
)
|
||||
st = compute_account_risk_status(conn, trading_day="2026-06-14", now=now)
|
||||
self.assertEqual(st["manual_close_count"], 1)
|
||||
self.assertEqual(st["status"], STATUS_FREEZE_4H)
|
||||
|
||||
def test_journal_manual_with_note_reduces_to_1h(self):
|
||||
conn = _mem_conn()
|
||||
@@ -105,23 +142,16 @@ class AccountRiskLibTests(unittest.TestCase):
|
||||
st = compute_account_risk_status(conn, trading_day="2026-06-14", now=now)
|
||||
self.assertEqual(st["status"], STATUS_DAILY)
|
||||
|
||||
def test_external_close_4h_cooloff(self):
|
||||
conn = _mem_conn()
|
||||
now = datetime(2026, 6, 14, 12, 0, 0)
|
||||
close_ms = int(now.replace(tzinfo=timezone.utc).timestamp() * 1000)
|
||||
on_external_close(conn, closed_at_ms=close_ms, trading_day="2026-06-14", now=now)
|
||||
st = compute_account_risk_status(conn, trading_day="2026-06-14", now=now)
|
||||
self.assertEqual(st["status"], STATUS_FREEZE_4H)
|
||||
|
||||
def test_cooloff_expired_returns_normal(self):
|
||||
conn = _mem_conn()
|
||||
start = datetime(2026, 6, 14, 8, 0, 0)
|
||||
close_ms = int(start.replace(tzinfo=timezone.utc).timestamp() * 1000)
|
||||
on_external_close(conn, closed_at_ms=close_ms, trading_day="2026-06-14", now=start)
|
||||
on_user_initiated_close(
|
||||
conn, source=CLOSE_SOURCE_USER_INSTANCE, closed_at_ms=close_ms, trading_day="2026-06-14", now=start
|
||||
)
|
||||
later = datetime(2026, 6, 14, 13, 0, 0)
|
||||
st = compute_account_risk_status(conn, trading_day="2026-06-14", now=later)
|
||||
self.assertEqual(st["status"], STATUS_NORMAL)
|
||||
self.assertTrue(st["can_trade"])
|
||||
|
||||
def test_trading_day_reset_clears_daily_frozen(self):
|
||||
conn = _mem_conn()
|
||||
@@ -137,7 +167,6 @@ class AccountRiskLibTests(unittest.TestCase):
|
||||
next_day = datetime(2026, 6, 15, 8, 0, 0)
|
||||
st = compute_account_risk_status(conn, trading_day="2026-06-15", now=next_day)
|
||||
self.assertEqual(st["status"], STATUS_NORMAL)
|
||||
self.assertFalse(st["daily_frozen"])
|
||||
|
||||
def test_parse_mood_issues_filters_unknown(self):
|
||||
self.assertEqual(parse_mood_issues("怕踏空,未知标签,扛单"), ["怕踏空", "扛单"])
|
||||
@@ -146,11 +175,14 @@ class AccountRiskLibTests(unittest.TestCase):
|
||||
os.environ["RISK_CONTROL_ENABLED"] = "0"
|
||||
conn = _mem_conn()
|
||||
now = datetime(2026, 6, 14, 12, 0, 0)
|
||||
close_ms = int(now.replace(tzinfo=timezone.utc).timestamp() * 1000)
|
||||
on_manual_close(conn, trade_record_id=1, closed_at_ms=close_ms, trading_day="2026-06-14", now=now)
|
||||
on_user_initiated_close(
|
||||
conn, source=CLOSE_SOURCE_USER_INSTANCE, trading_day="2026-06-14", now=now
|
||||
)
|
||||
st = compute_account_risk_status(conn, trading_day="2026-06-14", now=now)
|
||||
self.assertFalse(st["enabled"])
|
||||
self.assertTrue(st["can_trade"])
|
||||
ok, _ = account_risk_blocks_trading(conn, trading_day="2026-06-14", now=now)
|
||||
self.assertTrue(ok)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user