diff --git a/account_risk_lib.py b/account_risk_lib.py index 77083de..4fa1c14 100644 --- a/account_risk_lib.py +++ b/account_risk_lib.py @@ -206,23 +206,23 @@ def _cooloff_until_ms(row) -> Optional[int]: return None -def _in_active_manual_cooloff(row, now_ms: int) -> bool: +def _journal_can_reduce_cooloff(row, pending, now_ms: int) -> bool: if int(_row_get(row, "daily_frozen") or 0) == 1: return False - if int(_row_get(row, "manual_close_count") or 0) < 1: - return False until_ms = _cooloff_until_ms(row) - return until_ms is not None and until_ms > now_ms - - -def _journal_can_reduce_cooloff(row, pending, now_ms: int) -> bool: + if until_ms is None or until_ms <= now_ms: + return False + journal_h = cooling_hours_manual_journal() + cooloff_h = float(_row_get(row, "cooloff_hours") or cooling_hours_manual()) + if cooloff_h <= journal_h + 1e-6: + return False if pending is not None: try: if int(pending) != 0: return True except (TypeError, ValueError): return True - return _in_active_manual_cooloff(row, now_ms) + return True def _journal_cooloff_until_ms(row, now_ms: int, journal_hours: float) -> int: @@ -384,6 +384,27 @@ def on_journal_saved( ) +def apply_manual_close_journal_cooloff( + conn, + *, + early_exit_note: str, + trading_day: str, + now: Optional[datetime] = None, +) -> None: + """核对修改或复盘:手动平仓 + 说明后尝试将 4h 冷静期降为 1h。""" + note = (early_exit_note or "").strip() + if not note: + return + on_journal_saved( + conn, + early_exit_trigger="手动平仓", + early_exit_note=note, + mood_issues_raw="", + trading_day=trading_day, + now=now, + ) + + def compute_account_risk_status( conn, *, @@ -420,8 +441,9 @@ def compute_account_risk_status( status = STATUS_DAILY reason = f"账户今日已冻结(手动平仓 {manual_close_count} 次或复盘情绪标签)" elif cooloff_until_ms is not None and cooloff_until_ms > now_ms: - h = int(cooloff_hours or cooling_hours_manual()) - status = STATUS_FREEZE_1H if h <= 1 else STATUS_FREEZE_4H + h = float(cooloff_hours or cooling_hours_manual()) + journal_h = cooling_hours_manual_journal() + status = STATUS_FREEZE_1H if h <= journal_h + 1e-6 else STATUS_FREEZE_4H until_str = _ms_to_local_str(cooloff_until_ms, fmt_local_ms) if fmt_local_ms else None label = STATUS_LABELS[status] reason = f"账户{label}中" diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py index 90da23b..ac64e92 100644 --- a/crypto_monitor_binance/app.py +++ b/crypto_monitor_binance/app.py @@ -9200,6 +9200,15 @@ def api_trade_record_review_update(): WHERE id=?""", tuple(base_params + [rec_id]), ) + if reviewed_result == "手动平仓" and reviewed_miss_reason: + from account_risk_lib import apply_manual_close_journal_cooloff + + apply_manual_close_journal_cooloff( + conn, + early_exit_note=reviewed_miss_reason, + trading_day=get_trading_day(), + now=app_now(), + ) conn.commit() conn.close() return jsonify({"ok": True, "id": rec_id, "actual_rr": actual_rr, "hold_minutes": hold_minutes}) diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index 25e2196..f624eab 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -9144,6 +9144,15 @@ def api_trade_record_review_update(): WHERE id=?""", tuple(base_params + [rec_id]), ) + if reviewed_result == "手动平仓" and reviewed_miss_reason: + from account_risk_lib import apply_manual_close_journal_cooloff + + apply_manual_close_journal_cooloff( + conn, + early_exit_note=reviewed_miss_reason, + trading_day=get_trading_day(), + now=app_now(), + ) conn.commit() conn.close() return jsonify({"ok": True, "id": rec_id, "actual_rr": actual_rr, "hold_minutes": hold_minutes}) diff --git a/crypto_monitor_gate_bot/app.py b/crypto_monitor_gate_bot/app.py index b21ecb2..c790983 100644 --- a/crypto_monitor_gate_bot/app.py +++ b/crypto_monitor_gate_bot/app.py @@ -9144,6 +9144,15 @@ def api_trade_record_review_update(): WHERE id=?""", tuple(base_params + [rec_id]), ) + if reviewed_result == "手动平仓" and reviewed_miss_reason: + from account_risk_lib import apply_manual_close_journal_cooloff + + apply_manual_close_journal_cooloff( + conn, + early_exit_note=reviewed_miss_reason, + trading_day=get_trading_day(), + now=app_now(), + ) conn.commit() conn.close() return jsonify({"ok": True, "id": rec_id, "actual_rr": actual_rr, "hold_minutes": hold_minutes}) diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index 6082d2e..f839349 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -8682,6 +8682,15 @@ def api_trade_record_review_update(): WHERE id=?""", tuple(base_params + [rec_id]), ) + if reviewed_result == "手动平仓" and reviewed_miss_reason: + from account_risk_lib import apply_manual_close_journal_cooloff + + apply_manual_close_journal_cooloff( + conn, + early_exit_note=reviewed_miss_reason, + trading_day=get_trading_day(), + now=app_now(), + ) conn.commit() conn.close() return jsonify({"ok": True, "id": rec_id, "actual_rr": actual_rr, "hold_minutes": hold_minutes}) diff --git a/docs/account-risk-cooldown.md b/docs/account-risk-cooldown.md index fbceb3b..18a908e 100644 --- a/docs/account-risk-cooldown.md +++ b/docs/account-risk-cooldown.md @@ -45,8 +45,10 @@ **复盘缩短 1h 说明**: - 复盘表单须选 **离场触发 = 手动平仓**,并在 **离场补充** 填写说明(不是下方「备注」栏)。 -- 中控全平/实例手动平仓后,只要账户仍在 4h 冷静期内,完成上述复盘即可降为 1h。 +- 或在交易记录 **核对修改** 中:结果填 **手动平仓**,**备注** 填写说明(勾选「核对修改」后点按钮)。 +- 中控全平/实例手动平仓后,只要账户仍在 4h 冷静期内,完成上述任一种即可降为 1h。 - 若在平仓后超过 1h 才复盘,则从复盘保存时刻起再计 1h(不会延长原 4h 窗口)。 +- 保存后需 **重启对应实例**(`pm2 restart crypto_gate` 等)并刷新页面,状态才会更新。 ## 环境变量 diff --git a/tests/test_account_risk_lib.py b/tests/test_account_risk_lib.py index 9832fdd..1636055 100644 --- a/tests/test_account_risk_lib.py +++ b/tests/test_account_risk_lib.py @@ -150,6 +150,34 @@ class AccountRiskLibTests(unittest.TestCase): st = compute_account_risk_status(conn, trading_day="2026-06-14", now=now) self.assertEqual(st["status"], STATUS_FREEZE_1H) + def test_journal_reduces_when_manual_count_cleared_but_cooloff_active(self): + conn = _mem_conn() + now = datetime(2026, 6, 15, 10, 0, 0) + now_ms = int(now.replace(tzinfo=timezone.utc).timestamp() * 1000) + close_ms = now_ms - 3600 * 1000 + until_ms = close_ms + 4 * 3600 * 1000 + conn.execute( + """UPDATE account_risk_state SET + trading_day='2026-06-15', + manual_close_count=0, + cooloff_until_ms=?, + cooloff_hours=4, + last_close_at_ms=?, + daily_frozen=0 + WHERE id=1""", + (until_ms, close_ms), + ) + on_journal_saved( + conn, + early_exit_trigger="手动平仓", + early_exit_note="切日后补复盘", + mood_issues_raw="", + trading_day="2026-06-15", + now=now, + ) + st = compute_account_risk_status(conn, trading_day="2026-06-15", now=now) + self.assertEqual(st["status"], STATUS_FREEZE_1H) + def test_journal_late_save_still_gets_1h_from_now(self): conn = _mem_conn() close_at = datetime(2026, 6, 14, 12, 0, 0)