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:
@@ -90,6 +90,10 @@ GATE_TPSL_PRICE_TYPE=0
|
||||
# =============================================================================
|
||||
# 【最大同时持仓】active 下单监控数达到该值后禁止再开仓(默认 1=单仓)
|
||||
MAX_ACTIVE_POSITIONS=1
|
||||
# 【单日开仓 AI 提醒】本交易日开仓达到该次数时推送企业微信 AI 克制提醒(不拦单)
|
||||
DAILY_OPEN_ALERT_THRESHOLD=5
|
||||
# 【单日开仓硬上限】本交易日开仓次数>=该值后禁止一切新开仓直至下一交易日(北京时间 TRADING_DAY_RESET_HOUR 切日);0=不启用
|
||||
DAILY_OPEN_HARD_LIMIT=0
|
||||
# 整点前禁止新开仓:true=启用(默认),false=关闭(交易日划分仍用 TRADING_DAY_RESET_HOUR)
|
||||
# TRADING_DAY_RESET_OPEN_GUARD_ENABLED=true
|
||||
|
||||
@@ -150,7 +154,7 @@ AI_MODEL=huihui_ai/deepseek-r1-abliterated:latest
|
||||
# ORDER_CHART_TFS=4h,1h,15m,5m
|
||||
# ORDER_CHART_LIMIT=100
|
||||
# ORDER_CHART_DIR=static/images/order_charts
|
||||
# DAILY_OPEN_ALERT_THRESHOLD=5
|
||||
# 详见 DAILY_OPEN_ALERT_THRESHOLD / DAILY_OPEN_HARD_LIMIT;说明文档 docs/daily-open-limit.md
|
||||
# 以损定仓(按交易账户资金的百分比)
|
||||
# RISK_PERCENT=2
|
||||
# 移动保本触发(达到多少R触发)与偏移(百分比)
|
||||
|
||||
@@ -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]}")
|
||||
|
||||
@@ -381,7 +381,8 @@
|
||||
</div>
|
||||
<div class="rule-tip" id="order-rule-tip">
|
||||
规则:最大同时持仓 {{ max_active_positions }}(当前 active {{ active_count }});与「趋势回调」计划互斥;BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x;
|
||||
{% if can_trade %}可开仓{% else %}不可开仓(持仓达上限、有趋势回调计划,或未到北京时间 {{ reset_hour }}:00){% endif %};
|
||||
本交易日开仓 {{ opens_today }}{% if daily_open_hard_limit > 0 %} / 硬上限 {{ daily_open_hard_limit }}{% endif %}(AI 提醒 {{ daily_open_alert_threshold }});
|
||||
{% if can_trade %}可开仓{% else %}不可开仓(持仓达上限、单日开仓达上限、有趋势回调计划,或未到北京时间 {{ reset_hour }}:00){% endif %};
|
||||
人工开仓盈亏比不得低于 {{ manual_min_planned_rr }}:1
|
||||
</div>
|
||||
<div class="rule-tip">
|
||||
@@ -1974,12 +1975,30 @@ function refreshAccountSnapshot(){
|
||||
if (typeof data.available_trading_usdt !== "undefined" && data.available_trading_usdt !== null) {
|
||||
latestAvailableUsdt = Number(data.available_trading_usdt);
|
||||
}
|
||||
const canTradeText = data.can_trade ? "可开仓" : `不可开仓(持仓 ${data.active_count||0}/${data.max_active_positions||{{ max_active_positions }}}、有趋势回调计划,或未到北京时间 {{ reset_hour }}:00)`;
|
||||
let canTradeText = "可开仓";
|
||||
if (!data.can_trade) {
|
||||
const parts = [];
|
||||
const ac = Number(data.active_count || 0);
|
||||
const max = Number(data.max_active_positions || {{ max_active_positions }});
|
||||
if (ac >= max) parts.push(`持仓 ${ac}/${max}`);
|
||||
const hard = Number(data.daily_open_hard_limit != null ? data.daily_open_hard_limit : {{ daily_open_hard_limit }});
|
||||
const opens = Number(data.opens_today);
|
||||
if (hard > 0 && !Number.isNaN(opens) && opens >= hard) parts.push(`本交易日开仓 ${opens}/${hard} 已达上限`);
|
||||
parts.push("有趋势回调计划");
|
||||
parts.push(`或未到北京时间 {{ reset_hour }}:00`);
|
||||
canTradeText = `不可开仓(${parts.join(";")})`;
|
||||
}
|
||||
const opensToday = Number(data.opens_today);
|
||||
const hardLim = Number(data.daily_open_hard_limit != null ? data.daily_open_hard_limit : {{ daily_open_hard_limit }});
|
||||
const alertLim = Number(data.daily_open_alert_threshold != null ? data.daily_open_alert_threshold : {{ daily_open_alert_threshold }});
|
||||
const openCntTxt = !Number.isNaN(opensToday)
|
||||
? `本交易日开仓 ${opensToday}${hardLim > 0 ? ` / 硬上限 ${hardLim}` : ""}(AI 提醒 ${alertLim})`
|
||||
: "";
|
||||
const tip = document.getElementById("order-rule-tip");
|
||||
const avail = (latestAvailableUsdt !== null && !Number.isNaN(latestAvailableUsdt)) ? `;交易账户可用约${latestAvailableUsdt.toFixed(2)}U` : "";
|
||||
const minRr = data.manual_min_planned_rr != null ? data.manual_min_planned_rr : MANUAL_MIN_PLANNED_RR;
|
||||
if(tip){
|
||||
tip.innerText = `规则:最大同时持仓 ${data.max_active_positions || {{ max_active_positions }}}(当前 active ${data.active_count||0});与「趋势回调」计划互斥;BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x;${canTradeText}${avail};人工开仓盈亏比不得低于 ${minRr}:1`;
|
||||
tip.innerText = `规则:最大同时持仓 ${data.max_active_positions || {{ max_active_positions }}}(当前 active ${data.active_count||0});与「趋势回调」计划互斥;BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x;${openCntTxt ? openCntTxt + ";" : ""}${canTradeText}${avail};人工开仓盈亏比不得低于 ${minRr}:1`;
|
||||
}
|
||||
}).catch(()=>{});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user