bot增加保本

This commit is contained in:
dekun
2026-05-19 15:20:14 +08:00
parent 738cf0eccd
commit 44432a0688
4 changed files with 268 additions and 9 deletions
+2
View File
@@ -141,6 +141,8 @@ AI_MODEL=huihui_ai/deepseek-r1-abliterated:latest
# 趋势回调策略(可选,见 趋势回调策略说明.md)
# TREND_PULLBACK_DCA_LEGS=5
# TREND_PULLBACK_PREVIEW_TTL_SECONDS=120
# 趋势回调手动保本:相对持仓均价的默认偏移(%);多=均价×(1+pct/100),空=均价×(1-pct/100)
# TREND_PULLBACK_MANUAL_BREAKEVEN_OFFSET_PCT=0.3
# TREND_PREVIEW_MAX_BALANCE_DRIFT_PCT=5
APP_TIMEZONE=Asia/Shanghai
+177 -6
View File
@@ -29,6 +29,19 @@ except ImportError:
ImageFont = None # type: ignore
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
_REPO_ROOT = os.path.dirname(BASE_DIR)
import sys
if _REPO_ROOT not in sys.path:
sys.path.insert(0, _REPO_ROOT)
from history_window_lib import (
PRESET_CUSTOM,
PRESET_UTC_LAST24H,
PRESET_UTC_LAST7D,
PRESET_UTC_TODAY,
resolve_window,
utc_window_to_bj_sql_strings,
)
def load_env_file(path):
@@ -157,6 +170,10 @@ TREND_PULLBACK_DCA_LEGS = max(1, int(os.getenv("TREND_PULLBACK_DCA_LEGS", "5")))
TREND_PULLBACK_PREVIEW_TTL_SECONDS = max(10, int(os.getenv("TREND_PULLBACK_PREVIEW_TTL_SECONDS", "120")))
# 确认执行时:当前可用余额与预览快照相对偏差超过该百分比则拒绝(避免余额被划走后仍按旧计划满仓)
TREND_PREVIEW_MAX_BALANCE_DRIFT_PCT = float(os.getenv("TREND_PREVIEW_MAX_BALANCE_DRIFT_PCT", "5"))
# 趋势回调:手动保本默认相对均价偏移(%);多=均价×(1+pct/100),空=均价×(1-pct/100)
TREND_PULLBACK_MANUAL_BREAKEVEN_OFFSET_PCT = float(
os.getenv("TREND_PULLBACK_MANUAL_BREAKEVEN_OFFSET_PCT", "0.3")
)
MONITOR_TYPE_TREND = "趋势回调"
KLINE_TIMEFRAME = os.getenv("KLINE_TIMEFRAME", "5m")
FULL_MARGIN_BUFFER_RATIO = float(os.getenv("FULL_MARGIN_BUFFER_RATIO", "0.98"))
@@ -1294,6 +1311,15 @@ def init_db():
c.execute("ALTER TABLE trend_pullback_plans ADD COLUMN leg_amounts_json TEXT")
except Exception:
pass
for ddl in (
"ALTER TABLE trend_pullback_plans ADD COLUMN initial_stop_loss REAL",
"ALTER TABLE trend_pullback_plans ADD COLUMN breakeven_applied INTEGER DEFAULT 0",
"ALTER TABLE trend_pullback_plans ADD COLUMN breakeven_applied_at TEXT",
):
try:
c.execute(ddl)
except Exception:
pass
c.execute(
"""CREATE TABLE IF NOT EXISTS trend_pullback_previews (
@@ -3514,8 +3540,35 @@ def trend_plan_history_status_label(status):
}.get(s, status or "-")
def _list_window_from_request():
return resolve_window(request.args, default_preset=PRESET_UTC_TODAY)
def calc_trend_manual_breakeven_stop(direction, entry_price, offset_pct=None):
"""趋势回调手动保本:默认开仓均价 + offset_pct%(多上移、空下移)。"""
try:
e = float(entry_price)
pct = float(
offset_pct
if offset_pct is not None
else TREND_PULLBACK_MANUAL_BREAKEVEN_OFFSET_PCT
)
except (TypeError, ValueError):
return None
if e <= 0:
return None
direction = (direction or "long").strip().lower()
if direction == "short":
return e * (1.0 - pct / 100.0)
return e * (1.0 + pct / 100.0)
def enrich_active_trend_plan_row(row):
d = row_to_dict(row)
try:
d["breakeven_applied"] = int(d.get("breakeven_applied") or 0) != 0
except Exception:
d["breakeven_applied"] = False
ex_sym = d.get("exchange_symbol") or normalize_exchange_symbol(d.get("symbol") or "")
direction = (d.get("direction") or "long").lower()
m = get_live_position_exchange_metrics(ex_sym, direction)
@@ -4225,6 +4278,66 @@ def _trend_refresh_stop_only(exchange_symbol, direction, stop_loss):
_gate_place_stop_loss_only_position(exchange_symbol, direction, stop_loss)
def apply_trend_pullback_manual_breakeven(conn, row, offset_pct=None):
"""运行中趋势计划:将交易所止损移至均价+偏移(默认 0.3%),仅当新止损更优时生效。"""
if (row["status"] or "").strip() != "active":
return False, "计划已结束"
if not int(row["first_order_done"] or 0):
return False, "尚未完成首仓,无法保本"
avg_e = float(row["avg_entry_price"] or 0)
if avg_e <= 0:
return False, "缺少有效持仓均价"
direction = (row["direction"] or "long").lower()
ex_sym = row["exchange_symbol"] or normalize_exchange_symbol(row["symbol"])
pos = get_live_position_contracts(ex_sym, direction)
if pos is None or float(pos) <= 0:
return False, "交易所当前无该方向持仓"
new_sl_raw = calc_trend_manual_breakeven_stop(direction, avg_e, offset_pct)
if new_sl_raw is None:
return False, "保本价计算失败"
new_sl = round_price_to_exchange(ex_sym, new_sl_raw)
if new_sl is None:
return False, "保本价经交易所精度舍入后无效"
new_sl = float(new_sl)
cur_sl = float(row["stop_loss"] or 0)
if direction == "long":
if new_sl <= cur_sl:
return False, f"新止损 {new_sl} 未高于当前止损 {cur_sl}(多仓需上移)"
else:
if new_sl >= cur_sl:
return False, f"新止损 {new_sl} 未低于当前止损 {cur_sl}(空仓需下移)"
try:
_trend_refresh_stop_only(ex_sym, direction, new_sl)
except Exception as e:
return False, friendly_exchange_error(e)
now_s = app_now_str()
conn.execute(
"UPDATE trend_pullback_plans SET stop_loss=?, breakeven_applied=1, breakeven_applied_at=? WHERE id=?",
(new_sl, now_s, row["id"]),
)
pct_used = float(
offset_pct
if offset_pct is not None
else TREND_PULLBACK_MANUAL_BREAKEVEN_OFFSET_PCT
)
sym = row["symbol"]
send_wechat_msg(
"\n".join(
[
f"# ✅ {sym} 趋势回调手动保本",
f"**账户:{_wechat_account_label()}**",
f"- 计划 ID**{row['id']}**",
f"- 方向:{_wechat_direction_text(direction)}",
f"- 持仓均价:{format_price_for_symbol(sym, avg_e)}",
f"- 偏移:{pct_used}%(相对均价)",
f"- 新止损:{format_price_for_symbol(sym, new_sl)}",
f"- 交易所:已更新仓位止损触发单",
]
)
)
return True, None
def _trend_weighted_avg(old_avg, old_amt, fill_px, add_amt):
try:
oa, aa = float(old_amt), float(add_amt)
@@ -4937,6 +5050,8 @@ def api_sync_positions():
def render_main_page(page="trade"):
now = app_now()
trading_day = get_trading_day(now)
list_window = _list_window_from_request()
start_bj, end_bj = utc_window_to_bj_sql_strings(list_window["start_utc"], list_window["end_utc"], APP_TZ)
conn = get_db()
session_row = ensure_session(conn, trading_day)
local_current_capital = float(session_row["current_capital"])
@@ -4957,7 +5072,14 @@ def render_main_page(page="trade"):
sync_trend_trade_records_from_exchange(conn)
except Exception:
pass
raw_records = conn.execute("SELECT * FROM trade_records ORDER BY id DESC").fetchall()
if page in ("records", "plan_history"):
raw_records = conn.execute(
"SELECT * FROM trade_records WHERE COALESCE(closed_at, created_at, opened_at) >= ? "
"AND COALESCE(closed_at, created_at, opened_at) <= ? ORDER BY id DESC LIMIT 2000",
(start_bj, end_bj),
).fetchall()
else:
raw_records = conn.execute("SELECT * FROM trade_records ORDER BY id DESC LIMIT 500").fetchall()
records = [to_effective_trade_dict(r) for r in raw_records]
total = len(records)
miss_count = sum(1 for r in records if (r.get("effective_result") or "") == "错过")
@@ -4981,14 +5103,18 @@ def render_main_page(page="trade"):
preview_snapshots = []
if page == "plan_history":
plan_history_raw = conn.execute(
"SELECT * FROM trend_pullback_plans WHERE status != 'active' ORDER BY id DESC LIMIT 100"
"SELECT * FROM trend_pullback_plans WHERE status != 'active' "
"AND COALESCE(opened_at, '') >= ? AND COALESCE(opened_at, '') <= ? ORDER BY id DESC LIMIT 500",
(start_bj, end_bj),
).fetchall()
for pr in plan_history_raw:
pd = row_to_dict(pr)
pd["status_label"] = trend_plan_history_status_label(pd.get("status"))
plan_history.append(pd)
snap_rows = conn.execute(
"SELECT * FROM trend_pullback_preview_snapshots ORDER BY id DESC LIMIT 150"
"SELECT * FROM trend_pullback_preview_snapshots WHERE COALESCE(preview_created_at, '') >= ? "
"AND COALESCE(preview_created_at, '') <= ? ORDER BY id DESC LIMIT 500",
(start_bj, end_bj),
).fetchall()
for sr in snap_rows:
sd = row_to_dict(sr)
@@ -5067,6 +5193,14 @@ def render_main_page(page="trade"):
trend_preview_expired=trend_preview_expired,
trend_preview_id_arg=trend_preview_id_arg,
trend_preview_max_drift_pct=TREND_PREVIEW_MAX_BALANCE_DRIFT_PCT,
trend_manual_breakeven_offset_pct=TREND_PULLBACK_MANUAL_BREAKEVEN_OFFSET_PCT,
list_window=list_window,
list_window_presets={
"utc_today": PRESET_UTC_TODAY,
"utc_last24h": PRESET_UTC_LAST24H,
"utc_last7d": PRESET_UTC_LAST7D,
"custom": PRESET_CUSTOM,
},
focus_key_id=(key_list[0]["id"] if key_list else None),
focus_order_id=(order_list[0]["id"] if order_list else None),
data_export_version=3,
@@ -6019,10 +6153,10 @@ def execute_trend_pullback():
opened_ms = _to_ms_with_fallback(None, opened_at)
cur = conn.execute(
"""INSERT INTO trend_pullback_plans (
status,symbol,exchange_symbol,direction,leverage,stop_loss,add_upper,take_profit,risk_percent,
status,symbol,exchange_symbol,direction,leverage,stop_loss,initial_stop_loss,add_upper,take_profit,risk_percent,
snapshot_available_usdt,snapshot_at,plan_margin_capital,target_order_amount,first_order_amount,remainder_total,
dca_legs,per_leg_amount,grid_prices_json,leg_amounts_json,legs_done,first_order_done,last_mark_price,avg_entry_price,order_amount_open,opened_at,opened_at_ms,session_date,message
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(
"active",
symbol,
@@ -6030,6 +6164,7 @@ def execute_trend_pullback():
direction,
leverage,
stop_loss,
stop_loss,
add_upper,
take_profit,
risk_percent,
@@ -6086,6 +6221,37 @@ def cancel_trend_pullback_preview():
return redirect(url_for("trade_page"))
@app.route("/trend_pullback_breakeven/<int:pid>", methods=["POST"])
@login_required
def trend_pullback_breakeven(pid):
offset_raw = (request.form.get("breakeven_offset_pct") or "").strip()
offset_pct = None
if offset_raw:
try:
offset_pct = float(offset_raw)
if offset_pct < 0:
raise ValueError
except ValueError:
flash("保本偏移% 格式无效")
return redirect(url_for("trade_page"))
conn = get_db()
row = conn.execute(
"SELECT * FROM trend_pullback_plans WHERE id=? AND status='active'", (pid,)
).fetchone()
if not row:
conn.close()
flash("未找到运行中的趋势回调计划")
return redirect(url_for("trade_page"))
ok, err = apply_trend_pullback_manual_breakeven(conn, row, offset_pct=offset_pct)
conn.commit()
conn.close()
if ok:
flash("已手动保本:交易所止损已按均价+偏移更新")
else:
flash(err or "手动保本失败")
return redirect(url_for("trade_page"))
@app.route("/stop_trend_pullback/<int:pid>")
@login_required
def stop_trend_pullback(pid):
@@ -6222,12 +6388,17 @@ def _md_response(filename, content):
@app.route("/export/trade_records")
@login_required
def export_trade_records():
win = _list_window_from_request()
start_bj, end_bj = utc_window_to_bj_sql_strings(win["start_utc"], win["end_utc"], APP_TZ)
conn = get_db()
rows = conn.execute(
"SELECT id,symbol,monitor_type,direction,trigger_price,stop_loss,take_profit,margin_capital,leverage,"
"pnl_amount,hold_seconds,hold_minutes,opened_at,closed_at,result,miss_reason,"
"entry_reason,reviewed_entry_reason,created_at,trend_plan_id,exchange_realized_pnl,"
"exchange_opened_at,exchange_closed_at,exchange_sync_key FROM trade_records ORDER BY id ASC"
"exchange_opened_at,exchange_closed_at,exchange_sync_key FROM trade_records "
"WHERE COALESCE(closed_at, created_at, opened_at) >= ? "
"AND COALESCE(closed_at, created_at, opened_at) <= ? ORDER BY id ASC",
(start_bj, end_bj),
).fetchall()
conn.close()
head_base = [
+78 -1
View File
@@ -136,6 +136,8 @@
.export-bar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:12px;font-size:.85rem}
.export-bar a{color:#8fc8ff;text-decoration:none;padding:6px 10px;border:1px solid #304164;border-radius:8px;background:#151a2a}
.export-bar a:hover{background:#1f2740}
.list-window-bar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:12px;padding:10px 12px;background:#151a2a;border:1px solid #304164;border-radius:10px;font-size:.82rem}
.list-window-bar label{color:#9aa;display:flex;align-items:center;gap:6px}
.key-history{margin-top:12px;padding-top:10px;border-top:1px solid #2a3150}
.key-history h3{font-size:.88rem;color:#b8c4ff;margin-bottom:6px}
.key-history .sub{font-size:.72rem;color:#8892b0;margin-bottom:6px}
@@ -198,6 +200,27 @@
</div>
{% with msg=get_flashed_messages() %}{% if msg %}<div class="flash">{{ msg[0] }}</div>{% endif %}{% endwith %}
{% if page in ('records', 'plan_history') %}
<div class="list-window-bar">
<span style="color:#cfd3ef">列表筛选(<strong>UTC</strong>,默认当日):{{ list_window.label }}</span>
<label>预设
<select id="win-preset-select" onchange="toggleListWindowCustom()">
<option value="utc_today" {% if list_window.preset == 'utc_today' %}selected{% endif %}>UTC 当日</option>
<option value="utc_last24h" {% if list_window.preset == 'utc_last24h' %}selected{% endif %}>近 24 小时</option>
<option value="utc_last7d" {% if list_window.preset == 'utc_last7d' %}selected{% endif %}>近 7 天</option>
<option value="custom" {% if list_window.preset == 'custom' %}selected{% endif %}>自定义</option>
</select>
</label>
<span id="win-custom-range" style="{% if list_window.preset != 'custom' %}display:none{% endif %}">
<label>起(UTC) <input type="datetime-local" id="win-from-utc" value="{{ list_window.start_utc.strftime('%Y-%m-%dT%H:%M') }}"></label>
<label>止(UTC) <input type="datetime-local" id="win-to-utc" value="{{ list_window.end_utc.strftime('%Y-%m-%dT%H:%M') }}"></label>
</span>
<button type="button" style="padding:6px 12px" onclick="applyListWindow()">应用</button>
<span style="color:#8892b0;font-size:.75rem">统计页仍按北京时间 {{ reset_hour }}:00 切日</span>
</div>
{% endif %}
<div class="export-bar">
<span style="color:#9aa">数据导出(v{{ data_export_version }} CSVUTF-8;交易记录含开仓类型列及交易所对齐字段):</span>
<a href="/export/trade_records">交易记录</a>
@@ -339,7 +362,7 @@
<h2 style="margin-bottom:8px">趋势回调策略</h2>
<div class="rule-tip">
<strong>生成预览</strong>:读取合约 USDT <strong>可用余额快照</strong>并计算计划(不下单)。预览有效期 <strong>{{ trend_pullback_preview_ttl }} 秒</strong><br>
<strong>确认执行</strong>:市价首仓 50% + 挂交易所止损;剩余 50% 在止损与补仓区间之间共 {{ trend_pullback_dca_legs }} 档(做多为<strong>上沿</strong>、做空为<strong>下沿</strong>;程序可能因最小张数自动减档)市价补仓;<strong>止盈由程序监控</strong><br>
<strong>确认执行</strong>:市价首仓 50% + 挂交易所止损;首仓后可<strong>手动保本</strong>(默认均价+{{ trend_manual_breakeven_offset_pct }}%);剩余 50% 在止损与补仓区间之间共 {{ trend_pullback_dca_legs }} 档(做多为<strong>上沿</strong>、做空为<strong>下沿</strong>;程序可能因最小张数自动减档)市价补仓;<strong>止盈由程序监控</strong><br>
确认执行时若当前可用余额与预览快照相对偏差 &gt; <strong>{{ trend_preview_max_drift_pct }}%</strong> 会拒绝并要求重新预览。
</div>
<form id="trend-pullback-form" action="{{ url_for('preview_trend_pullback') }}" method="post" class="form-row">
@@ -357,6 +380,45 @@
<button type="submit" {% if not can_trade %}disabled style="opacity:.5;cursor:not-allowed"{% endif %}>生成预览</button>
</form>
<script>
function listWindowQueryString(){
const presetEl = document.getElementById("win-preset-select");
const preset = (presetEl && presetEl.value) || new URLSearchParams(window.location.search).get("win_preset") || "utc_today";
const q = new URLSearchParams(window.location.search);
q.set("win_preset", preset);
if(preset === "custom"){
const fromEl = document.getElementById("win-from-utc");
const toEl = document.getElementById("win-to-utc");
if(fromEl && fromEl.value) q.set("from_utc", fromEl.value.replace("T", " ") + ":00");
else q.delete("from_utc");
if(toEl && toEl.value) q.set("to_utc", toEl.value.replace("T", " ") + ":00");
else q.delete("to_utc");
} else {
q.delete("from_utc");
q.delete("to_utc");
}
return q.toString();
}
function toggleListWindowCustom(){
const preset = document.getElementById("win-preset-select");
const box = document.getElementById("win-custom-range");
if(!preset || !box) return;
box.style.display = preset.value === "custom" ? "" : "none";
}
function applyListWindow(){
const qs = listWindowQueryString();
const path = window.location.pathname || "/records";
window.location.href = qs ? (path + "?" + qs) : path;
}
function attachListWindowToExports(){
const qs = listWindowQueryString();
if(!qs) return;
document.querySelectorAll('.export-bar a[href^="/export/trade_records"]').forEach(a=>{
const base = a.getAttribute("href").split("?")[0];
a.setAttribute("href", base + "?" + qs);
});
}
(function(){
const dirSel = document.getElementById("trend-direction");
const addInp = document.getElementById("trend-add-upper");
@@ -490,6 +552,19 @@
</span>
</div>
</div>
<div class="plan-card-meta" style="margin-top:8px">
<form action="{{ url_for('trend_pullback_breakeven', pid=t.id) }}" method="post" class="form-row" style="margin:0;align-items:center" onsubmit="return confirm('将交易所止损移至持仓均价+偏移?仅当新止损优于当前止损时生效。');">
<label style="font-size:.78rem;color:#cfd3ef;display:flex;align-items:center;gap:6px">
手动保本 偏移%
<input name="breakeven_offset_pct" type="number" min="0" step="0.01" value="{{ trend_manual_breakeven_offset_pct }}" style="width:72px;padding:4px 8px">
(默认均价+{{ trend_manual_breakeven_offset_pct }}%
</label>
<button type="submit" style="padding:6px 12px;background:#1f4a3a;color:#8fc8ff">应用保本止损</button>
{% if t.breakeven_applied %}<span style="color:#6ab88a;font-size:.75rem">已保本 {{ (t.breakeven_applied_at or '')[:16] }}</span>{% endif %}
{% if t.initial_stop_loss is not none and t.initial_stop_loss != t.stop_loss %}<span style="color:#8892b0;font-size:.75rem">原止损 {{ price_fmt(sym, t.initial_stop_loss) }}</span>{% endif %}
</form>
</div>
<div class="plan-card-meta" style="margin-bottom:0">
快照可用: {% if t.snapshot_available_usdt is not none %}{{ money_fmt(t.snapshot_available_usdt) }}U{% else %}—{% endif %}
计划保证金≈{% if t.plan_margin_capital is not none %}{{ money_fmt(t.plan_margin_capital) }}U{% else %}—{% endif %}
@@ -1184,6 +1259,8 @@ function toggleStatsCard(){
btn.innerText = collapsed ? "展开" : "折叠";
}
attachListWindowToExports();
toggleListWindowCustom();
if(document.getElementById("journal-list")) loadJournals();
if(document.getElementById("review-list")) loadReviews();
const reviewToggle = document.getElementById("review-mode-toggle");
@@ -27,6 +27,12 @@
## 3. 执行流程(时间顺序)
### 3.0 列表时间窗(交易记录 / 计划历史)
- **交易记录**、**计划历史**(含预览快照)列表与 **交易记录 CSV 导出** 支持 **UTC** 时间筛选(默认 UTC 当日;可选近 24h、近 7d、自定义起止)。
- 查询参数:`win_preset``utc_today` / `utc_last24h` / `utc_last7d` / `custom`)、自定义时另传 `from_utc``to_utc`
- **统计分析**页仍按北京时间 `TRADING_DAY_RESET_HOUR` 切日,不受列表窗影响。
### 3.1 预览阶段(不下单)
1. **风控**:与「机器人下单监控」**互斥**——存在活跃机器人持仓或运行中趋势计划时,不可生成预览。
@@ -41,6 +47,7 @@
5. 再次校验:预览未过期;**当前可用余额**与预览快照相对偏差 ≤ `TREND_PREVIEW_MAX_BALANCE_DRIFT_PCT`(默认 **5%**),否则拒绝执行并要求重新预览。
6. **首仓****立即市价** 开立 **总计划张数 × 50%**(不附带交易所止盈单)。
7. **止损**:撤销旧条件单后,挂 **仅止损** 的仓位触发单;之后每次补仓成交会 **刷新** 止损挂单。
7b. **手动保本**(可选):首仓完成且交易所有持仓后,可在运行中计划卡片点击「应用保本止损」——将止损移至 **持仓均价 ± 偏移%**(默认 **+0.3%** 多 / **0.3%** 空);仅当新止损 **优于** 当前止损时生效,并同步 Gate 仓位止损单。
8. **补仓**:当价格 **穿越** 下一档触发价(做多为自上向下穿越,做空为自下向上穿越)时,按该档张数 **市价加仓**;直至 `N` 档执行完毕或计划结束。
9. **止盈监控**:后台线程若发现价格触及止盈,则 **市价全平**
10. **止损触发**:若仓位被交易所止损打光,本地检测到 **持仓为 0** 后记账为 **止损** 并结束计划。
@@ -58,7 +65,8 @@
- 每行提供 **删除**:删除该计划行,并删除 `trade_records`**`trend_plan_id` 与之相同** 且类型为「趋势回调」的记录(用于与计划一一对应的新数据;历史旧行若无 `trend_plan_id` 则不会随删)。
- **运行中的计划(交易执行页)**
- 在计划摘要下方展示 **浮盈亏(交易所)**:来自 Gate 当前持仓接口的 **未实现盈亏**(及标记价,若可得);与本地按均价估算可能略有差异,以交易所为准便于对照。
- **补仓边界**按方向显示「补仓上沿」或「补仓下沿」(数值仍为 `add_upper` 字段)。
- **补仓边界**按方向显示「补仓上沿」或「补仓下沿」(数值仍为 `add_upper` 字段)。
- **手动保本**:表单可改偏移 %(默认见 `TREND_PULLBACK_MANUAL_BREAKEVEN_OFFSET_PCT`);成功后显示「已保本」时间与原止损(若与当前不同)。
### 3.5 交易记录与交易所「已实现盈亏」对齐
@@ -78,7 +86,7 @@
| 开仓 | 单次市价 + 条件止盈+止损 | 首仓 50% 市价 + 多档补仓 + **仅止损在交易所** |
| 止盈 | 条件单 + 本地监控 | **仅本地监控市价止盈** |
| 仓位基数 | 以损定仓(表单/会话基数) | **可用余额快照 × 风险比例** 推导 |
| 移动保本 | 支持 | **不支持**未实现 |
| 移动保本 | 支持(按 R 自动上移) | **手动保本**首仓后有持仓即可点;默认均价 +0.3%,可改偏移;同步 Gate 止损单;**无**自动 R 保本 |
---
@@ -95,6 +103,7 @@
| 变量 | 说明 | 默认 |
|------|------|------|
| `TREND_PULLBACK_MANUAL_BREAKEVEN_OFFSET_PCT` | 手动保本默认偏移(相对持仓均价,%) | `0.3` |
| `TREND_PULLBACK_DCA_LEGS` | 剩余 50% 拆档数量上限 | `5` |
| `TREND_PULLBACK_PREVIEW_TTL_SECONDS` | 预览有效时间(秒) | `120` |
| `TREND_PREVIEW_MAX_BALANCE_DRIFT_PCT` | 确认执行时允许「当前可用 / 预览快照」最大相对偏差(%) | `5` |