bot增加保本
This commit is contained in:
@@ -141,6 +141,8 @@ AI_MODEL=huihui_ai/deepseek-r1-abliterated:latest
|
|||||||
# 趋势回调策略(可选,见 趋势回调策略说明.md)
|
# 趋势回调策略(可选,见 趋势回调策略说明.md)
|
||||||
# TREND_PULLBACK_DCA_LEGS=5
|
# TREND_PULLBACK_DCA_LEGS=5
|
||||||
# TREND_PULLBACK_PREVIEW_TTL_SECONDS=120
|
# 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
|
# TREND_PREVIEW_MAX_BALANCE_DRIFT_PCT=5
|
||||||
|
|
||||||
APP_TIMEZONE=Asia/Shanghai
|
APP_TIMEZONE=Asia/Shanghai
|
||||||
|
|||||||
@@ -29,6 +29,19 @@ except ImportError:
|
|||||||
ImageFont = None # type: ignore
|
ImageFont = None # type: ignore
|
||||||
|
|
||||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
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):
|
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_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"))
|
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 = "趋势回调"
|
MONITOR_TYPE_TREND = "趋势回调"
|
||||||
KLINE_TIMEFRAME = os.getenv("KLINE_TIMEFRAME", "5m")
|
KLINE_TIMEFRAME = os.getenv("KLINE_TIMEFRAME", "5m")
|
||||||
FULL_MARGIN_BUFFER_RATIO = float(os.getenv("FULL_MARGIN_BUFFER_RATIO", "0.98"))
|
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")
|
c.execute("ALTER TABLE trend_pullback_plans ADD COLUMN leg_amounts_json TEXT")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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(
|
c.execute(
|
||||||
"""CREATE TABLE IF NOT EXISTS trend_pullback_previews (
|
"""CREATE TABLE IF NOT EXISTS trend_pullback_previews (
|
||||||
@@ -3514,8 +3540,35 @@ def trend_plan_history_status_label(status):
|
|||||||
}.get(s, status or "-")
|
}.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):
|
def enrich_active_trend_plan_row(row):
|
||||||
d = row_to_dict(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 "")
|
ex_sym = d.get("exchange_symbol") or normalize_exchange_symbol(d.get("symbol") or "")
|
||||||
direction = (d.get("direction") or "long").lower()
|
direction = (d.get("direction") or "long").lower()
|
||||||
m = get_live_position_exchange_metrics(ex_sym, direction)
|
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)
|
_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):
|
def _trend_weighted_avg(old_avg, old_amt, fill_px, add_amt):
|
||||||
try:
|
try:
|
||||||
oa, aa = float(old_amt), float(add_amt)
|
oa, aa = float(old_amt), float(add_amt)
|
||||||
@@ -4937,6 +5050,8 @@ def api_sync_positions():
|
|||||||
def render_main_page(page="trade"):
|
def render_main_page(page="trade"):
|
||||||
now = app_now()
|
now = app_now()
|
||||||
trading_day = get_trading_day(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()
|
conn = get_db()
|
||||||
session_row = ensure_session(conn, trading_day)
|
session_row = ensure_session(conn, trading_day)
|
||||||
local_current_capital = float(session_row["current_capital"])
|
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)
|
sync_trend_trade_records_from_exchange(conn)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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]
|
records = [to_effective_trade_dict(r) for r in raw_records]
|
||||||
total = len(records)
|
total = len(records)
|
||||||
miss_count = sum(1 for r in records if (r.get("effective_result") or "") == "错过")
|
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 = []
|
preview_snapshots = []
|
||||||
if page == "plan_history":
|
if page == "plan_history":
|
||||||
plan_history_raw = conn.execute(
|
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()
|
).fetchall()
|
||||||
for pr in plan_history_raw:
|
for pr in plan_history_raw:
|
||||||
pd = row_to_dict(pr)
|
pd = row_to_dict(pr)
|
||||||
pd["status_label"] = trend_plan_history_status_label(pd.get("status"))
|
pd["status_label"] = trend_plan_history_status_label(pd.get("status"))
|
||||||
plan_history.append(pd)
|
plan_history.append(pd)
|
||||||
snap_rows = conn.execute(
|
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()
|
).fetchall()
|
||||||
for sr in snap_rows:
|
for sr in snap_rows:
|
||||||
sd = row_to_dict(sr)
|
sd = row_to_dict(sr)
|
||||||
@@ -5067,6 +5193,14 @@ def render_main_page(page="trade"):
|
|||||||
trend_preview_expired=trend_preview_expired,
|
trend_preview_expired=trend_preview_expired,
|
||||||
trend_preview_id_arg=trend_preview_id_arg,
|
trend_preview_id_arg=trend_preview_id_arg,
|
||||||
trend_preview_max_drift_pct=TREND_PREVIEW_MAX_BALANCE_DRIFT_PCT,
|
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_key_id=(key_list[0]["id"] if key_list else None),
|
||||||
focus_order_id=(order_list[0]["id"] if order_list else None),
|
focus_order_id=(order_list[0]["id"] if order_list else None),
|
||||||
data_export_version=3,
|
data_export_version=3,
|
||||||
@@ -6019,10 +6153,10 @@ def execute_trend_pullback():
|
|||||||
opened_ms = _to_ms_with_fallback(None, opened_at)
|
opened_ms = _to_ms_with_fallback(None, opened_at)
|
||||||
cur = conn.execute(
|
cur = conn.execute(
|
||||||
"""INSERT INTO trend_pullback_plans (
|
"""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,
|
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
|
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",
|
"active",
|
||||||
symbol,
|
symbol,
|
||||||
@@ -6030,6 +6164,7 @@ def execute_trend_pullback():
|
|||||||
direction,
|
direction,
|
||||||
leverage,
|
leverage,
|
||||||
stop_loss,
|
stop_loss,
|
||||||
|
stop_loss,
|
||||||
add_upper,
|
add_upper,
|
||||||
take_profit,
|
take_profit,
|
||||||
risk_percent,
|
risk_percent,
|
||||||
@@ -6086,6 +6221,37 @@ def cancel_trend_pullback_preview():
|
|||||||
return redirect(url_for("trade_page"))
|
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>")
|
@app.route("/stop_trend_pullback/<int:pid>")
|
||||||
@login_required
|
@login_required
|
||||||
def stop_trend_pullback(pid):
|
def stop_trend_pullback(pid):
|
||||||
@@ -6222,12 +6388,17 @@ def _md_response(filename, content):
|
|||||||
@app.route("/export/trade_records")
|
@app.route("/export/trade_records")
|
||||||
@login_required
|
@login_required
|
||||||
def export_trade_records():
|
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()
|
conn = get_db()
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
"SELECT id,symbol,monitor_type,direction,trigger_price,stop_loss,take_profit,margin_capital,leverage,"
|
"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,"
|
"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,"
|
"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()
|
).fetchall()
|
||||||
conn.close()
|
conn.close()
|
||||||
head_base = [
|
head_base = [
|
||||||
|
|||||||
@@ -136,6 +136,8 @@
|
|||||||
.export-bar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:12px;font-size:.85rem}
|
.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{color:#8fc8ff;text-decoration:none;padding:6px 10px;border:1px solid #304164;border-radius:8px;background:#151a2a}
|
||||||
.export-bar a:hover{background:#1f2740}
|
.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{margin-top:12px;padding-top:10px;border-top:1px solid #2a3150}
|
||||||
.key-history h3{font-size:.88rem;color:#b8c4ff;margin-bottom:6px}
|
.key-history h3{font-size:.88rem;color:#b8c4ff;margin-bottom:6px}
|
||||||
.key-history .sub{font-size:.72rem;color:#8892b0;margin-bottom:6px}
|
.key-history .sub{font-size:.72rem;color:#8892b0;margin-bottom:6px}
|
||||||
@@ -198,6 +200,27 @@
|
|||||||
</div>
|
</div>
|
||||||
{% with msg=get_flashed_messages() %}{% if msg %}<div class="flash">{{ msg[0] }}</div>{% endif %}{% endwith %}
|
{% 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">
|
<div class="export-bar">
|
||||||
<span style="color:#9aa">数据导出(v{{ data_export_version }} CSV,UTF-8;交易记录含开仓类型列及交易所对齐字段):</span>
|
<span style="color:#9aa">数据导出(v{{ data_export_version }} CSV,UTF-8;交易记录含开仓类型列及交易所对齐字段):</span>
|
||||||
<a href="/export/trade_records">交易记录</a>
|
<a href="/export/trade_records">交易记录</a>
|
||||||
@@ -339,7 +362,7 @@
|
|||||||
<h2 style="margin-bottom:8px">趋势回调策略</h2>
|
<h2 style="margin-bottom:8px">趋势回调策略</h2>
|
||||||
<div class="rule-tip">
|
<div class="rule-tip">
|
||||||
① <strong>生成预览</strong>:读取合约 USDT <strong>可用余额快照</strong>并计算计划(不下单)。预览有效期 <strong>{{ trend_pullback_preview_ttl }} 秒</strong>。<br>
|
① <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>
|
||||||
确认执行时若当前可用余额与预览快照相对偏差 > <strong>{{ trend_preview_max_drift_pct }}%</strong> 会拒绝并要求重新预览。
|
确认执行时若当前可用余额与预览快照相对偏差 > <strong>{{ trend_preview_max_drift_pct }}%</strong> 会拒绝并要求重新预览。
|
||||||
</div>
|
</div>
|
||||||
<form id="trend-pullback-form" action="{{ url_for('preview_trend_pullback') }}" method="post" class="form-row">
|
<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>
|
<button type="submit" {% if not can_trade %}disabled style="opacity:.5;cursor:not-allowed"{% endif %}>生成预览</button>
|
||||||
</form>
|
</form>
|
||||||
<script>
|
<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(){
|
(function(){
|
||||||
const dirSel = document.getElementById("trend-direction");
|
const dirSel = document.getElementById("trend-direction");
|
||||||
const addInp = document.getElementById("trend-add-upper");
|
const addInp = document.getElementById("trend-add-upper");
|
||||||
@@ -490,6 +552,19 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<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.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 %}
|
| 计划保证金≈{% 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 ? "展开" : "折叠";
|
btn.innerText = collapsed ? "展开" : "折叠";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
attachListWindowToExports();
|
||||||
|
toggleListWindowCustom();
|
||||||
if(document.getElementById("journal-list")) loadJournals();
|
if(document.getElementById("journal-list")) loadJournals();
|
||||||
if(document.getElementById("review-list")) loadReviews();
|
if(document.getElementById("review-list")) loadReviews();
|
||||||
const reviewToggle = document.getElementById("review-mode-toggle");
|
const reviewToggle = document.getElementById("review-mode-toggle");
|
||||||
|
|||||||
@@ -27,6 +27,12 @@
|
|||||||
|
|
||||||
## 3. 执行流程(时间顺序)
|
## 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 预览阶段(不下单)
|
### 3.1 预览阶段(不下单)
|
||||||
|
|
||||||
1. **风控**:与「机器人下单监控」**互斥**——存在活跃机器人持仓或运行中趋势计划时,不可生成预览。
|
1. **风控**:与「机器人下单监控」**互斥**——存在活跃机器人持仓或运行中趋势计划时,不可生成预览。
|
||||||
@@ -41,6 +47,7 @@
|
|||||||
5. 再次校验:预览未过期;**当前可用余额**与预览快照相对偏差 ≤ `TREND_PREVIEW_MAX_BALANCE_DRIFT_PCT`(默认 **5%**),否则拒绝执行并要求重新预览。
|
5. 再次校验:预览未过期;**当前可用余额**与预览快照相对偏差 ≤ `TREND_PREVIEW_MAX_BALANCE_DRIFT_PCT`(默认 **5%**),否则拒绝执行并要求重新预览。
|
||||||
6. **首仓**:**立即市价** 开立 **总计划张数 × 50%**(不附带交易所止盈单)。
|
6. **首仓**:**立即市价** 开立 **总计划张数 × 50%**(不附带交易所止盈单)。
|
||||||
7. **止损**:撤销旧条件单后,挂 **仅止损** 的仓位触发单;之后每次补仓成交会 **刷新** 止损挂单。
|
7. **止损**:撤销旧条件单后,挂 **仅止损** 的仓位触发单;之后每次补仓成交会 **刷新** 止损挂单。
|
||||||
|
7b. **手动保本**(可选):首仓完成且交易所有持仓后,可在运行中计划卡片点击「应用保本止损」——将止损移至 **持仓均价 ± 偏移%**(默认 **+0.3%** 多 / **−0.3%** 空);仅当新止损 **优于** 当前止损时生效,并同步 Gate 仓位止损单。
|
||||||
8. **补仓**:当价格 **穿越** 下一档触发价(做多为自上向下穿越,做空为自下向上穿越)时,按该档张数 **市价加仓**;直至 `N` 档执行完毕或计划结束。
|
8. **补仓**:当价格 **穿越** 下一档触发价(做多为自上向下穿越,做空为自下向上穿越)时,按该档张数 **市价加仓**;直至 `N` 档执行完毕或计划结束。
|
||||||
9. **止盈监控**:后台线程若发现价格触及止盈,则 **市价全平**。
|
9. **止盈监控**:后台线程若发现价格触及止盈,则 **市价全平**。
|
||||||
10. **止损触发**:若仓位被交易所止损打光,本地检测到 **持仓为 0** 后记账为 **止损** 并结束计划。
|
10. **止损触发**:若仓位被交易所止损打光,本地检测到 **持仓为 0** 后记账为 **止损** 并结束计划。
|
||||||
@@ -59,6 +66,7 @@
|
|||||||
- **运行中的计划(交易执行页)**
|
- **运行中的计划(交易执行页)**
|
||||||
- 在计划摘要下方展示 **浮盈亏(交易所)**:来自 Gate 当前持仓接口的 **未实现盈亏**(及标记价,若可得);与本地按均价估算可能略有差异,以交易所为准便于对照。
|
- 在计划摘要下方展示 **浮盈亏(交易所)**:来自 Gate 当前持仓接口的 **未实现盈亏**(及标记价,若可得);与本地按均价估算可能略有差异,以交易所为准便于对照。
|
||||||
- **补仓边界**按方向显示「补仓上沿」或「补仓下沿」(数值仍为 `add_upper` 字段)。
|
- **补仓边界**按方向显示「补仓上沿」或「补仓下沿」(数值仍为 `add_upper` 字段)。
|
||||||
|
- **手动保本**:表单可改偏移 %(默认见 `TREND_PULLBACK_MANUAL_BREAKEVEN_OFFSET_PCT`);成功后显示「已保本」时间与原止损(若与当前不同)。
|
||||||
|
|
||||||
### 3.5 交易记录与交易所「已实现盈亏」对齐
|
### 3.5 交易记录与交易所「已实现盈亏」对齐
|
||||||
|
|
||||||
@@ -78,7 +86,7 @@
|
|||||||
| 开仓 | 单次市价 + 条件止盈+止损 | 首仓 50% 市价 + 多档补仓 + **仅止损在交易所** |
|
| 开仓 | 单次市价 + 条件止盈+止损 | 首仓 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_DCA_LEGS` | 剩余 50% 拆档数量上限 | `5` |
|
||||||
| `TREND_PULLBACK_PREVIEW_TTL_SECONDS` | 预览有效时间(秒) | `120` |
|
| `TREND_PULLBACK_PREVIEW_TTL_SECONDS` | 预览有效时间(秒) | `120` |
|
||||||
| `TREND_PREVIEW_MAX_BALANCE_DRIFT_PCT` | 确认执行时允许「当前可用 / 预览快照」最大相对偏差(%) | `5` |
|
| `TREND_PREVIEW_MAX_BALANCE_DRIFT_PCT` | 确认执行时允许「当前可用 / 预览快照」最大相对偏差(%) | `5` |
|
||||||
|
|||||||
Reference in New Issue
Block a user