bot增加保本
This commit is contained in:
@@ -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 = [
|
||||
|
||||
Reference in New Issue
Block a user