feat: add per-account daily open hard limit across all exchanges

Enforce optional DAILY_OPEN_HARD_LIMIT in precheck_risk and can_trade, keep AI alerts at DAILY_OPEN_ALERT_THRESHOLD, and document env setup for all four instances.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-09 15:46:18 +08:00
parent f7d94f67d7
commit 24a86a710c
17 changed files with 698 additions and 77 deletions
+60 -15
View File
@@ -252,7 +252,18 @@ ORDER_CHART_ENABLED = os.getenv("ORDER_CHART_ENABLED", "true").lower() == "true"
ORDER_CHART_TFS = [x.strip() for x in (os.getenv("ORDER_CHART_TFS", "4h,1h,15m,5m") or "").split(",") if x.strip()]
ORDER_CHART_LIMIT = int(os.getenv("ORDER_CHART_LIMIT", "100"))
ORDER_CHART_DIR = resolve_path(os.getenv("ORDER_CHART_DIR", "static/images/order_charts"))
DAILY_OPEN_ALERT_THRESHOLD = int(os.getenv("DAILY_OPEN_ALERT_THRESHOLD", "5"))
from daily_open_limit_lib import (
build_daily_open_alert_prompt,
can_trade_new_open,
check_daily_open_hard_limit,
count_opens_for_trading_day,
format_daily_open_counter_line,
format_daily_open_summary_short,
load_daily_open_limits_from_env,
should_send_daily_open_alert,
)
DAILY_OPEN_ALERT_THRESHOLD, DAILY_OPEN_HARD_LIMIT = load_daily_open_limits_from_env()
RISK_PERCENT = float(os.getenv("RISK_PERCENT", "2"))
MANUAL_MIN_PLANNED_RR = float(os.getenv("MANUAL_MIN_PLANNED_RR", "1.4"))
BREAKEVEN_RR_TRIGGER = float(os.getenv("BREAKEVEN_RR_TRIGGER", "1.0"))
@@ -2696,6 +2707,11 @@ def precheck_risk(conn, symbol, direction):
active_count = get_active_position_count(conn)
if active_count >= MAX_ACTIVE_POSITIONS:
return False, f"已达最大持仓数({active_count}/{MAX_ACTIVE_POSITIONS}"
ok_daily, daily_reason, _opens = check_daily_open_hard_limit(
conn, get_trading_day(now), DAILY_OPEN_HARD_LIMIT, TRADING_DAY_RESET_HOUR
)
if not ok_daily:
return False, daily_reason
trend_n = conn.execute(
"SELECT COUNT(*) FROM trend_pullback_plans WHERE status='active'"
).fetchone()[0]
@@ -2717,6 +2733,11 @@ def precheck_trend_pullback_start(conn):
now = app_now()
if not trading_day_reset_allows_new_open(now):
return False, f"北京时间 {TRADING_DAY_RESET_HOUR}:00 前不允许持仓"
ok_daily, daily_reason, _opens = check_daily_open_hard_limit(
conn, get_trading_day(now), DAILY_OPEN_HARD_LIMIT, TRADING_DAY_RESET_HOUR
)
if not ok_daily:
return False, daily_reason
active_count = get_active_position_count(conn)
if active_count >= MAX_ACTIVE_POSITIONS:
return False, (
@@ -5383,10 +5404,14 @@ def render_main_page(page="trade"):
preview_snapshots.append(sd)
except Exception as e:
print(f"[records] trend_pullback_preview_snapshots: {e}")
can_trade = (
trading_day_reset_allows_new_open(now)
and active_count < MAX_ACTIVE_POSITIONS
and int(trend_active or 0) == 0
opens_today = count_opens_for_trading_day(conn, trading_day)
can_trade = can_trade_new_open(
time_allows=trading_day_reset_allows_new_open(now),
active_count=active_count,
max_active_positions=MAX_ACTIVE_POSITIONS,
opens_today=opens_today,
hard_limit=DAILY_OPEN_HARD_LIMIT,
extra_blocks=int(trend_active or 0) != 0,
)
strategy_extra = {}
if page in ("strategy", "strategy_trend", "strategy_roll", "strategy_records"):
@@ -5440,6 +5465,9 @@ def render_main_page(page="trade"):
max_active_positions=MAX_ACTIVE_POSITIONS,
manual_min_planned_rr=MANUAL_MIN_PLANNED_RR,
can_trade=can_trade,
opens_today=opens_today,
daily_open_hard_limit=DAILY_OPEN_HARD_LIMIT,
daily_open_alert_threshold=DAILY_OPEN_ALERT_THRESHOLD,
live_trading_enabled=LIVE_TRADING_ENABLED,
preview_snapshots=preview_snapshots,
exchange_sync_from_label=(EXCHANGE_POSITION_SYNC_FROM_BJ or "最近90天"),
@@ -5538,11 +5566,15 @@ def api_account_snapshot():
trend_active = conn.execute(
"SELECT COUNT(*) FROM trend_pullback_plans WHERE status='active'"
).fetchone()[0]
opens_today = count_opens_for_trading_day(conn, trading_day)
conn.close()
can_trade = (
trading_day_reset_allows_new_open(now)
and active_count < MAX_ACTIVE_POSITIONS
and int(trend_active or 0) == 0
can_trade = can_trade_new_open(
time_allows=trading_day_reset_allows_new_open(now),
active_count=active_count,
max_active_positions=MAX_ACTIVE_POSITIONS,
opens_today=opens_today,
hard_limit=DAILY_OPEN_HARD_LIMIT,
extra_blocks=int(trend_active or 0) != 0,
)
available_trading_usdt = get_available_trading_usdt()
return jsonify({
@@ -5553,6 +5585,9 @@ def api_account_snapshot():
"active_count": active_count,
"max_active_positions": MAX_ACTIVE_POSITIONS,
"can_trade": can_trade,
"opens_today": opens_today,
"daily_open_hard_limit": DAILY_OPEN_HARD_LIMIT,
"daily_open_alert_threshold": DAILY_OPEN_ALERT_THRESHOLD,
"manual_min_planned_rr": MANUAL_MIN_PLANNED_RR,
"trading_day": trading_day
})
@@ -6557,7 +6592,9 @@ def add_order():
f"移动保本位:{breakeven_rr_trigger}R → {be_wx}",
"📌 状态统计",
f"✅ 条件委托:{order_state_text}",
f"📅 当日开仓次数:{opens_today_after} / {DAILY_OPEN_ALERT_THRESHOLD} 次(风控阈值提醒)",
format_daily_open_counter_line(
opens_today_after, DAILY_OPEN_ALERT_THRESHOLD, DAILY_OPEN_HARD_LIMIT
),
]
if chart_url:
wx_lines.append(f"多周期K线图:{chart_url}")
@@ -6566,17 +6603,25 @@ def add_order():
flash_lines = [
f"机器人开单成功:风格 {trade_style};风险 {risk_display};基数 {margin_capital}U,杠杆 {leverage}x,名义仓位 {notional_value}U,仓位占比 {position_ratio}%,合约张数 {amount}(折算标的 {base_amount}),"
f"计划RR {planned_rr if planned_rr is not None else '-'};已在交易所挂条件止盈/止损委托(非仓位绑定型)",
f"本交易日累计开仓:{opens_today_after}",
format_daily_open_summary_short(
opens_today_after, DAILY_OPEN_ALERT_THRESHOLD, DAILY_OPEN_HARD_LIMIT
),
]
if chart_url:
flash_lines.append(f"已生成多周期K线图:{chart_url}")
flash(" ".join(flash_lines))
if opens_today_before < DAILY_OPEN_ALERT_THRESHOLD <= opens_today_after:
if should_send_daily_open_alert(
opens_today_before, opens_today_after, DAILY_OPEN_ALERT_THRESHOLD
):
advice = ai_short_advice(
f"用户在北京时间交易日 {trading_day} 已累计开仓 {opens_today_after} 次(阈值 {DAILY_OPEN_ALERT_THRESHOLD})。"
f"最新一笔:{symbol} {direction},杠杆{leverage}x,基数{margin_capital}U。"
f"用户自述“上头了”。请给克制提醒。"
build_daily_open_alert_prompt(
trading_day,
opens_today_after,
DAILY_OPEN_ALERT_THRESHOLD,
hard_limit=DAILY_OPEN_HARD_LIMIT,
detail_line=f"最新一笔:{symbol} {direction},杠杆{leverage}x,基数{margin_capital}U。",
)
)
if advice:
send_wechat_msg(f"【AI提醒】今日开仓次数已达 {opens_today_after}\n{advice[:800]}")