Normalize fullwidth punctuation to ASCII across codebase.

Add scripts/normalize_ambiguous_unicode.py; fix corrupted patch_instance_theme_templates.py. Preserves curly quotes in string literals; removes Git homoglyph warnings on .env.example.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-08 23:42:26 +08:00
parent aaa72c7961
commit b733e551a0
392 changed files with 71522 additions and 71369 deletions
+232 -232
View File
@@ -1,232 +1,232 @@
"""各交易所 app 模块 → strategy_register 配置统一工厂)。"""
from __future__ import annotations
import sys
from typing import Any
def resolve_trading_app_module(app_module: Any = None) -> Any:
"""
须在 login_required 定义之后调用
PM2 / python app.py 时 __name__ 为 __main__请传入 sys.modules[__name__]
"""
if app_module is None:
main = sys.modules.get("__main__")
if main is not None and hasattr(main, "login_required"):
m = main
else:
import inspect
m = None
for fr in inspect.stack():
g = fr.frame.f_globals
if callable(g.get("login_required")) and callable(g.get("get_db")):
m = g
break
if m is None:
raise RuntimeError(
"策略交易注册失败请使用 install_strategy_trading(app, repo_root, app_module=sys.modules[__name__])"
)
else:
m = app_module
if not hasattr(m, "login_required"):
raise RuntimeError(
"策略交易注册须在 login_required 定义之后执行将 install_strategy_trading 放在 app.py 末尾"
)
return m
def build_strategy_config(
app_module: Any = None, *, trend_enabled: bool = False, trend_disabled_note: str = ""
) -> dict:
m = resolve_trading_app_module(app_module)
def get_trading_capital_usdt(conn):
if hasattr(m, "get_exchange_capitals"):
_, tc = m.get_exchange_capitals(force=True)
if tc is not None:
return float(tc)
if hasattr(m, "get_available_trading_usdt"):
snap = m.get_available_trading_usdt()
if snap is not None:
return float(snap)
day = m.get_trading_day(m.app_now())
row = m.ensure_session(conn, day)
return float(row["current_capital"])
def get_position(ex_sym, direction):
from lib.hub.hub_position_metrics import normalize_contracts_qty
qty = m.get_live_position_contracts(ex_sym, direction)
entry = None
try:
rows = m.exchange.fetch_positions([ex_sym])
for p in rows or []:
matcher = getattr(m, "_row_matches_monitor_direction", None)
if matcher and not matcher(direction, p):
continue
contracts = getattr(m, "_position_row_effective_contracts", lambda x: abs(float(x.get("contracts") or 0)))(p)
if contracts <= 0:
continue
coerce = getattr(m, "_coerce_float", None)
if coerce:
entry = coerce(
p.get("entryPrice"),
p.get("average"),
(p.get("info") or {}).get("entryPrice"),
)
if entry:
break
except Exception:
pass
return {"contracts": normalize_contracts_qty(qty or 0), "entry_price": entry}
def amount_to_precision(ex_sym, amount):
try:
return float(m.exchange.amount_to_precision(ex_sym, float(amount)))
except Exception:
return None
def price_to_precision(ex_sym, price):
try:
return float(m.exchange.price_to_precision(ex_sym, float(price)))
except Exception:
return None
def market_add(ex_sym, direction, amount, leverage):
return m.place_exchange_order(ex_sym, direction, amount, leverage, stop_loss=None, take_profit=None)
def limit_add(ex_sym, direction, amount, price, leverage):
m.exchange.set_leverage(int(leverage), ex_sym)
side = "buy" if direction == "long" else "sell"
if hasattr(m, "build_okx_order_params"):
params = m.build_okx_order_params(direction, reduce_only=False)
elif hasattr(m, "build_binance_order_params"):
params = m.build_binance_order_params(direction, reduce_only=False)
elif hasattr(m, "build_gate_order_params"):
params = m.build_gate_order_params(direction, reduce_only=False)
else:
params = {}
return m.exchange.create_order(
ex_sym, "limit", side, float(amount), float(price), params if params is not None else {}
)
def replace_tpsl(ex_sym, direction, sl, tp, order_row):
row = order_row or {"symbol": ex_sym, "exchange_symbol": ex_sym, "direction": direction}
m.replace_active_monitor_tpsl_on_exchange(row, sl, tp)
def count_trends(conn):
try:
return int(
conn.execute(
"SELECT COUNT(*) FROM trend_pullback_plans WHERE status='active'"
).fetchone()[0]
)
except Exception:
return 0
def friendly_error(err):
fn = getattr(m, "friendly_exchange_error", None) or getattr(
m, "friendly_okx_error", None
)
if not callable(fn):
return str(err)
try:
snap = m.get_available_trading_usdt()
except Exception:
snap = None
try:
return fn(err, available_usdt=snap)
except TypeError:
return fn(err)
def limit_order_status(ex_sym, order_id):
fn = getattr(m, "fib_limit_order_status", None)
if callable(fn):
return fn(ex_sym, order_id)
return "unknown"
def cancel_limit_order(ex_sym, order_id):
fn = getattr(m, "cancel_fib_limit_order", None)
if callable(fn):
try:
return fn(ex_sym, order_id)
except Exception:
pass
if not order_id:
return False
try:
m.exchange.cancel_order(str(order_id), ex_sym)
return True
except Exception:
return False
def get_mark_price(symbol):
fn = getattr(m, "get_symbol_mark_price", None) or getattr(m, "get_price", None)
if not callable(fn):
return None
try:
return fn(symbol)
except Exception:
return None
def wechat_account_label():
fn = getattr(m, "_wechat_account_label", None)
if callable(fn):
try:
return fn()
except Exception:
pass
return getattr(m, "EXCHANGE_DISPLAY_NAME", "") or ""
def wechat_direction_text(direction):
fn = getattr(m, "_wechat_direction_text", None)
if callable(fn):
try:
return fn(direction)
except Exception:
pass
d = (direction or "long").strip().lower()
return "做多" if d == "long" else "做空"
def send_wechat(content):
fn = getattr(m, "send_wechat_msg", None)
if callable(fn):
fn(content)
note = trend_disabled_note or (
"趋势回调自动补仓请在 Gate机器人实例使用/strategy/trend"
)
return {
"app_module": m,
"exchange_display": getattr(m, "EXCHANGE_DISPLAY_NAME", ""),
"trend_enabled": trend_enabled,
"trend_disabled_note": note,
"login_required": m.login_required,
"get_db": m.get_db,
"normalize_symbol_input": m.normalize_symbol_input,
"normalize_exchange_symbol": m.normalize_exchange_symbol,
"get_price": m.get_price,
"get_trading_capital_usdt": get_trading_capital_usdt,
"get_position": get_position,
"amount_to_precision": amount_to_precision,
"price_to_precision": price_to_precision,
"market_add": market_add,
"limit_add": limit_add,
"replace_tpsl": replace_tpsl,
"ensure_live_ready": m.ensure_exchange_live_ready,
"default_risk_percent": float(getattr(m, "RISK_PERCENT", 2)),
"default_leverage": m.infer_leverage,
"friendly_error": friendly_error,
"app_now_str": m.app_now_str,
"resolve_fill_price": m.resolve_order_entry_price,
"price_fmt": m.format_price_for_symbol,
"count_active_trend_plans": count_trends if trend_enabled else count_trends,
"limit_order_status": limit_order_status,
"cancel_limit_order": cancel_limit_order,
"get_mark_price": get_mark_price,
"send_wechat": send_wechat,
"format_price": getattr(m, "format_price_for_symbol", None),
"wechat_account_label": wechat_account_label,
"wechat_direction_text": wechat_direction_text,
}
"""各交易所 app 模块 → strategy_register 配置(统一工厂)."""
from __future__ import annotations
import sys
from typing import Any
def resolve_trading_app_module(app_module: Any = None) -> Any:
"""
须在 login_required 定义之后调用.
PM2 / python app.py 时 __name__ 为 __main__,请传入 sys.modules[__name__].
"""
if app_module is None:
main = sys.modules.get("__main__")
if main is not None and hasattr(main, "login_required"):
m = main
else:
import inspect
m = None
for fr in inspect.stack():
g = fr.frame.f_globals
if callable(g.get("login_required")) and callable(g.get("get_db")):
m = g
break
if m is None:
raise RuntimeError(
"策略交易注册失败:请使用 install_strategy_trading(app, repo_root, app_module=sys.modules[__name__])"
)
else:
m = app_module
if not hasattr(m, "login_required"):
raise RuntimeError(
"策略交易注册须在 login_required 定义之后执行(将 install_strategy_trading 放在 app.py 末尾)"
)
return m
def build_strategy_config(
app_module: Any = None, *, trend_enabled: bool = False, trend_disabled_note: str = ""
) -> dict:
m = resolve_trading_app_module(app_module)
def get_trading_capital_usdt(conn):
if hasattr(m, "get_exchange_capitals"):
_, tc = m.get_exchange_capitals(force=True)
if tc is not None:
return float(tc)
if hasattr(m, "get_available_trading_usdt"):
snap = m.get_available_trading_usdt()
if snap is not None:
return float(snap)
day = m.get_trading_day(m.app_now())
row = m.ensure_session(conn, day)
return float(row["current_capital"])
def get_position(ex_sym, direction):
from lib.hub.hub_position_metrics import normalize_contracts_qty
qty = m.get_live_position_contracts(ex_sym, direction)
entry = None
try:
rows = m.exchange.fetch_positions([ex_sym])
for p in rows or []:
matcher = getattr(m, "_row_matches_monitor_direction", None)
if matcher and not matcher(direction, p):
continue
contracts = getattr(m, "_position_row_effective_contracts", lambda x: abs(float(x.get("contracts") or 0)))(p)
if contracts <= 0:
continue
coerce = getattr(m, "_coerce_float", None)
if coerce:
entry = coerce(
p.get("entryPrice"),
p.get("average"),
(p.get("info") or {}).get("entryPrice"),
)
if entry:
break
except Exception:
pass
return {"contracts": normalize_contracts_qty(qty or 0), "entry_price": entry}
def amount_to_precision(ex_sym, amount):
try:
return float(m.exchange.amount_to_precision(ex_sym, float(amount)))
except Exception:
return None
def price_to_precision(ex_sym, price):
try:
return float(m.exchange.price_to_precision(ex_sym, float(price)))
except Exception:
return None
def market_add(ex_sym, direction, amount, leverage):
return m.place_exchange_order(ex_sym, direction, amount, leverage, stop_loss=None, take_profit=None)
def limit_add(ex_sym, direction, amount, price, leverage):
m.exchange.set_leverage(int(leverage), ex_sym)
side = "buy" if direction == "long" else "sell"
if hasattr(m, "build_okx_order_params"):
params = m.build_okx_order_params(direction, reduce_only=False)
elif hasattr(m, "build_binance_order_params"):
params = m.build_binance_order_params(direction, reduce_only=False)
elif hasattr(m, "build_gate_order_params"):
params = m.build_gate_order_params(direction, reduce_only=False)
else:
params = {}
return m.exchange.create_order(
ex_sym, "limit", side, float(amount), float(price), params if params is not None else {}
)
def replace_tpsl(ex_sym, direction, sl, tp, order_row):
row = order_row or {"symbol": ex_sym, "exchange_symbol": ex_sym, "direction": direction}
m.replace_active_monitor_tpsl_on_exchange(row, sl, tp)
def count_trends(conn):
try:
return int(
conn.execute(
"SELECT COUNT(*) FROM trend_pullback_plans WHERE status='active'"
).fetchone()[0]
)
except Exception:
return 0
def friendly_error(err):
fn = getattr(m, "friendly_exchange_error", None) or getattr(
m, "friendly_okx_error", None
)
if not callable(fn):
return str(err)
try:
snap = m.get_available_trading_usdt()
except Exception:
snap = None
try:
return fn(err, available_usdt=snap)
except TypeError:
return fn(err)
def limit_order_status(ex_sym, order_id):
fn = getattr(m, "fib_limit_order_status", None)
if callable(fn):
return fn(ex_sym, order_id)
return "unknown"
def cancel_limit_order(ex_sym, order_id):
fn = getattr(m, "cancel_fib_limit_order", None)
if callable(fn):
try:
return fn(ex_sym, order_id)
except Exception:
pass
if not order_id:
return False
try:
m.exchange.cancel_order(str(order_id), ex_sym)
return True
except Exception:
return False
def get_mark_price(symbol):
fn = getattr(m, "get_symbol_mark_price", None) or getattr(m, "get_price", None)
if not callable(fn):
return None
try:
return fn(symbol)
except Exception:
return None
def wechat_account_label():
fn = getattr(m, "_wechat_account_label", None)
if callable(fn):
try:
return fn()
except Exception:
pass
return getattr(m, "EXCHANGE_DISPLAY_NAME", "") or ""
def wechat_direction_text(direction):
fn = getattr(m, "_wechat_direction_text", None)
if callable(fn):
try:
return fn(direction)
except Exception:
pass
d = (direction or "long").strip().lower()
return "做多" if d == "long" else "做空"
def send_wechat(content):
fn = getattr(m, "send_wechat_msg", None)
if callable(fn):
fn(content)
note = trend_disabled_note or (
"趋势回调(自动补仓)请在 Gate机器人实例使用:/strategy/trend"
)
return {
"app_module": m,
"exchange_display": getattr(m, "EXCHANGE_DISPLAY_NAME", ""),
"trend_enabled": trend_enabled,
"trend_disabled_note": note,
"login_required": m.login_required,
"get_db": m.get_db,
"normalize_symbol_input": m.normalize_symbol_input,
"normalize_exchange_symbol": m.normalize_exchange_symbol,
"get_price": m.get_price,
"get_trading_capital_usdt": get_trading_capital_usdt,
"get_position": get_position,
"amount_to_precision": amount_to_precision,
"price_to_precision": price_to_precision,
"market_add": market_add,
"limit_add": limit_add,
"replace_tpsl": replace_tpsl,
"ensure_live_ready": m.ensure_exchange_live_ready,
"default_risk_percent": float(getattr(m, "RISK_PERCENT", 2)),
"default_leverage": m.infer_leverage,
"friendly_error": friendly_error,
"app_now_str": m.app_now_str,
"resolve_fill_price": m.resolve_order_entry_price,
"price_fmt": m.format_price_for_symbol,
"count_active_trend_plans": count_trends if trend_enabled else count_trends,
"limit_order_status": limit_order_status,
"cancel_limit_order": cancel_limit_order,
"get_mark_price": get_mark_price,
"send_wechat": send_wechat,
"format_price": getattr(m, "format_price_for_symbol", None),
"wechat_account_label": wechat_account_label,
"wechat_direction_text": wechat_direction_text,
}
+164 -164
View File
@@ -1,164 +1,164 @@
"""策略交易相关表结构各所 crypto.db 共用 schema)。"""
ROLL_GROUPS_SQL = """
CREATE TABLE IF NOT EXISTS roll_groups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_monitor_id INTEGER,
symbol TEXT NOT NULL,
exchange_symbol TEXT,
direction TEXT NOT NULL,
initial_take_profit REAL,
initial_stop_loss REAL,
current_stop_loss REAL,
risk_percent REAL DEFAULT 2,
leg_count INTEGER DEFAULT 0,
status TEXT DEFAULT 'active',
created_at TEXT,
updated_at TEXT
)
"""
ROLL_LEGS_SQL = """
CREATE TABLE IF NOT EXISTS roll_legs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
roll_group_id INTEGER NOT NULL,
leg_index INTEGER NOT NULL,
add_mode TEXT NOT NULL,
fib_upper REAL,
fib_lower REAL,
limit_price REAL,
fill_price REAL,
amount REAL,
new_stop_loss REAL,
exchange_order_id TEXT,
status TEXT DEFAULT 'filled',
created_at TEXT,
FOREIGN KEY (roll_group_id) REFERENCES roll_groups(id)
)
"""
TREND_PLANS_SQL = """
CREATE TABLE IF NOT EXISTS trend_pullback_plans (
id INTEGER PRIMARY KEY AUTOINCREMENT,
status TEXT DEFAULT 'active',
symbol TEXT NOT NULL,
exchange_symbol TEXT,
direction TEXT NOT NULL DEFAULT 'long',
leverage INTEGER NOT NULL,
stop_loss REAL NOT NULL,
add_upper REAL NOT NULL,
take_profit REAL NOT NULL,
risk_percent REAL DEFAULT 5,
snapshot_available_usdt REAL,
snapshot_at TEXT,
plan_margin_capital REAL,
target_order_amount REAL,
first_order_amount REAL,
remainder_total REAL,
dca_legs INTEGER DEFAULT 5,
per_leg_amount REAL,
grid_prices_json TEXT,
leg_amounts_json TEXT,
legs_done INTEGER DEFAULT 0,
first_order_done INTEGER DEFAULT 0,
last_mark_price REAL,
avg_entry_price REAL,
order_amount_open REAL,
opened_at TEXT,
opened_at_ms INTEGER,
session_date TEXT,
message TEXT,
initial_stop_loss REAL,
breakeven_applied INTEGER DEFAULT 0,
breakeven_applied_at TEXT
)
"""
TREND_PREVIEWS_SQL = """
CREATE TABLE IF NOT EXISTS trend_pullback_previews (
id TEXT PRIMARY KEY,
symbol TEXT NOT NULL,
exchange_symbol TEXT NOT NULL,
direction TEXT NOT NULL,
leverage INTEGER NOT NULL,
stop_loss REAL NOT NULL,
add_upper REAL NOT NULL,
take_profit REAL NOT NULL,
risk_percent REAL NOT NULL,
snapshot_available_usdt REAL NOT NULL,
snapshot_at TEXT,
live_price_ref REAL,
plan_margin_capital REAL,
target_order_amount REAL,
first_order_amount REAL,
remainder_total REAL,
dca_legs INTEGER,
per_leg_amount REAL,
grid_prices_json TEXT,
leg_amounts_json TEXT,
expires_at_ms INTEGER NOT NULL,
created_at TEXT
)
"""
TREND_PREVIEW_SNAPSHOTS_SQL = """
CREATE TABLE IF NOT EXISTS trend_pullback_preview_snapshots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
preview_id TEXT NOT NULL UNIQUE,
symbol TEXT NOT NULL,
exchange_symbol TEXT NOT NULL,
direction TEXT NOT NULL,
leverage INTEGER NOT NULL,
stop_loss REAL NOT NULL,
add_upper REAL NOT NULL,
take_profit REAL NOT NULL,
risk_percent REAL NOT NULL,
snapshot_available_usdt REAL NOT NULL,
snapshot_at TEXT,
live_price_ref REAL,
plan_margin_capital REAL,
target_order_amount REAL,
first_order_amount REAL,
remainder_total REAL,
dca_legs INTEGER,
per_leg_amount REAL,
grid_prices_json TEXT,
leg_amounts_json TEXT,
expires_at_ms INTEGER NOT NULL,
preview_created_at TEXT,
outcome TEXT DEFAULT 'open',
executed_plan_id INTEGER
)
"""
def init_strategy_tables(conn) -> None:
from lib.strategy.strategy_snapshot_lib import init_strategy_snapshot_table
conn.execute(ROLL_GROUPS_SQL)
conn.execute(ROLL_LEGS_SQL)
conn.execute(TREND_PLANS_SQL)
conn.execute(TREND_PREVIEWS_SQL)
conn.execute(TREND_PREVIEW_SNAPSHOTS_SQL)
init_strategy_snapshot_table(conn)
for ddl in (
"ALTER TABLE trend_pullback_plans ADD COLUMN leg_amounts_json TEXT",
"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",
"ALTER TABLE trend_pullback_preview_snapshots ADD COLUMN preview_created_at TEXT",
"ALTER TABLE trend_pullback_preview_snapshots ADD COLUMN outcome TEXT DEFAULT 'open'",
"ALTER TABLE trend_pullback_preview_snapshots ADD COLUMN executed_plan_id INTEGER",
"ALTER TABLE trade_records ADD COLUMN trend_plan_id INTEGER",
"ALTER TABLE order_monitors ADD COLUMN trend_plan_id INTEGER",
"ALTER TABLE order_monitors ADD COLUMN monitor_type TEXT",
"ALTER TABLE order_monitors ADD COLUMN key_signal_type TEXT",
"ALTER TABLE trend_pullback_plans ADD COLUMN leg_fill_prices_json TEXT",
"ALTER TABLE roll_legs ADD COLUMN stop_offset_pct REAL",
"ALTER TABLE roll_legs ADD COLUMN breakthrough_price REAL",
"ALTER TABLE roll_legs ADD COLUMN last_mark_price REAL",
):
try:
conn.execute(ddl)
except Exception:
pass
"""策略交易相关表结构(各所 crypto.db 共用 schema)."""
ROLL_GROUPS_SQL = """
CREATE TABLE IF NOT EXISTS roll_groups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_monitor_id INTEGER,
symbol TEXT NOT NULL,
exchange_symbol TEXT,
direction TEXT NOT NULL,
initial_take_profit REAL,
initial_stop_loss REAL,
current_stop_loss REAL,
risk_percent REAL DEFAULT 2,
leg_count INTEGER DEFAULT 0,
status TEXT DEFAULT 'active',
created_at TEXT,
updated_at TEXT
)
"""
ROLL_LEGS_SQL = """
CREATE TABLE IF NOT EXISTS roll_legs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
roll_group_id INTEGER NOT NULL,
leg_index INTEGER NOT NULL,
add_mode TEXT NOT NULL,
fib_upper REAL,
fib_lower REAL,
limit_price REAL,
fill_price REAL,
amount REAL,
new_stop_loss REAL,
exchange_order_id TEXT,
status TEXT DEFAULT 'filled',
created_at TEXT,
FOREIGN KEY (roll_group_id) REFERENCES roll_groups(id)
)
"""
TREND_PLANS_SQL = """
CREATE TABLE IF NOT EXISTS trend_pullback_plans (
id INTEGER PRIMARY KEY AUTOINCREMENT,
status TEXT DEFAULT 'active',
symbol TEXT NOT NULL,
exchange_symbol TEXT,
direction TEXT NOT NULL DEFAULT 'long',
leverage INTEGER NOT NULL,
stop_loss REAL NOT NULL,
add_upper REAL NOT NULL,
take_profit REAL NOT NULL,
risk_percent REAL DEFAULT 5,
snapshot_available_usdt REAL,
snapshot_at TEXT,
plan_margin_capital REAL,
target_order_amount REAL,
first_order_amount REAL,
remainder_total REAL,
dca_legs INTEGER DEFAULT 5,
per_leg_amount REAL,
grid_prices_json TEXT,
leg_amounts_json TEXT,
legs_done INTEGER DEFAULT 0,
first_order_done INTEGER DEFAULT 0,
last_mark_price REAL,
avg_entry_price REAL,
order_amount_open REAL,
opened_at TEXT,
opened_at_ms INTEGER,
session_date TEXT,
message TEXT,
initial_stop_loss REAL,
breakeven_applied INTEGER DEFAULT 0,
breakeven_applied_at TEXT
)
"""
TREND_PREVIEWS_SQL = """
CREATE TABLE IF NOT EXISTS trend_pullback_previews (
id TEXT PRIMARY KEY,
symbol TEXT NOT NULL,
exchange_symbol TEXT NOT NULL,
direction TEXT NOT NULL,
leverage INTEGER NOT NULL,
stop_loss REAL NOT NULL,
add_upper REAL NOT NULL,
take_profit REAL NOT NULL,
risk_percent REAL NOT NULL,
snapshot_available_usdt REAL NOT NULL,
snapshot_at TEXT,
live_price_ref REAL,
plan_margin_capital REAL,
target_order_amount REAL,
first_order_amount REAL,
remainder_total REAL,
dca_legs INTEGER,
per_leg_amount REAL,
grid_prices_json TEXT,
leg_amounts_json TEXT,
expires_at_ms INTEGER NOT NULL,
created_at TEXT
)
"""
TREND_PREVIEW_SNAPSHOTS_SQL = """
CREATE TABLE IF NOT EXISTS trend_pullback_preview_snapshots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
preview_id TEXT NOT NULL UNIQUE,
symbol TEXT NOT NULL,
exchange_symbol TEXT NOT NULL,
direction TEXT NOT NULL,
leverage INTEGER NOT NULL,
stop_loss REAL NOT NULL,
add_upper REAL NOT NULL,
take_profit REAL NOT NULL,
risk_percent REAL NOT NULL,
snapshot_available_usdt REAL NOT NULL,
snapshot_at TEXT,
live_price_ref REAL,
plan_margin_capital REAL,
target_order_amount REAL,
first_order_amount REAL,
remainder_total REAL,
dca_legs INTEGER,
per_leg_amount REAL,
grid_prices_json TEXT,
leg_amounts_json TEXT,
expires_at_ms INTEGER NOT NULL,
preview_created_at TEXT,
outcome TEXT DEFAULT 'open',
executed_plan_id INTEGER
)
"""
def init_strategy_tables(conn) -> None:
from lib.strategy.strategy_snapshot_lib import init_strategy_snapshot_table
conn.execute(ROLL_GROUPS_SQL)
conn.execute(ROLL_LEGS_SQL)
conn.execute(TREND_PLANS_SQL)
conn.execute(TREND_PREVIEWS_SQL)
conn.execute(TREND_PREVIEW_SNAPSHOTS_SQL)
init_strategy_snapshot_table(conn)
for ddl in (
"ALTER TABLE trend_pullback_plans ADD COLUMN leg_amounts_json TEXT",
"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",
"ALTER TABLE trend_pullback_preview_snapshots ADD COLUMN preview_created_at TEXT",
"ALTER TABLE trend_pullback_preview_snapshots ADD COLUMN outcome TEXT DEFAULT 'open'",
"ALTER TABLE trend_pullback_preview_snapshots ADD COLUMN executed_plan_id INTEGER",
"ALTER TABLE trade_records ADD COLUMN trend_plan_id INTEGER",
"ALTER TABLE order_monitors ADD COLUMN trend_plan_id INTEGER",
"ALTER TABLE order_monitors ADD COLUMN monitor_type TEXT",
"ALTER TABLE order_monitors ADD COLUMN key_signal_type TEXT",
"ALTER TABLE trend_pullback_plans ADD COLUMN leg_fill_prices_json TEXT",
"ALTER TABLE roll_legs ADD COLUMN stop_offset_pct REAL",
"ALTER TABLE roll_legs ADD COLUMN breakthrough_price REAL",
"ALTER TABLE roll_legs ADD COLUMN last_mark_price REAL",
):
try:
conn.execute(ddl)
except Exception:
pass
+2 -2
View File
@@ -1,4 +1,4 @@
"""交易所策略适配器接口各所 app 注入 ccxt 实现)。"""
"""交易所策略适配器接口(各所 app 注入 ccxt 实现)."""
from __future__ import annotations
from typing import Any, Optional, Protocol
@@ -14,7 +14,7 @@ class StrategyExchangeAdapter(Protocol):
def get_mark_price(self, symbol: str) -> Optional[float]: ...
def get_position(self, exchange_symbol: str, direction: str) -> dict[str, Any]:
"""返回 {contracts, entry_price, leverage?}"""
"""返回 {contracts, entry_price, leverage?}."""
...
def amount_to_precision(self, exchange_symbol: str, amount: float) -> Optional[float]: ...
+4 -4
View File
@@ -1,4 +1,4 @@
"""Binance USDT-M 永续 — 策略交易交易所适配见 strategy_config.build_strategy_config)。"""
from lib.strategy.strategy_exchange_base import StrategyExchangeAdapter
__all__ = ["StrategyExchangeAdapter"]
"""Binance USDT-M 永续 — 策略交易交易所适配(见 strategy_config.build_strategy_config)."""
from lib.strategy.strategy_exchange_base import StrategyExchangeAdapter
__all__ = ["StrategyExchangeAdapter"]
+9 -9
View File
@@ -1,9 +1,9 @@
"""
Gate.io USDT 永续 — 策略交易交易所侧能力
实现方式各 Gate 实例 app 通过 strategy_config.build_strategy_config(app_module) 注入
ccxt 下单精度换 TP/SL本文件为文档与类型锚点避免在各 app 重复实现滚仓公式
"""
from lib.strategy.strategy_exchange_base import StrategyExchangeAdapter
__all__ = ["StrategyExchangeAdapter"]
"""
Gate.io USDT 永续 — 策略交易交易所侧能力.
实现方式:各 Gate 实例 app 通过 strategy_config.build_strategy_config(app_module) 注入
ccxt 下单,精度,换 TP/SL;本文件为文档与类型锚点,避免在各 app 重复实现滚仓公式.
"""
from lib.strategy.strategy_exchange_base import StrategyExchangeAdapter
__all__ = ["StrategyExchangeAdapter"]
+4 -4
View File
@@ -1,4 +1,4 @@
"""OKX 永续 — 策略交易交易所适配见 strategy_config.build_strategy_config)。"""
from lib.strategy.strategy_exchange_base import StrategyExchangeAdapter
__all__ = ["StrategyExchangeAdapter"]
"""OKX 永续 — 策略交易交易所适配(见 strategy_config.build_strategy_config)."""
from lib.strategy.strategy_exchange_base import StrategyExchangeAdapter
__all__ = ["StrategyExchangeAdapter"]
+72 -72
View File
@@ -1,72 +1,72 @@
"""策略交易记录页已结束趋势 / 顺势加仓快照三所统一)。"""
from __future__ import annotations
import json
from typing import Any
from flask import flash, redirect, url_for
from lib.strategy.strategy_snapshot_lib import (
STRATEGY_SNAPSHOTS_MAX_ROWS,
dedupe_strategy_snapshots,
list_strategy_snapshots_split,
)
def load_strategy_records_page(
conn, *, limit: int = STRATEGY_SNAPSHOTS_MAX_ROWS
) -> dict[str, Any]:
try:
if dedupe_strategy_snapshots(conn):
conn.commit()
except Exception:
pass
trend, roll, symbols = list_strategy_snapshots_split(conn, limit=limit)
return {
"strategy_trend_records": trend,
"strategy_roll_records": roll,
"strategy_record_symbols": symbols,
"strategy_records_limit": limit,
"strategy_snapshots": trend + roll,
}
def register_strategy_records(app, cfg: dict[str, Any]) -> None:
login_required = cfg["login_required"]
get_db = cfg["get_db"]
def _lr(f):
return login_required(f)
@_lr
@app.route("/strategy/records")
def strategy_records_page():
m = cfg.get("app_module")
fn = getattr(m, "render_main_page", None)
if not callable(fn):
flash("render_main_page 未配置")
return redirect(url_for("strategy_trading_page"))
return fn("strategy_records")
@_lr
@app.route("/strategy/records/<int:snap_id>")
def strategy_records_detail(snap_id: int):
conn = get_db()
row = conn.execute(
"SELECT * FROM strategy_trade_snapshots WHERE id=?",
(int(snap_id),),
).fetchone()
conn.close()
if not row:
flash("未找到该策略快照")
return redirect(url_for("strategy_records_page"))
try:
snap = json.loads(row["snapshot_json"] or "{}")
except Exception:
snap = {}
dca = snap.get("dca_levels") or []
flash(
f"快照 #{snap_id} {row['strategy_type']} {row['symbol']} "
f"{row['result_label']} · 补仓档 {len(dca)}详情见列表页"
)
return redirect(url_for("strategy_records_page"))
"""策略交易记录页:已结束趋势 / 顺势加仓快照(三所统一)."""
from __future__ import annotations
import json
from typing import Any
from flask import flash, redirect, url_for
from lib.strategy.strategy_snapshot_lib import (
STRATEGY_SNAPSHOTS_MAX_ROWS,
dedupe_strategy_snapshots,
list_strategy_snapshots_split,
)
def load_strategy_records_page(
conn, *, limit: int = STRATEGY_SNAPSHOTS_MAX_ROWS
) -> dict[str, Any]:
try:
if dedupe_strategy_snapshots(conn):
conn.commit()
except Exception:
pass
trend, roll, symbols = list_strategy_snapshots_split(conn, limit=limit)
return {
"strategy_trend_records": trend,
"strategy_roll_records": roll,
"strategy_record_symbols": symbols,
"strategy_records_limit": limit,
"strategy_snapshots": trend + roll,
}
def register_strategy_records(app, cfg: dict[str, Any]) -> None:
login_required = cfg["login_required"]
get_db = cfg["get_db"]
def _lr(f):
return login_required(f)
@_lr
@app.route("/strategy/records")
def strategy_records_page():
m = cfg.get("app_module")
fn = getattr(m, "render_main_page", None)
if not callable(fn):
flash("render_main_page 未配置")
return redirect(url_for("strategy_trading_page"))
return fn("strategy_records")
@_lr
@app.route("/strategy/records/<int:snap_id>")
def strategy_records_detail(snap_id: int):
conn = get_db()
row = conn.execute(
"SELECT * FROM strategy_trade_snapshots WHERE id=?",
(int(snap_id),),
).fetchone()
conn.close()
if not row:
flash("未找到该策略快照")
return redirect(url_for("strategy_records_page"))
try:
snap = json.loads(row["snapshot_json"] or "{}")
except Exception:
snap = {}
dca = snap.get("dca_levels") or []
flash(
f"快照 #{snap_id} {row['strategy_type']} {row['symbol']} "
f"{row['result_label']} · 补仓档 {len(dca)}(详情见列表页)"
)
return redirect(url_for("strategy_records_page"))
File diff suppressed because it is too large Load Diff
+384 -384
View File
@@ -1,384 +1,384 @@
"""顺势加仓滚仓):纯计算人工触发止盈锁定首仓程序监控触价市价成交"""
from __future__ import annotations
from typing import Any, Optional, Tuple
from lib.key_monitor.fib_key_monitor_lib import calc_fib_plan, fib_invalidate_by_mark
ROLL_MAX_LEGS_LONG = 3
ROLL_MAX_LEGS_SHORT = 3
MARKET_MODE = "market"
FIB_MODES = frozenset({"fib_618", "fib_786"})
BREAKOUT_MODE = "breakout"
MODE_LABELS = {
MARKET_MODE: "市价加仓",
"fib_618": "斐波0.618",
"fib_786": "斐波0.786",
BREAKOUT_MODE: "突破加仓",
}
def fib_ratio_from_mode(mode: str) -> Optional[float]:
m = (mode or "").strip().lower()
if m in ("fib_618", "618", "0.618"):
return 0.618
if m in ("fib_786", "786", "0.786"):
return 0.786
return None
def mode_label(mode: str) -> str:
m = (mode or MARKET_MODE).strip().lower()
return MODE_LABELS.get(m, m)
def fib_limit_entry(direction: str, upper: float, lower: float, mode: str) -> Tuple[Optional[float], Optional[str]]:
"""H/L 仅用于计算限价加仓价;多:下沿=止损侧;空:上沿=止损侧"""
ratio = fib_ratio_from_mode(mode)
if ratio is None:
return None, "斐波档位无效"
h, l = float(upper), float(lower)
if h <= l:
return None, "上沿须大于下沿"
direction = (direction or "long").strip().lower()
if direction == "short":
plan = calc_fib_plan("short", h, l, ratio)
else:
plan = calc_fib_plan("long", h, l, ratio)
if not plan:
return None, "无法计算斐波限价"
entry, _sl, _tp = plan
return float(entry), None
def max_roll_legs(direction: str) -> int:
return ROLL_MAX_LEGS_LONG if (direction or "long").strip().lower() == "long" else ROLL_MAX_LEGS_SHORT
def avg_entry_after_add(
qty_existing: float,
entry_existing: float,
add_qty: float,
add_price: float,
) -> float:
q1 = float(qty_existing)
e1 = float(entry_existing)
q2 = float(add_qty)
e2 = float(add_price)
total = q1 + q2
if total <= 0:
return 0.0
return (q1 * e1 + q2 * e2) / total
def calc_risk_budget_usdt(capital_base_usdt: float, risk_percent: float) -> float:
return float(capital_base_usdt) * (float(risk_percent) / 100.0)
def solve_add_amount_for_total_risk(
direction: str,
qty_existing: float,
entry_existing: float,
add_price: float,
new_stop: float,
risk_budget_usdt: float,
contract_size: float = 1.0,
) -> Tuple[Optional[float], Optional[str]]:
"""
合并持仓打到 new_stop 时总亏损 ≈ risk_budget方案 C)。
long: (avg - SL) * (Q1+Q2) * cs = B => Q2 = (B/cs - Q1*(E1-SL)) / (E2-SL)
short: (SL - avg) * (Q1+Q2) * cs = B => Q2 = (B/cs - Q1*(SL-E1)) / (SL-E2)
"""
try:
q1 = float(qty_existing)
e1 = float(entry_existing)
e2 = float(add_price)
sl = float(new_stop)
b = float(risk_budget_usdt)
cs = float(contract_size) if contract_size else 1.0
except (TypeError, ValueError):
return None, "参数格式错误"
if q1 <= 0 or e1 <= 0 or e2 <= 0 or b <= 0 or cs <= 0:
return None, "持仓或风险预算无效"
direction = (direction or "long").strip().lower()
if direction == "short":
denom = sl - e2
numer = b / cs - q1 * (sl - e1)
if denom <= 0:
return None, "做空新止损须高于加仓价"
else:
denom = e2 - sl
numer = b / cs - q1 * (e1 - sl)
if denom <= 0:
return None, "做多新止损须低于加仓价"
q2 = numer / denom
if q2 <= 0:
return None, "按当前新止损与风险预算无需加仓或无法再加已满足风险上限"
return q2, None
def loss_at_stop_usdt(
direction: str,
avg: float,
qty: float,
stop: float,
contract_size: float = 1.0,
) -> float:
cs = float(contract_size or 1.0)
direction = (direction or "long").strip().lower()
if direction == "short":
return (float(stop) - float(avg)) * float(qty) * cs
return (float(avg) - float(stop)) * float(qty) * cs
def reward_at_tp_usdt(
direction: str,
avg: float,
take_profit: float,
qty: float,
contract_size: float = 1.0,
) -> float:
cs = float(contract_size or 1.0)
direction = (direction or "long").strip().lower()
if direction == "short":
return (float(avg) - float(take_profit)) * float(qty) * cs
return (float(take_profit) - float(avg)) * float(qty) * cs
def roll_fib_trigger_crossed(
direction: str,
prev_mark: Optional[float],
mark: float,
limit_price: float,
) -> bool:
"""斐波多=向下穿越限价空=向上穿越限价"""
try:
m = float(mark)
lv = float(limit_price)
pm = float(prev_mark) if prev_mark is not None else None
except (TypeError, ValueError):
return False
direction = (direction or "long").strip().lower()
if direction == "long":
if pm is None:
return m <= lv
return pm > lv and m <= lv
if pm is None:
return m >= lv
return pm < lv and m >= lv
def roll_breakout_trigger_crossed(
direction: str,
prev_mark: Optional[float],
mark: float,
breakthrough_price: float,
) -> bool:
"""突破多=mark 在突破价之上空=mark 在突破价之下
提交时已校验 mark 在逆势侧多低于突破价空高于突破价),触价侧到达即成交
不再要求单 tick 内穿越避免 mark 已破位但 last_mark 也落在突破价另一侧时永久漏触发
"""
try:
m = float(mark)
bp = float(breakthrough_price)
except (TypeError, ValueError):
return False
direction = (direction or "long").strip().lower()
if direction == "long":
return m > bp
return m < bp
def roll_fib_invalidate(direction: str, mark: float, upper: float, lower: float) -> bool:
"""斐波 pending 失效止盈侧突破多 mark>=H空 mark<=L)。"""
return fib_invalidate_by_mark(direction, mark, upper, lower)
def roll_breakout_invalidate(direction: str, mark: float, stop_loss: float) -> bool:
"""突破 pending 失效未到突破价先触达止损侧多 mark<=S空 mark>=S)。"""
try:
m = float(mark)
sl = float(stop_loss)
except (TypeError, ValueError):
return False
direction = (direction or "long").strip().lower()
if direction == "long":
return m <= sl
return m >= sl
def validate_roll_geometry(
direction: str,
add_mode: str,
*,
new_stop_loss: float,
add_price: Optional[float] = None,
fib_upper: Optional[float] = None,
fib_lower: Optional[float] = None,
breakthrough_price: Optional[float] = None,
entry_existing: float = 0.0,
initial_take_profit: float = 0.0,
mark_price: Optional[float] = None,
) -> Optional[str]:
direction = (direction or "long").strip().lower()
mode = (add_mode or MARKET_MODE).strip().lower()
try:
sl = float(new_stop_loss)
tp = float(initial_take_profit)
e1 = float(entry_existing or 0)
except (TypeError, ValueError):
return "止损/止盈格式错误"
if sl <= 0 or tp <= 0:
return "止损与首仓止盈须大于0"
if direction == "long":
if e1 > 0 and tp <= e1:
return "做多首仓止盈须高于当前持仓均价"
else:
if e1 > 0 and tp >= e1:
return "做空首仓止盈须低于当前持仓均价"
if mode == MARKET_MODE:
if add_price is None or float(add_price) <= 0:
return "市价加仓需要有效参考价"
entry_add = float(add_price)
elif mode in FIB_MODES:
if fib_upper is None or fib_lower is None:
return "斐波须填写上沿 H 与下沿 L"
entry_add, err = fib_limit_entry(direction, float(fib_upper), float(fib_lower), mode)
if err:
return err
if entry_add is None or entry_add <= 0:
return "无法计算斐波限价"
elif mode == BREAKOUT_MODE:
if breakthrough_price is None:
return "突破加仓须填写突破价"
try:
bp = float(breakthrough_price)
except (TypeError, ValueError):
return "突破价格式错误"
if bp <= 0:
return "突破价须大于0"
entry_add = bp
if direction == "long":
if sl >= bp:
return "做多止损须低于突破价"
if mark_price is not None and float(mark_price) >= bp:
return "做多当前价须低于突破价等待向上突破"
else:
if sl <= bp:
return "做空止损须高于突破价"
if mark_price is not None and float(mark_price) <= bp:
return "做空当前价须高于突破价等待向下跌破"
else:
return "加仓方式无效"
if mode != BREAKOUT_MODE:
entry_add = float(entry_add) # type: ignore[arg-type]
if direction == "long":
if sl >= entry_add:
return "做多新止损须低于加仓价"
else:
if sl <= entry_add:
return "做空新止损须高于加仓价"
return None
def preview_roll(
*,
direction: str,
symbol: str,
qty_existing: float,
entry_existing: float,
initial_take_profit: float,
add_mode: str,
new_stop_loss: Optional[float] = None,
risk_percent: float,
capital_base_usdt: float,
add_price: Optional[float] = None,
fib_upper: Optional[float] = None,
fib_lower: Optional[float] = None,
breakthrough_price: Optional[float] = None,
legs_done: int = 0,
contract_size: float = 1.0,
) -> Tuple[Optional[dict[str, Any]], Optional[str]]:
direction = (direction or "long").strip().lower()
if legs_done >= max_roll_legs(direction):
return None, f"{'做多' if direction == 'long' else '做空'}滚仓已达 {max_roll_legs(direction)} 次上限"
mode = (add_mode or MARKET_MODE).strip().lower()
if new_stop_loss is None:
return None, "请填写新止损价"
try:
sl = float(new_stop_loss)
except (TypeError, ValueError):
return None, "止损价格式错误"
if sl <= 0:
return None, "止损须大于0"
geom_err = validate_roll_geometry(
direction,
mode,
new_stop_loss=sl,
add_price=add_price,
fib_upper=fib_upper,
fib_lower=fib_lower,
breakthrough_price=breakthrough_price,
entry_existing=entry_existing,
initial_take_profit=initial_take_profit,
mark_price=add_price if mode == BREAKOUT_MODE else add_price,
)
if geom_err:
return None, geom_err
if mode == MARKET_MODE:
entry_add = float(add_price) # validated
elif mode in FIB_MODES:
entry_add, _ = fib_limit_entry(direction, float(fib_upper), float(fib_lower), mode)
entry_add = float(entry_add or 0)
else:
entry_add = float(breakthrough_price or 0)
risk_budget = calc_risk_budget_usdt(capital_base_usdt, risk_percent)
q2_raw, err = solve_add_amount_for_total_risk(
direction,
qty_existing,
entry_existing,
entry_add,
sl,
risk_budget,
contract_size,
)
if err:
return None, err
q2 = float(q2_raw)
new_qty = qty_existing + q2
new_avg = avg_entry_after_add(qty_existing, entry_existing, q2, entry_add)
cs = float(contract_size or 1.0)
loss_sl = loss_at_stop_usdt(direction, new_avg, new_qty, sl, cs)
reward_tp = reward_at_tp_usdt(direction, new_avg, initial_take_profit, new_qty, cs)
return {
"symbol": symbol,
"direction": direction,
"add_mode": mode,
"add_mode_label": mode_label(mode),
"add_price": round(entry_add, 10),
"new_stop_loss": round(sl, 10),
"breakthrough_price": float(breakthrough_price) if breakthrough_price not in (None, "") else None,
"initial_take_profit": float(initial_take_profit),
"risk_percent": float(risk_percent),
"risk_budget_usdt": round(risk_budget, 4),
"add_amount_raw": q2,
"qty_existing": float(qty_existing),
"entry_existing": float(entry_existing),
"qty_after": new_qty,
"avg_entry_after": round(new_avg, 10),
"loss_at_sl_usdt": round(loss_sl, 4),
"reward_at_tp_usdt": round(reward_tp, 4),
"legs_done": int(legs_done),
"leg_index_next": int(legs_done) + 1,
"fib_upper": fib_upper,
"fib_lower": fib_lower,
"contract_size": cs,
}, None
"""顺势加仓(滚仓):纯计算.人工触发;止盈锁定首仓;程序监控触价市价成交."""
from __future__ import annotations
from typing import Any, Optional, Tuple
from lib.key_monitor.fib_key_monitor_lib import calc_fib_plan, fib_invalidate_by_mark
ROLL_MAX_LEGS_LONG = 3
ROLL_MAX_LEGS_SHORT = 3
MARKET_MODE = "market"
FIB_MODES = frozenset({"fib_618", "fib_786"})
BREAKOUT_MODE = "breakout"
MODE_LABELS = {
MARKET_MODE: "市价加仓",
"fib_618": "斐波0.618",
"fib_786": "斐波0.786",
BREAKOUT_MODE: "突破加仓",
}
def fib_ratio_from_mode(mode: str) -> Optional[float]:
m = (mode or "").strip().lower()
if m in ("fib_618", "618", "0.618"):
return 0.618
if m in ("fib_786", "786", "0.786"):
return 0.786
return None
def mode_label(mode: str) -> str:
m = (mode or MARKET_MODE).strip().lower()
return MODE_LABELS.get(m, m)
def fib_limit_entry(direction: str, upper: float, lower: float, mode: str) -> Tuple[Optional[float], Optional[str]]:
"""H/L 仅用于计算限价加仓价;多:下沿=止损侧;空:上沿=止损侧."""
ratio = fib_ratio_from_mode(mode)
if ratio is None:
return None, "斐波档位无效"
h, l = float(upper), float(lower)
if h <= l:
return None, "上沿须大于下沿"
direction = (direction or "long").strip().lower()
if direction == "short":
plan = calc_fib_plan("short", h, l, ratio)
else:
plan = calc_fib_plan("long", h, l, ratio)
if not plan:
return None, "无法计算斐波限价"
entry, _sl, _tp = plan
return float(entry), None
def max_roll_legs(direction: str) -> int:
return ROLL_MAX_LEGS_LONG if (direction or "long").strip().lower() == "long" else ROLL_MAX_LEGS_SHORT
def avg_entry_after_add(
qty_existing: float,
entry_existing: float,
add_qty: float,
add_price: float,
) -> float:
q1 = float(qty_existing)
e1 = float(entry_existing)
q2 = float(add_qty)
e2 = float(add_price)
total = q1 + q2
if total <= 0:
return 0.0
return (q1 * e1 + q2 * e2) / total
def calc_risk_budget_usdt(capital_base_usdt: float, risk_percent: float) -> float:
return float(capital_base_usdt) * (float(risk_percent) / 100.0)
def solve_add_amount_for_total_risk(
direction: str,
qty_existing: float,
entry_existing: float,
add_price: float,
new_stop: float,
risk_budget_usdt: float,
contract_size: float = 1.0,
) -> Tuple[Optional[float], Optional[str]]:
"""
合并持仓打到 new_stop 时总亏损 ≈ risk_budget(方案 C).
long: (avg - SL) * (Q1+Q2) * cs = B => Q2 = (B/cs - Q1*(E1-SL)) / (E2-SL)
short: (SL - avg) * (Q1+Q2) * cs = B => Q2 = (B/cs - Q1*(SL-E1)) / (SL-E2)
"""
try:
q1 = float(qty_existing)
e1 = float(entry_existing)
e2 = float(add_price)
sl = float(new_stop)
b = float(risk_budget_usdt)
cs = float(contract_size) if contract_size else 1.0
except (TypeError, ValueError):
return None, "参数格式错误"
if q1 <= 0 or e1 <= 0 or e2 <= 0 or b <= 0 or cs <= 0:
return None, "持仓或风险预算无效"
direction = (direction or "long").strip().lower()
if direction == "short":
denom = sl - e2
numer = b / cs - q1 * (sl - e1)
if denom <= 0:
return None, "做空:新止损须高于加仓价"
else:
denom = e2 - sl
numer = b / cs - q1 * (e1 - sl)
if denom <= 0:
return None, "做多:新止损须低于加仓价"
q2 = numer / denom
if q2 <= 0:
return None, "按当前新止损与风险预算,无需加仓或无法再加(已满足风险上限)"
return q2, None
def loss_at_stop_usdt(
direction: str,
avg: float,
qty: float,
stop: float,
contract_size: float = 1.0,
) -> float:
cs = float(contract_size or 1.0)
direction = (direction or "long").strip().lower()
if direction == "short":
return (float(stop) - float(avg)) * float(qty) * cs
return (float(avg) - float(stop)) * float(qty) * cs
def reward_at_tp_usdt(
direction: str,
avg: float,
take_profit: float,
qty: float,
contract_size: float = 1.0,
) -> float:
cs = float(contract_size or 1.0)
direction = (direction or "long").strip().lower()
if direction == "short":
return (float(avg) - float(take_profit)) * float(qty) * cs
return (float(take_profit) - float(avg)) * float(qty) * cs
def roll_fib_trigger_crossed(
direction: str,
prev_mark: Optional[float],
mark: float,
limit_price: float,
) -> bool:
"""斐波:多=向下穿越限价;空=向上穿越限价."""
try:
m = float(mark)
lv = float(limit_price)
pm = float(prev_mark) if prev_mark is not None else None
except (TypeError, ValueError):
return False
direction = (direction or "long").strip().lower()
if direction == "long":
if pm is None:
return m <= lv
return pm > lv and m <= lv
if pm is None:
return m >= lv
return pm < lv and m >= lv
def roll_breakout_trigger_crossed(
direction: str,
prev_mark: Optional[float],
mark: float,
breakthrough_price: float,
) -> bool:
"""突破:多=mark 在突破价之上;空=mark 在突破价之下.
提交时已校验 mark 在逆势侧(多低于突破价,空高于突破价),触价侧到达即成交.
不再要求单 tick 内穿越,避免 mark 已破位但 last_mark 也落在突破价另一侧时永久漏触发.
"""
try:
m = float(mark)
bp = float(breakthrough_price)
except (TypeError, ValueError):
return False
direction = (direction or "long").strip().lower()
if direction == "long":
return m > bp
return m < bp
def roll_fib_invalidate(direction: str, mark: float, upper: float, lower: float) -> bool:
"""斐波 pending 失效:止盈侧突破(多 mark>=H;空 mark<=L)."""
return fib_invalidate_by_mark(direction, mark, upper, lower)
def roll_breakout_invalidate(direction: str, mark: float, stop_loss: float) -> bool:
"""突破 pending 失效:未到突破价先触达止损侧(多 mark<=S;空 mark>=S)."""
try:
m = float(mark)
sl = float(stop_loss)
except (TypeError, ValueError):
return False
direction = (direction or "long").strip().lower()
if direction == "long":
return m <= sl
return m >= sl
def validate_roll_geometry(
direction: str,
add_mode: str,
*,
new_stop_loss: float,
add_price: Optional[float] = None,
fib_upper: Optional[float] = None,
fib_lower: Optional[float] = None,
breakthrough_price: Optional[float] = None,
entry_existing: float = 0.0,
initial_take_profit: float = 0.0,
mark_price: Optional[float] = None,
) -> Optional[str]:
direction = (direction or "long").strip().lower()
mode = (add_mode or MARKET_MODE).strip().lower()
try:
sl = float(new_stop_loss)
tp = float(initial_take_profit)
e1 = float(entry_existing or 0)
except (TypeError, ValueError):
return "止损/止盈格式错误"
if sl <= 0 or tp <= 0:
return "止损与首仓止盈须大于0"
if direction == "long":
if e1 > 0 and tp <= e1:
return "做多:首仓止盈须高于当前持仓均价"
else:
if e1 > 0 and tp >= e1:
return "做空:首仓止盈须低于当前持仓均价"
if mode == MARKET_MODE:
if add_price is None or float(add_price) <= 0:
return "市价加仓需要有效参考价"
entry_add = float(add_price)
elif mode in FIB_MODES:
if fib_upper is None or fib_lower is None:
return "斐波须填写上沿 H 与下沿 L"
entry_add, err = fib_limit_entry(direction, float(fib_upper), float(fib_lower), mode)
if err:
return err
if entry_add is None or entry_add <= 0:
return "无法计算斐波限价"
elif mode == BREAKOUT_MODE:
if breakthrough_price is None:
return "突破加仓须填写突破价"
try:
bp = float(breakthrough_price)
except (TypeError, ValueError):
return "突破价格式错误"
if bp <= 0:
return "突破价须大于0"
entry_add = bp
if direction == "long":
if sl >= bp:
return "做多:止损须低于突破价"
if mark_price is not None and float(mark_price) >= bp:
return "做多:当前价须低于突破价(等待向上突破)"
else:
if sl <= bp:
return "做空:止损须高于突破价"
if mark_price is not None and float(mark_price) <= bp:
return "做空:当前价须高于突破价(等待向下跌破)"
else:
return "加仓方式无效"
if mode != BREAKOUT_MODE:
entry_add = float(entry_add) # type: ignore[arg-type]
if direction == "long":
if sl >= entry_add:
return "做多:新止损须低于加仓价"
else:
if sl <= entry_add:
return "做空:新止损须高于加仓价"
return None
def preview_roll(
*,
direction: str,
symbol: str,
qty_existing: float,
entry_existing: float,
initial_take_profit: float,
add_mode: str,
new_stop_loss: Optional[float] = None,
risk_percent: float,
capital_base_usdt: float,
add_price: Optional[float] = None,
fib_upper: Optional[float] = None,
fib_lower: Optional[float] = None,
breakthrough_price: Optional[float] = None,
legs_done: int = 0,
contract_size: float = 1.0,
) -> Tuple[Optional[dict[str, Any]], Optional[str]]:
direction = (direction or "long").strip().lower()
if legs_done >= max_roll_legs(direction):
return None, f"{'做多' if direction == 'long' else '做空'}滚仓已达 {max_roll_legs(direction)} 次上限"
mode = (add_mode or MARKET_MODE).strip().lower()
if new_stop_loss is None:
return None, "请填写新止损价"
try:
sl = float(new_stop_loss)
except (TypeError, ValueError):
return None, "止损价格式错误"
if sl <= 0:
return None, "止损须大于0"
geom_err = validate_roll_geometry(
direction,
mode,
new_stop_loss=sl,
add_price=add_price,
fib_upper=fib_upper,
fib_lower=fib_lower,
breakthrough_price=breakthrough_price,
entry_existing=entry_existing,
initial_take_profit=initial_take_profit,
mark_price=add_price if mode == BREAKOUT_MODE else add_price,
)
if geom_err:
return None, geom_err
if mode == MARKET_MODE:
entry_add = float(add_price) # validated
elif mode in FIB_MODES:
entry_add, _ = fib_limit_entry(direction, float(fib_upper), float(fib_lower), mode)
entry_add = float(entry_add or 0)
else:
entry_add = float(breakthrough_price or 0)
risk_budget = calc_risk_budget_usdt(capital_base_usdt, risk_percent)
q2_raw, err = solve_add_amount_for_total_risk(
direction,
qty_existing,
entry_existing,
entry_add,
sl,
risk_budget,
contract_size,
)
if err:
return None, err
q2 = float(q2_raw)
new_qty = qty_existing + q2
new_avg = avg_entry_after_add(qty_existing, entry_existing, q2, entry_add)
cs = float(contract_size or 1.0)
loss_sl = loss_at_stop_usdt(direction, new_avg, new_qty, sl, cs)
reward_tp = reward_at_tp_usdt(direction, new_avg, initial_take_profit, new_qty, cs)
return {
"symbol": symbol,
"direction": direction,
"add_mode": mode,
"add_mode_label": mode_label(mode),
"add_price": round(entry_add, 10),
"new_stop_loss": round(sl, 10),
"breakthrough_price": float(breakthrough_price) if breakthrough_price not in (None, "") else None,
"initial_take_profit": float(initial_take_profit),
"risk_percent": float(risk_percent),
"risk_budget_usdt": round(risk_budget, 4),
"add_amount_raw": q2,
"qty_existing": float(qty_existing),
"entry_existing": float(entry_existing),
"qty_after": new_qty,
"avg_entry_after": round(new_avg, 10),
"loss_at_sl_usdt": round(loss_sl, 4),
"reward_at_tp_usdt": round(reward_tp, 4),
"legs_done": int(legs_done),
"leg_index_next": int(legs_done) + 1,
"fib_upper": fib_upper,
"fib_lower": fib_lower,
"contract_size": cs,
}, None
File diff suppressed because it is too large Load Diff
+8 -8
View File
@@ -1,4 +1,4 @@
"""顺势加仓 UI滚仓腿合并均价与止盈盈利展示实例页 + 中控)。"""
"""顺势加仓 UI:滚仓腿合并均价与止盈盈利展示(实例页 + 中控)."""
from __future__ import annotations
from typing import Any, Callable, Optional
@@ -16,7 +16,7 @@ def reward_at_tp_usdt(
*,
contract_size: float = 1.0,
) -> Optional[float]:
"""与 strategy_roll_lib.preview_roll 一致线性合约 U 本位盈利"""
"""与 strategy_roll_lib.preview_roll 一致:线性合约 U 本位盈利."""
try:
avg = float(avg_entry)
tp = float(take_profit)
@@ -57,7 +57,7 @@ def infer_initial_position(
*,
monitor: dict | None = None,
) -> tuple[Optional[float], Optional[float]]:
"""由当前持仓与各腿成交价反推首仓张数/均价"""
"""由当前持仓与各腿成交价反推首仓张数/均价."""
try:
qty_live = float(qty_live)
entry_live = float(entry_live)
@@ -106,7 +106,7 @@ def compute_roll_chain_metrics(
contract_size: float = 1.0,
) -> tuple[dict[Any, dict], dict]:
"""
返回 (leg_metrics_by_id, group_metrics)
返回 (leg_metrics_by_id, group_metrics).
leg_metrics: leg id -> {avg_entry_after, reward_at_tp_usdt}
group_metrics: 最后一腿后的 {avg_entry, reward_at_tp_usdt}
"""
@@ -187,7 +187,7 @@ def _row_to_dict(row) -> dict:
def _resolve_roll_live(cfg: dict, group: dict, monitor: dict | None) -> tuple[Optional[float], Optional[float], float]:
"""读取交易所持仓张数均价contract_size"""
"""读取交易所持仓张数,均价,contract_size."""
m = cfg.get("app_module")
ex_sym = group.get("exchange_symbol")
sym = group.get("symbol") or ""
@@ -244,7 +244,7 @@ def _resolve_roll_live(cfg: dict, group: dict, monitor: dict | None) -> tuple[Op
def enrich_roll_page_data(conn, page_data: dict, cfg: dict | None) -> dict:
"""为 roll_groups / roll_legs 附加 avg_entryreward_at_tp 展示字段"""
"""为 roll_groups / roll_legs 附加 avg_entry,reward_at_tp 展示字段."""
if not isinstance(page_data, dict) or not cfg:
return page_data
groups = list(page_data.get("roll_groups") or [])
@@ -313,7 +313,7 @@ def enrich_roll_page_data(conn, page_data: dict, cfg: dict | None) -> dict:
def enrich_roll_groups_for_hub(rolls: list[dict], conn, cfg: dict | None) -> list[dict]:
"""中控 monitor API每组附带当前均价止盈盈利与最近滚仓腿"""
"""中控 monitor API:每组附带当前均价,止盈盈利与最近滚仓腿."""
if not rolls or not cfg:
return rolls
out = []
@@ -396,7 +396,7 @@ def enrich_roll_groups_for_hub(rolls: list[dict], conn, cfg: dict | None) -> lis
def patch_roll_hub_enrich(app: Flask, cfg: dict) -> None:
"""hub_bridge install 后/api/hub/monitor 的 rolls 附带均价/止盈盈利"""
"""hub_bridge install 后:/api/hub/monitor 的 rolls 附带均价/止盈盈利."""
ctx = dict(app.config.get("HUB_CTX") or {})
prev: Callable | None = ctx.get("enrich_monitor")
File diff suppressed because it is too large Load Diff
+9 -9
View File
@@ -1,4 +1,4 @@
"""策略交易写入 trade_records 时的类型与复盘开仓类型标注"""
"""策略交易写入 trade_records 时的类型与复盘开仓类型标注."""
from __future__ import annotations
from typing import Optional
@@ -14,20 +14,20 @@ STRATEGY_ENTRY_REASON_OPTIONS = (
ENTRY_REASON_ROLL,
)
# 趋势回调保本移交下单监控order_monitors.key_signal_type / 平仓备注
# 趋势回调保本移交下单监控:order_monitors.key_signal_type / 平仓备注
TREND_HANDOFF_KEY_SIGNAL = ENTRY_REASON_TREND_PULLBACK
TREND_HANDOFF_TRADE_NOTE = "趋势回调计划"
def handoff_trade_miss_reason(miss_reason, row) -> Optional[str]:
"""趋势保本移交的监控单平仓交易记录备注带来源"""
"""趋势保本移交的监控单平仓:交易记录备注带来源."""
if trend_plan_id_from_monitor_row(row) is None:
return miss_reason
base = (miss_reason or "").strip()
if TREND_HANDOFF_TRADE_NOTE in base:
return base or TREND_HANDOFF_TRADE_NOTE
if base:
return f"{TREND_HANDOFF_TRADE_NOTE}{base}"
return f"{TREND_HANDOFF_TRADE_NOTE};{base}"
return TREND_HANDOFF_TRADE_NOTE
@@ -94,7 +94,7 @@ def _row_key_signal_type(row) -> str:
def order_monitor_source_type(row, *, default_manual: str = "下单监控") -> str:
"""展示/平仓记录趋势保本移交单来源为「趋势回调」非「下单监控」"""
"""展示/平仓记录:趋势保本移交单来源为「趋势回调」,非「下单监控」."""
if trend_plan_id_from_monitor_row(row) is not None:
return MONITOR_TYPE_TREND_PULLBACK
mt = _row_monitor_type(row, default_manual)
@@ -112,14 +112,14 @@ def order_monitor_source_type(row, *, default_manual: str = "下单监控") -> s
def apply_order_monitor_source_labels(item: dict, *, default_manual: str = "下单监控") -> dict:
"""实例页 / 中控 API统一修正 order_monitors 展示用 monitor_type"""
"""实例页 / 中控 API:统一修正 order_monitors 展示用 monitor_type."""
out = dict(item or {})
out["monitor_type"] = order_monitor_source_type(out, default_manual=default_manual)
return out
def trade_record_monitor_type(conn, order_row, *, default_manual: str = "下单监控") -> str:
"""平仓写入 trade_records 时曾顺势加仓则标「顺势加仓」否则沿用监控单来源类型"""
"""平仓写入 trade_records 时:曾顺势加仓则标「顺势加仓」,否则沿用监控单来源类型."""
oid = None
try:
keys = order_row.keys() if hasattr(order_row, "keys") else []
@@ -142,12 +142,12 @@ def entry_reason_for_monitor_type(monitor_type: str | None) -> str:
def order_monitor_excluded_from_position_limit(conn, row) -> bool:
"""趋势回调不计入 MAX_ACTIVE_POSITIONS顺势加仓在已有持仓上操作单独放行"""
"""趋势回调不计入 MAX_ACTIVE_POSITIONS;顺势加仓在已有持仓上操作,单独放行."""
return order_monitor_source_type(row) == MONITOR_TYPE_TREND_PULLBACK
def count_position_limit_active_monitors(conn) -> int:
"""计入仓位上限冻结的活跃监控数不含趋势回调顺势加仓)。"""
"""计入仓位上限冻结的活跃监控数(不含趋势回调,顺势加仓)."""
try:
rows = conn.execute("SELECT * FROM order_monitors WHERE status='active'").fetchall()
except Exception:
+2 -2
View File
@@ -1,4 +1,4 @@
"""趋势回调各交易所止损刷新市价加/平仓通过 app 模块能力探测)。"""
"""趋势回调:各交易所止损刷新,市价加/平仓(通过 app 模块能力探测)."""
from __future__ import annotations
import time
@@ -76,7 +76,7 @@ def trend_market_close(cfg: dict, exchange_symbol: str, direction: str, pos_qty:
def trend_replace_tpsl(cfg: dict, order_row: dict, stop_loss: float, take_profit: float) -> None:
"""趋势保本移交先撤条件单再挂保本止损 + 计划止盈与下单监控一致)。"""
"""趋势保本移交:先撤条件单再挂保本止损 + 计划止盈(与下单监控一致)."""
m = _m(cfg)
fn = getattr(m, "replace_active_monitor_tpsl_on_exchange", None)
if not callable(fn):
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+143 -143
View File
@@ -1,143 +1,143 @@
"""策略交易页主站 index.html 所需数据顺势加仓等)。"""
from __future__ import annotations
from typing import Any, Callable, Optional
from lib.strategy.strategy_db import init_strategy_tables
from lib.strategy.strategy_roll_monitor_lib import roll_leg_status_label
def _row_to_dict(row) -> dict:
if row is None:
return {}
try:
return dict(row)
except Exception:
return {}
def count_active_trend_plans(conn, count_fn: Optional[Callable] = None) -> int:
if callable(count_fn):
return int(count_fn(conn) or 0)
try:
return int(
conn.execute(
"SELECT COUNT(*) FROM trend_pullback_plans WHERE status='active'"
).fetchone()[0]
)
except Exception:
return 0
def fetch_roll_page_data(
conn,
*,
default_risk_percent: float = 2.0,
count_active_trends: Optional[Callable] = None,
roll_cfg: dict | None = None,
) -> dict[str, Any]:
init_strategy_tables(conn)
monitors = []
for row in conn.execute(
"SELECT * FROM order_monitors WHERE status='active' ORDER BY id DESC"
).fetchall():
monitors.append(_row_to_dict(row))
roll_groups = []
for row in conn.execute(
"""SELECT g.* FROM roll_groups g
INNER JOIN order_monitors m ON m.id = g.order_monitor_id AND m.status='active'
WHERE g.status='active'
ORDER BY g.id DESC"""
).fetchall():
roll_groups.append(_row_to_dict(row))
active_gids = {int(g["id"]) for g in roll_groups if g.get("id") is not None}
roll_legs = []
for row in conn.execute(
"SELECT * FROM roll_legs ORDER BY id DESC LIMIT 80"
).fetchall():
leg = _row_to_dict(row)
gid = leg.get("roll_group_id")
if gid is not None and int(gid) not in active_gids:
continue
leg["status_label"] = roll_leg_status_label(leg.get("status"))
roll_legs.append(leg)
roll_legs = roll_legs[:50]
out = {
"roll_monitors": monitors,
"roll_groups": roll_groups,
"roll_legs": roll_legs,
"roll_trend_active": count_active_trend_plans(conn, count_active_trends),
"default_risk_percent": default_risk_percent,
}
if roll_cfg:
from lib.strategy.strategy_roll_ui_lib import enrich_roll_page_data
enrich_roll_page_data(conn, out, roll_cfg)
return out
DEFAULT_TREND_DISABLED_NOTE = (
"趋势回调预览自动补仓程序止盈须在本实例 .env 设置 "
"`LIVE_TRADING_ENABLED=true` 并重启对应 PM2 进程如 crypto_gate / crypto_okx / crypto_binance)。"
)
def strategy_render_extras(
conn,
page: str,
*,
default_risk_percent: float = 2.0,
count_active_trends: Optional[Callable] = None,
trend_disabled_note: str = "",
request_obj=None,
trend_cfg: Optional[dict] = None,
) -> dict[str, Any]:
"""render_main_page 策略相关页变量含策略交易记录)。"""
if page == "strategy_records":
from lib.strategy.strategy_records_register import load_strategy_records_page
return load_strategy_records_page(conn)
return strategy_page_template_vars(
conn,
page,
default_risk_percent=default_risk_percent,
count_active_trends=count_active_trends,
trend_disabled_note=trend_disabled_note,
request_obj=request_obj,
trend_cfg=trend_cfg,
)
def strategy_page_template_vars(
conn,
page: str,
*,
default_risk_percent: float = 2.0,
count_active_trends: Optional[Callable] = None,
trend_disabled_note: str = "",
request_obj=None,
trend_cfg: Optional[dict] = None,
) -> dict[str, Any]:
"""render_main_page 在 conn.close() 前合并进 render_template 的变量"""
if page not in ("strategy", "strategy_trend", "strategy_roll"):
return {}
roll_cfg = None
try:
from flask import current_app
roll_cfg = (current_app.extensions or {}).get("strategy_roll_cfg")
except Exception:
roll_cfg = None
out = fetch_roll_page_data(
conn,
default_risk_percent=default_risk_percent,
count_active_trends=count_active_trends,
roll_cfg=roll_cfg if isinstance(roll_cfg, dict) else None,
)
if trend_cfg and request_obj is not None:
from lib.strategy.strategy_trend_register import load_trend_page_context
out.update(load_trend_page_context(conn, request_obj, trend_cfg))
elif page == "strategy_trend":
out["trend_disabled_note"] = trend_disabled_note or DEFAULT_TREND_DISABLED_NOTE
return out
"""策略交易页:主站 index.html 所需数据(顺势加仓等)."""
from __future__ import annotations
from typing import Any, Callable, Optional
from lib.strategy.strategy_db import init_strategy_tables
from lib.strategy.strategy_roll_monitor_lib import roll_leg_status_label
def _row_to_dict(row) -> dict:
if row is None:
return {}
try:
return dict(row)
except Exception:
return {}
def count_active_trend_plans(conn, count_fn: Optional[Callable] = None) -> int:
if callable(count_fn):
return int(count_fn(conn) or 0)
try:
return int(
conn.execute(
"SELECT COUNT(*) FROM trend_pullback_plans WHERE status='active'"
).fetchone()[0]
)
except Exception:
return 0
def fetch_roll_page_data(
conn,
*,
default_risk_percent: float = 2.0,
count_active_trends: Optional[Callable] = None,
roll_cfg: dict | None = None,
) -> dict[str, Any]:
init_strategy_tables(conn)
monitors = []
for row in conn.execute(
"SELECT * FROM order_monitors WHERE status='active' ORDER BY id DESC"
).fetchall():
monitors.append(_row_to_dict(row))
roll_groups = []
for row in conn.execute(
"""SELECT g.* FROM roll_groups g
INNER JOIN order_monitors m ON m.id = g.order_monitor_id AND m.status='active'
WHERE g.status='active'
ORDER BY g.id DESC"""
).fetchall():
roll_groups.append(_row_to_dict(row))
active_gids = {int(g["id"]) for g in roll_groups if g.get("id") is not None}
roll_legs = []
for row in conn.execute(
"SELECT * FROM roll_legs ORDER BY id DESC LIMIT 80"
).fetchall():
leg = _row_to_dict(row)
gid = leg.get("roll_group_id")
if gid is not None and int(gid) not in active_gids:
continue
leg["status_label"] = roll_leg_status_label(leg.get("status"))
roll_legs.append(leg)
roll_legs = roll_legs[:50]
out = {
"roll_monitors": monitors,
"roll_groups": roll_groups,
"roll_legs": roll_legs,
"roll_trend_active": count_active_trend_plans(conn, count_active_trends),
"default_risk_percent": default_risk_percent,
}
if roll_cfg:
from lib.strategy.strategy_roll_ui_lib import enrich_roll_page_data
enrich_roll_page_data(conn, out, roll_cfg)
return out
DEFAULT_TREND_DISABLED_NOTE = (
"趋势回调(预览,自动补仓,程序止盈)须在本实例 .env 设置 "
"`LIVE_TRADING_ENABLED=true` 并重启对应 PM2 进程(如 crypto_gate / crypto_okx / crypto_binance)."
)
def strategy_render_extras(
conn,
page: str,
*,
default_risk_percent: float = 2.0,
count_active_trends: Optional[Callable] = None,
trend_disabled_note: str = "",
request_obj=None,
trend_cfg: Optional[dict] = None,
) -> dict[str, Any]:
"""render_main_page 策略相关页变量(含策略交易记录)."""
if page == "strategy_records":
from lib.strategy.strategy_records_register import load_strategy_records_page
return load_strategy_records_page(conn)
return strategy_page_template_vars(
conn,
page,
default_risk_percent=default_risk_percent,
count_active_trends=count_active_trends,
trend_disabled_note=trend_disabled_note,
request_obj=request_obj,
trend_cfg=trend_cfg,
)
def strategy_page_template_vars(
conn,
page: str,
*,
default_risk_percent: float = 2.0,
count_active_trends: Optional[Callable] = None,
trend_disabled_note: str = "",
request_obj=None,
trend_cfg: Optional[dict] = None,
) -> dict[str, Any]:
"""render_main_page 在 conn.close() 前合并进 render_template 的变量."""
if page not in ("strategy", "strategy_trend", "strategy_roll"):
return {}
roll_cfg = None
try:
from flask import current_app
roll_cfg = (current_app.extensions or {}).get("strategy_roll_cfg")
except Exception:
roll_cfg = None
out = fetch_roll_page_data(
conn,
default_risk_percent=default_risk_percent,
count_active_trends=count_active_trends,
roll_cfg=roll_cfg if isinstance(roll_cfg, dict) else None,
)
if trend_cfg and request_obj is not None:
from lib.strategy.strategy_trend_register import load_trend_page_context
out.update(load_trend_page_context(conn, request_obj, trend_cfg))
elif page == "strategy_trend":
out["trend_disabled_note"] = trend_disabled_note or DEFAULT_TREND_DISABLED_NOTE
return out
+192 -192
View File
@@ -1,192 +1,192 @@
"""策略计划趋势回调 / 滚仓开始与结束 — 企业微信推送三所共用)。"""
from __future__ import annotations
from typing import Any, Optional
from lib.common.wechat_notify_lib import wechat_direction_label
def _send(cfg: dict[str, Any], content: str) -> None:
fn = cfg.get("send_wechat")
if callable(fn):
try:
fn(content)
return
except Exception:
pass
m = cfg.get("app_module")
if m is not None:
sw = getattr(m, "send_wechat_msg", None)
if callable(sw):
try:
sw(content)
except Exception:
pass
def _account(cfg: dict[str, Any]) -> str:
fn = cfg.get("wechat_account_label")
if callable(fn):
try:
return str(fn()).strip() or _exchange(cfg)
except Exception:
pass
return _exchange(cfg)
def _exchange(cfg: dict[str, Any]) -> str:
return str(cfg.get("exchange_display") or "").strip() or "交易账户"
def _dir_text(cfg: dict[str, Any], direction: str) -> str:
fn = cfg.get("wechat_direction_text")
if callable(fn):
try:
return str(fn(direction))
except Exception:
pass
return wechat_direction_label(direction)
def _fmt_price(cfg: dict[str, Any], symbol: str, price: Any) -> str:
if price is None or price == "":
return ""
fn = cfg.get("format_price") or cfg.get("price_fmt")
if callable(fn):
try:
return str(fn(symbol, price))
except Exception:
pass
m = cfg.get("app_module")
pf = getattr(m, "format_price_for_symbol", None) if m else None
if callable(pf):
try:
return str(pf(symbol, price))
except Exception:
pass
try:
return str(round(float(price), 8))
except (TypeError, ValueError):
return str(price)
def _fmt_pnl(pnl: Any) -> str:
if pnl is None:
return ""
try:
v = float(pnl)
return f"{'+' if v > 0 else ''}{round(v, 2)} U"
except (TypeError, ValueError):
return str(pnl)
def notify_trend_plan_started(
cfg: dict[str, Any],
*,
plan_id: int,
symbol: str,
direction: str,
leverage: int,
stop_loss: float,
take_profit: float,
add_upper: float,
risk_percent: float,
dca_legs: int,
first_order_amount: float,
avg_entry: Optional[float] = None,
snapshot_usdt: Optional[float] = None,
) -> None:
sym = symbol or ""
lines = [
f"# 🚀 {sym} 趋势回调计划已开始",
f"**账户{_account(cfg)}**",
f"- 计划 ID**{plan_id}**",
f"- 方向{_dir_text(cfg, direction)}|杠杆 **{int(leverage or 1)}x**",
f"- 止损{_fmt_price(cfg, sym, stop_loss)}|止盈{_fmt_price(cfg, sym, take_profit)}",
f"- 补仓区{_fmt_price(cfg, sym, add_upper)}|补仓档 **{int(dca_legs or 0)}** 档",
f"- 风险**{risk_percent}%**|首仓张数**{first_order_amount}**",
]
if avg_entry is not None:
lines.append(f"- 首仓成交价{_fmt_price(cfg, sym, avg_entry)}")
if snapshot_usdt is not None:
try:
lines.append(f"- 启动时合约可用**{round(float(snapshot_usdt), 2)} U**")
except (TypeError, ValueError):
pass
lines.append("- 说明交易所已挂止损止盈由程序监控结束/保本将另行推送")
_send(cfg, "\n".join(lines))
def notify_trend_plan_ended(
cfg: dict[str, Any],
*,
plan_id: int,
symbol: str,
direction: str,
end_type: str,
result_label: Optional[str] = None,
exit_price: Optional[float] = None,
pnl_amount: Optional[float] = None,
extra: Optional[str] = None,
) -> None:
sym = symbol or ""
res = (result_label or end_type or "").strip()
lines = [
f"# 🏁 {sym} 趋势回调计划已结束",
f"**账户{_account(cfg)}**",
f"- 计划 ID**{plan_id}**",
f"- 方向{_dir_text(cfg, direction)}",
f"- 结束方式**{end_type}**",
f"- 结果**{res}**",
]
if exit_price is not None:
lines.append(f"- 离场参考价{_fmt_price(cfg, sym, exit_price)}")
if pnl_amount is not None:
lines.append(f"- 本单盈亏**{_fmt_pnl(pnl_amount)}**")
if extra:
lines.append(f"- {extra}")
_send(cfg, "\n".join(lines))
def notify_roll_group_started(
cfg: dict[str, Any],
*,
group_id: int,
symbol: str,
direction: str,
order_monitor_id: int,
initial_take_profit: Optional[float] = None,
initial_stop_loss: Optional[float] = None,
) -> None:
sym = symbol or ""
lines = [
f"# 🚀 {sym} 滚仓计划已开始",
f"**账户{_account(cfg)}**",
f"- 滚仓组 ID**{group_id}**|绑定下单监控 **#{order_monitor_id}**",
f"- 方向{_dir_text(cfg, direction)}",
f"- 首仓止盈锁定):{_fmt_price(cfg, sym, initial_take_profit)}",
f"- 当前止损{_fmt_price(cfg, sym, initial_stop_loss)}",
"- 说明顺势加仓为人工触发组结束无持仓/监控结案将另行推送",
]
_send(cfg, "\n".join(lines))
def notify_roll_group_ended(
cfg: dict[str, Any],
*,
group_id: int,
symbol: str,
direction: str,
reason: str,
leg_count: int = 0,
) -> None:
sym = symbol or ""
lines = [
f"# 🏁 {sym} 滚仓计划已结束",
f"**账户{_account(cfg)}**",
f"- 滚仓组 ID**{group_id}**",
f"- 方向{_dir_text(cfg, direction)}",
f"- 结束原因**{reason}**",
f"- 已完成滚仓腿数**{int(leg_count or 0)}**",
]
_send(cfg, "\n".join(lines))
"""策略计划(趋势回调 / 滚仓)开始与结束 — 企业微信推送(三所共用)."""
from __future__ import annotations
from typing import Any, Optional
from lib.common.wechat_notify_lib import wechat_direction_label
def _send(cfg: dict[str, Any], content: str) -> None:
fn = cfg.get("send_wechat")
if callable(fn):
try:
fn(content)
return
except Exception:
pass
m = cfg.get("app_module")
if m is not None:
sw = getattr(m, "send_wechat_msg", None)
if callable(sw):
try:
sw(content)
except Exception:
pass
def _account(cfg: dict[str, Any]) -> str:
fn = cfg.get("wechat_account_label")
if callable(fn):
try:
return str(fn()).strip() or _exchange(cfg)
except Exception:
pass
return _exchange(cfg)
def _exchange(cfg: dict[str, Any]) -> str:
return str(cfg.get("exchange_display") or "").strip() or "交易账户"
def _dir_text(cfg: dict[str, Any], direction: str) -> str:
fn = cfg.get("wechat_direction_text")
if callable(fn):
try:
return str(fn(direction))
except Exception:
pass
return wechat_direction_label(direction)
def _fmt_price(cfg: dict[str, Any], symbol: str, price: Any) -> str:
if price is None or price == "":
return ""
fn = cfg.get("format_price") or cfg.get("price_fmt")
if callable(fn):
try:
return str(fn(symbol, price))
except Exception:
pass
m = cfg.get("app_module")
pf = getattr(m, "format_price_for_symbol", None) if m else None
if callable(pf):
try:
return str(pf(symbol, price))
except Exception:
pass
try:
return str(round(float(price), 8))
except (TypeError, ValueError):
return str(price)
def _fmt_pnl(pnl: Any) -> str:
if pnl is None:
return ""
try:
v = float(pnl)
return f"{'+' if v > 0 else ''}{round(v, 2)} U"
except (TypeError, ValueError):
return str(pnl)
def notify_trend_plan_started(
cfg: dict[str, Any],
*,
plan_id: int,
symbol: str,
direction: str,
leverage: int,
stop_loss: float,
take_profit: float,
add_upper: float,
risk_percent: float,
dca_legs: int,
first_order_amount: float,
avg_entry: Optional[float] = None,
snapshot_usdt: Optional[float] = None,
) -> None:
sym = symbol or ""
lines = [
f"# 🚀 {sym} 趋势回调计划已开始",
f"**账户:{_account(cfg)}**",
f"- 计划 ID:**{plan_id}**",
f"- 方向:{_dir_text(cfg, direction)}|杠杆 **{int(leverage or 1)}x**",
f"- 止损:{_fmt_price(cfg, sym, stop_loss)}|止盈:{_fmt_price(cfg, sym, take_profit)}",
f"- 补仓区:{_fmt_price(cfg, sym, add_upper)}|补仓档 **{int(dca_legs or 0)}** 档",
f"- 风险:**{risk_percent}%**|首仓张数:**{first_order_amount}**",
]
if avg_entry is not None:
lines.append(f"- 首仓成交价:{_fmt_price(cfg, sym, avg_entry)}")
if snapshot_usdt is not None:
try:
lines.append(f"- 启动时合约可用:**{round(float(snapshot_usdt), 2)} U**")
except (TypeError, ValueError):
pass
lines.append("- 说明:交易所已挂止损;止盈由程序监控;结束/保本将另行推送")
_send(cfg, "\n".join(lines))
def notify_trend_plan_ended(
cfg: dict[str, Any],
*,
plan_id: int,
symbol: str,
direction: str,
end_type: str,
result_label: Optional[str] = None,
exit_price: Optional[float] = None,
pnl_amount: Optional[float] = None,
extra: Optional[str] = None,
) -> None:
sym = symbol or ""
res = (result_label or end_type or "").strip()
lines = [
f"# 🏁 {sym} 趋势回调计划已结束",
f"**账户:{_account(cfg)}**",
f"- 计划 ID:**{plan_id}**",
f"- 方向:{_dir_text(cfg, direction)}",
f"- 结束方式:**{end_type}**",
f"- 结果:**{res}**",
]
if exit_price is not None:
lines.append(f"- 离场参考价:{_fmt_price(cfg, sym, exit_price)}")
if pnl_amount is not None:
lines.append(f"- 本单盈亏:**{_fmt_pnl(pnl_amount)}**")
if extra:
lines.append(f"- {extra}")
_send(cfg, "\n".join(lines))
def notify_roll_group_started(
cfg: dict[str, Any],
*,
group_id: int,
symbol: str,
direction: str,
order_monitor_id: int,
initial_take_profit: Optional[float] = None,
initial_stop_loss: Optional[float] = None,
) -> None:
sym = symbol or ""
lines = [
f"# 🚀 {sym} 滚仓计划已开始",
f"**账户:{_account(cfg)}**",
f"- 滚仓组 ID:**{group_id}**|绑定下单监控 **#{order_monitor_id}**",
f"- 方向:{_dir_text(cfg, direction)}",
f"- 首仓止盈(锁定):{_fmt_price(cfg, sym, initial_take_profit)}",
f"- 当前止损:{_fmt_price(cfg, sym, initial_stop_loss)}",
"- 说明:顺势加仓为人工触发;组结束(无持仓/监控结案)将另行推送",
]
_send(cfg, "\n".join(lines))
def notify_roll_group_ended(
cfg: dict[str, Any],
*,
group_id: int,
symbol: str,
direction: str,
reason: str,
leg_count: int = 0,
) -> None:
sym = symbol or ""
lines = [
f"# 🏁 {sym} 滚仓计划已结束",
f"**账户:{_account(cfg)}**",
f"- 滚仓组 ID:**{group_id}**",
f"- 方向:{_dir_text(cfg, direction)}",
f"- 结束原因:**{reason}**",
f"- 已完成滚仓腿数:**{int(leg_count or 0)}**",
]
_send(cfg, "\n".join(lines))
@@ -1,10 +1,10 @@
<details class="tip-collapse gate-top-tips-collapse">
<summary class="tip-collapse-summary">
实时价格更新<span id="price-last-updated">--</span>北京时间 UTC+8
实时价格更新:<span id="price-last-updated">--</span>(北京时间 UTC+8)
<span class="tip-collapse-hint">· 划转规则</span>
</summary>
<div class="tip-collapse-body rule-tip gate-transfer-tip">
划转自动划转 {{ '开启' if auto_transfer_enabled else '关闭' }}每天<strong>北京时间 {{ auto_transfer_bj_hour }}:00</strong>起该整点小时内尝试账簿按 <strong>UTC 自然日</strong>去重将 {{ auto_transfer_to }} 调整至 {{ transfer_amount_fmt|default(funds_fmt(auto_transfer_amount)) }}U不足从 {{ auto_transfer_from }} 划入超出划回 {{ auto_transfer_from }}<strong>持仓中不划转</strong>并微信通知
划转:自动划转 {{ '开启' if auto_transfer_enabled else '关闭' }}(每天<strong>北京时间 {{ auto_transfer_bj_hour }}:00</strong>起该整点小时内尝试;账簿按 <strong>UTC 自然日</strong>去重;将 {{ auto_transfer_to }} 调整至 {{ transfer_amount_fmt|default(funds_fmt(auto_transfer_amount)) }}U:不足从 {{ auto_transfer_from }} 划入,超出划回 {{ auto_transfer_from }};<strong>持仓中不划转</strong>并微信通知)
</div>
</details>
<form action="/manual_transfer" method="post" class="form-row gate-transfer-form">
+33 -33
View File
@@ -1,33 +1,33 @@
{# 复盘表单首行按字段宽度比例开仓类型与离场触发同一行 #}
{% macro journal_form_fields(entry_reason_options) -%}
<div class="form-grid journal-form-row1">
<input type="datetime-local" name="open_datetime" class="journal-field-datetime" required>
<input type="datetime-local" name="close_datetime" class="journal-field-datetime" required>
<input name="coin" class="journal-field-coin" placeholder="BTC" required>
<input name="tf" class="journal-field-tf" placeholder="5m" required>
<input name="pnl" class="journal-field-num" placeholder="盈亏(U)" required>
<input name="expect_rr" class="journal-field-num" placeholder="预期RR">
<input name="real_rr" class="journal-field-num" placeholder="实际RR">
</div>
<div class="form-grid journal-form-row2">
<select name="entry_reason" id="journal-entry-reason" class="journal-field-entry-reason" required title="日内假破/结构突破/回调触价/突破触价趋势户反转/顺势/波段或策略项">
<option value="">开仓类型必选</option>
{% for er in entry_reason_options %}
<option value="{{ er }}">{{ er }}</option>
{% endfor %}
</select>
<select name="early_exit_trigger" required title="平仓如何触发">
<option value="">离场触发必选</option>
<option value="止盈">止盈</option>
<option value="保本止盈">保本止盈</option>
<option value="移动止盈">移动止盈</option>
<option value="时间平仓">时间平仓</option>
<option value="强制清仓">强制清仓</option>
<option value="手动平仓">手动平仓</option>
<option value="止损">止损</option>
<option value="其他">其他</option>
</select>
<input name="early_exit_note" id="early-exit-note" placeholder="离场补充仅手工平仓必填">
<select name="post_breakeven_stare"><option value="否">保本后盯盘:否</option><option value="是">保本后盯盘:是</option></select>
</div>
{%- endmacro %}
{# 复盘表单:首行按字段宽度比例;开仓类型与离场触发同一行 #}
{% macro journal_form_fields(entry_reason_options) -%}
<div class="form-grid journal-form-row1">
<input type="datetime-local" name="open_datetime" class="journal-field-datetime" required>
<input type="datetime-local" name="close_datetime" class="journal-field-datetime" required>
<input name="coin" class="journal-field-coin" placeholder="BTC" required>
<input name="tf" class="journal-field-tf" placeholder="5m" required>
<input name="pnl" class="journal-field-num" placeholder="盈亏(U)" required>
<input name="expect_rr" class="journal-field-num" placeholder="预期RR">
<input name="real_rr" class="journal-field-num" placeholder="实际RR">
</div>
<div class="form-grid journal-form-row2">
<select name="entry_reason" id="journal-entry-reason" class="journal-field-entry-reason" required title="日内:假破/结构突破/回调触价/突破触价;趋势户:反转/顺势/波段或策略项">
<option value="">开仓类型(必选)</option>
{% for er in entry_reason_options %}
<option value="{{ er }}">{{ er }}</option>
{% endfor %}
</select>
<select name="early_exit_trigger" required title="平仓如何触发">
<option value="">离场触发(必选)</option>
<option value="止盈">止盈</option>
<option value="保本止盈">保本止盈</option>
<option value="移动止盈">移动止盈</option>
<option value="时间平仓">时间平仓</option>
<option value="强制清仓">强制清仓</option>
<option value="手动平仓">手动平仓</option>
<option value="止损">止损</option>
<option value="其他">其他</option>
</select>
<input name="early_exit_note" id="early-exit-note" placeholder="离场补充(仅手工平仓必填)">
<select name="post_breakeven_stare"><option value="否">保本后盯盘:否</option><option value="是">保本后盯盘:是</option></select>
</div>
{%- endmacro %}
@@ -1,20 +1,20 @@
{# 复盘四周期截图槽位须加载 journal_upload_slots.js #}
{% macro journal_upload_slots() -%}
<input type="hidden" name="journal_draft_id" id="journal-draft-id" value="">
<div class="journal-upload-slots" id="journal-upload-slots">
{% for tf in ['5m', '15m', '1h', '4h'] %}
<div class="journal-upload-row" data-tf="{{ tf }}">
<span class="journal-upload-slot-label">{{ tf }}</span>
<input
type="file"
accept="image/*"
class="journal-upload-slot-input"
data-tf="{{ tf }}"
>
<input type="hidden" name="uploaded_screenshot_{{ tf }}" class="journal-upload-hidden-file" data-tf="{{ tf }}" value="">
<span class="journal-upload-status" data-tf="{{ tf }}" aria-live="polite"></span>
</div>
{% endfor %}
</div>
<p class="sub journal-upload-hint">可只传部分周期选文件后即时上传保存后详情页四宫格查看</p>
{%- endmacro %}
{# 复盘四周期截图槽位(须加载 journal_upload_slots.js) #}
{% macro journal_upload_slots() -%}
<input type="hidden" name="journal_draft_id" id="journal-draft-id" value="">
<div class="journal-upload-slots" id="journal-upload-slots">
{% for tf in ['5m', '15m', '1h', '4h'] %}
<div class="journal-upload-row" data-tf="{{ tf }}">
<span class="journal-upload-slot-label">{{ tf }}</span>
<input
type="file"
accept="image/*"
class="journal-upload-slot-input"
data-tf="{{ tf }}"
>
<input type="hidden" name="uploaded_screenshot_{{ tf }}" class="journal-upload-hidden-file" data-tf="{{ tf }}" value="">
<span class="journal-upload-status" data-tf="{{ tf }}" aria-live="polite"></span>
</div>
{% endfor %}
</div>
<p class="sub journal-upload-hint">可只传部分周期;选文件后即时上传,保存后详情页四宫格查看</p>
{%- endmacro %}
+3 -3
View File
@@ -29,9 +29,9 @@
</div>
<div class="row">
<a class="btn" href="/">返回首页</a>
<strong class="focus-title">关键位放大{% if trade_policy.symbol_restrict_enabled %}选择币种{% else %}可输入币种{% endif %}</strong><span class="exchange-tag">{{ exchange_display }}</span>
<strong class="focus-title">关键位放大{% if trade_policy.symbol_restrict_enabled %}(选择币种){% else %}(可输入币种){% endif %}</strong><span class="exchange-tag">{{ exchange_display }}</span>
</div>
<div class="status">最近刷新<span id="updated-at">--</span></div>
<div class="status">最近刷新:<span id="updated-at">--</span></div>
</div>
<div class="row" style="margin-top:10px">
<label>币种</label>
@@ -41,7 +41,7 @@
{{ symbol_live_price_hint('key-focus-symbol-live-price', 'symbol-input') }}
<label>关键位</label>
<select id="key-id">
<option value="">仅看K线</option>
<option value="">(仅看K线)</option>
{% for k in key_list %}
<option value="{{ k.id }}" {% if selected_key and k.id == selected_key.id %}selected{% endif %}>#{{ k.id }} {{ k.symbol }} {{ k.monitor_type }} {{ '做多' if k.direction == 'long' else '做空' }}</option>
{% endfor %}
@@ -150,7 +150,7 @@
<option value="收敛突破">收敛突破</option>
<option value="斐波回调0.618">斐波回调0.618</option>
<option value="斐波回调0.786">斐波回调0.786</option>
<option value="假突破">假突破BTC/ETH</option>
<option value="假突破">假突破(BTC/ETH)</option>
{% endif %}
{% if key_auto_order_enabled|default(false) %}
<option value="回调触价开仓">回调触价开仓</option>
@@ -13,47 +13,47 @@
<tbody>
<tr>
<td class="key-rule-type">箱体突破<br><span class="key-rule-sub">收敛突破</span></td>
<td class="key-rule-cell">方向必选填 H/L<br>方案标准 / 1R·1.5H / 趋势<br>可勾移动保本</td>
<td class="key-rule-cell">{{ r.tf }} 两根闭合 K{{ r.breakout_bar }}/{{ r.confirm_bar }}<br>突破 &gt;{{ r.amp_min_pct }}%确认在箱外<br>&gt;前{{ r.vol_ma_bars }}均×{{ r.vol_ratio_min }}<br>成交 Top{{ r.vol_rank_max }}RR &gt;{{ r.min_rr }}<br>标记价先破反向边界→失效</td>
<td class="key-rule-cell">标准SL 极值外{{ r.stop_outside_pct }}%TP=E±H<br>1RSL=E∓HTP=E∓1.5H<br>趋势SL 极值外{{ r.trend_stop_outside_pct }}%TP 自填</td>
<td class="key-rule-cell">方向必选;填 H/L<br>方案:标准 / 1R·1.5H / 趋势<br>可勾移动保本</td>
<td class="key-rule-cell">{{ r.tf }} 两根闭合 K({{ r.breakout_bar }}/{{ r.confirm_bar }})<br>突破 &gt;{{ r.amp_min_pct }}%;确认在箱外<br>&gt;前{{ r.vol_ma_bars }}均×{{ r.vol_ratio_min }}<br>成交 Top{{ r.vol_rank_max }};RR &gt;{{ r.min_rr }}<br>标记价先破反向边界→失效</td>
<td class="key-rule-cell">标准:SL 极值外{{ r.stop_outside_pct }}%,TP=E±H<br>1R:SL=E∓H,TP=E∓1.5H<br>趋势:SL 极值外{{ r.trend_stop_outside_pct }}%,TP 自填</td>
<td class="key-rule-cell">门控过→市价开仓→下单监控<br>满仓不可再加</td>
</tr>
<tr>
<td class="key-rule-type">斐波回调<br><span class="key-rule-sub">0.618 / 0.786</span></td>
<td class="key-rule-cell">方向 + H/L 波段<br>系统算 E/SL/TP</td>
<td class="key-rule-cell">E=HSL=LTP=H<br>E=L+rΔSL=HTP=L<br>RR &gt;{{ r.min_rr }}先触 TP 侧失效</td>
<td class="key-rule-cell">:E=H,SL=L,TP=H<br>:E=L+rΔ,SL=H,TP=L<br>RR &gt;{{ r.min_rr }};先触 TP 侧失效</td>
<td class="key-rule-cell">公式固定 SL/TP<br>成交后挂所</td>
<td class="key-rule-cell">挂限价等成交<br>成交→下单监控</td>
</tr>
<tr>
<td class="key-rule-type">假突破<br><span class="key-rule-sub">BTC / ETH</span></td>
<td class="key-rule-cell">空填高点 / 多填低点<br>同币仅 1 条</td>
<td class="key-rule-cell">外侧 {{ r.fb_offset_pct }}% 限价<br>SL {{ r.fb_sl_pct }}%RR {{ r.fb_rr }}<br>有效 {{ r.fb_valid_hours }}h</td>
<td class="key-rule-cell">外侧 {{ r.fb_offset_pct }}% 限价<br>SL {{ r.fb_sl_pct }}%;RR {{ r.fb_rr }}<br>有效 {{ r.fb_valid_hours }}h</td>
<td class="key-rule-cell">自动 E/SL/TP<br>可保本</td>
<td class="key-rule-cell">即挂限价<br>成交/过期→历史</td>
</tr>
<tr>
<td class="key-rule-type">回调触价开仓</td>
<td class="key-rule-cell">方向 + 入场 E / 止损 SL / 止盈 TP<br>可勾移动保本时间平仓</td>
<td class="key-rule-cell">RR &gt;{{ r.min_rr }}做多 SL&lt;E&lt;TP<br>标记价回调触 E多≤E / 空≥E后下一轮询市价开<br>先触 TP 侧失效有效 {{ r.trigger_entry_validity_hours }}h</td>
<td class="key-rule-cell">程序盯价无交易所挂单<br>成交后挂所 TP/SL → 下单监控</td>
<td class="key-rule-cell">方向 + 入场 E / 止损 SL / 止盈 TP<br>可勾移动保本,时间平仓</td>
<td class="key-rule-cell">RR &gt;{{ r.min_rr }};做多 SL&lt;E&lt;TP<br>标记价回调触 E(多≤E / 空≥E)后下一轮询市价开<br>先触 TP 侧失效;有效 {{ r.trigger_entry_validity_hours }}h</td>
<td class="key-rule-cell">程序盯价,无交易所挂单<br>成交后挂所 TP/SL → 下单监控</td>
<td class="key-rule-cell">占当日开仓意图<br>全仓模式可用</td>
</tr>
<tr>
<td class="key-rule-type">突破触价开仓</td>
<td class="key-rule-cell">方向 + 突破价 E / 止损 SL / 止盈 TP<br>可勾移动保本时间平仓</td>
<td class="key-rule-cell">RR &gt;{{ r.min_rr }}做多 SL&lt;E&lt;TP<br>标记价<strong>穿越</strong> E 立即市价开多向上 / 空向下<br>先触 TP 或 SL 侧失效有效 {{ r.trigger_entry_validity_hours }}h</td>
<td class="key-rule-cell">程序盯价无交易所挂单<br>成交后挂所 TP/SL → 下单监控</td>
<td class="key-rule-cell">方向 + 突破价 E / 止损 SL / 止盈 TP<br>可勾移动保本,时间平仓</td>
<td class="key-rule-cell">RR &gt;{{ r.min_rr }};做多 SL&lt;E&lt;TP<br>标记价<strong>穿越</strong> E 立即市价开(多向上 / 空向下)<br>先触 TP 或 SL 侧失效;有效 {{ r.trigger_entry_validity_hours }}h</td>
<td class="key-rule-cell">程序盯价,无交易所挂单<br>成交后挂所 TP/SL → 下单监控</td>
<td class="key-rule-cell">占当日开仓意图<br>全仓模式可用</td>
</tr>
<tr>
<td class="key-rule-type">关键支撑阻力</td>
<td class="key-rule-cell">双向填上/下沿</td>
<td class="key-rule-cell">双向;填上/下沿</td>
<td class="key-rule-cell">{{ r.tf }} 收盘破上沿或下沿<br>上沿优先</td>
<td class="key-rule-cell">仅提醒</td>
<td class="key-rule-cell">(仅提醒)</td>
<td class="key-rule-cell">微信 ≤{{ r.alert_max }} 次<br>间隔 ≥{{ r.alert_interval_min }} 分</td>
</tr>
</tbody>
</table>
</div>
<p class="key-rule-foot">阈值来自 <code>.env</code>修改后重启实例</p>
<p class="key-rule-foot">阈值来自 <code>.env</code>,修改后重启实例.</p>
+3 -3
View File
@@ -25,9 +25,9 @@
</div>
<div class="row">
<a class="btn" href="/">返回首页</a>
<strong class="focus-title">实盘下单放大100根K线</strong><span class="exchange-tag">{{ exchange_display }}</span>
<strong class="focus-title">实盘下单放大(100根K线)</strong><span class="exchange-tag">{{ exchange_display }}</span>
</div>
<div class="status">最近刷新<span id="updated-at">--</span></div>
<div class="status">最近刷新:<span id="updated-at">--</span></div>
</div>
{% if orders %}
<div class="row" style="margin-top:10px">
@@ -49,7 +49,7 @@
<span id="load-status" class="status"></span>
</div>
{% else %}
<div class="empty">当前没有激活订单无法展示放大K线</div>
<div class="empty">当前没有激活订单,无法展示放大K线.</div>
{% endif %}
</div>
@@ -1,21 +1,21 @@
<details class="tip-collapse order-rule-collapse">
<summary class="tip-collapse-summary">开仓规则说明</summary>
<div class="tip-collapse-body rule-tip" id="order-rule-tip">
规则最多 {{ max_active_positions }} 仓BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x
本交易日开仓 {{ 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 %}
规则:最多 {{ max_active_positions }} 仓;BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x;
本交易日开仓 {{ 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>
</details>
<details class="tip-collapse order-sizing-collapse">
<summary class="tip-collapse-summary">计仓与保本说明</summary>
<div class="tip-collapse-body rule-tip">
计仓模式<strong>{{ position_sizing_mode_label }}</strong>仅 .env <code>POSITION_SIZING_MODE</code>须无仓后重启
计仓模式:<strong>{{ position_sizing_mode_label }}</strong>(仅 .env <code>POSITION_SIZING_MODE</code>,须无仓后重启)
{% if position_sizing_mode == 'full_margin' %}
|全仓合约可用×{{ full_margin_buffer_ratio }}BTC/ETH {{ btc_leverage }}x其它 {{ alt_leverage }}x单仓张数按交易所精度
|全仓:合约可用×{{ full_margin_buffer_ratio }},BTC/ETH {{ btc_leverage }}x,其它 {{ alt_leverage }}x,单仓;张数按交易所精度
{% else %}
|以损定仓风险 {{ risk_percent }}%
|以损定仓:风险 {{ risk_percent }}%
{% endif %}
|移动保本下单可勾选关闭开启时 {{ breakeven_rr_trigger }}R 触发每 1R 阶梯上移),偏移 {{ breakeven_offset_pct }}%
|移动保本:下单可勾选关闭;开启时 {{ breakeven_rr_trigger }}R 触发(每 1R 阶梯上移),偏移 {{ breakeven_offset_pct }}%
</div>
</details>
@@ -1,21 +1,21 @@
<details class="tip-collapse order-rule-collapse">
<summary class="tip-collapse-summary">开仓规则说明</summary>
<div class="tip-collapse-body rule-tip" id="order-rule-tip">
规则最多 {{ max_active_positions }} 仓BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x
本交易日开仓 {{ 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 %}
规则:最多 {{ max_active_positions }} 仓;BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x;
本交易日开仓 {{ 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>
</details>
<details class="tip-collapse order-sizing-collapse">
<summary class="tip-collapse-summary">计仓与保本说明</summary>
<div class="tip-collapse-body rule-tip">
计仓模式<strong>{{ position_sizing_mode_label }}</strong>仅 .env <code>POSITION_SIZING_MODE</code>须无仓后重启
计仓模式:<strong>{{ position_sizing_mode_label }}</strong>(仅 .env <code>POSITION_SIZING_MODE</code>,须无仓后重启)
{% if position_sizing_mode == 'full_margin' %}
|全仓合约可用×{{ full_margin_buffer_ratio }}BTC/ETH {{ btc_leverage }}x其它 {{ alt_leverage }}x单仓张数按交易所精度
|全仓:合约可用×{{ full_margin_buffer_ratio }},BTC/ETH {{ btc_leverage }}x,其它 {{ alt_leverage }}x,单仓;张数按交易所精度
{% else %}
|以损定仓风险 {{ risk_percent }}%
|以损定仓:风险 {{ risk_percent }}%
{% endif %}
|移动保本下单可勾选关闭开启时 {{ breakeven_rr_trigger }}R 触发每 1R 阶梯上移),偏移 {{ breakeven_offset_pct }}%
|移动保本:下单可勾选关闭;开启时 {{ breakeven_rr_trigger }}R 触发(每 1R 阶梯上移),偏移 {{ breakeven_offset_pct }}%
</div>
</details>
@@ -1,21 +1,21 @@
<details class="tip-collapse order-rule-collapse">
<summary class="tip-collapse-summary">开仓规则说明</summary>
<div class="tip-collapse-body rule-tip" id="order-rule-tip">
规则最大同时持仓 {{ max_active_positions }}当前 active {{ active_count }});与「趋势回调」计划互斥BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x
本交易日开仓 {{ 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 %}
规则:最大同时持仓 {{ max_active_positions }}(当前 active {{ active_count }});与「趋势回调」计划互斥;BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x;
本交易日开仓 {{ 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>
</details>
<details class="tip-collapse order-sizing-collapse">
<summary class="tip-collapse-summary">计仓与保本说明</summary>
<div class="tip-collapse-body rule-tip">
计仓模式<strong>{{ position_sizing_mode_label }}</strong>仅 .env <code>POSITION_SIZING_MODE</code>须无仓后重启
计仓模式:<strong>{{ position_sizing_mode_label }}</strong>(仅 .env <code>POSITION_SIZING_MODE</code>,须无仓后重启)
{% if position_sizing_mode == 'full_margin' %}
|全仓合约可用×{{ full_margin_buffer_ratio }}BTC/ETH {{ btc_leverage }}x其它 {{ alt_leverage }}x单仓张数按交易所精度
|全仓:合约可用×{{ full_margin_buffer_ratio }},BTC/ETH {{ btc_leverage }}x,其它 {{ alt_leverage }}x,单仓;张数按交易所精度
{% else %}
|以损定仓风险 {{ risk_percent }}%
|以损定仓:风险 {{ risk_percent }}%
{% endif %}
|移动保本下单可勾选关闭开启时 {{ breakeven_rr_trigger }}R 触发每 1R 阶梯上移),偏移 {{ breakeven_offset_pct }}%
|移动保本:下单可勾选关闭;开启时 {{ breakeven_rr_trigger }}R 触发(每 1R 阶梯上移),偏移 {{ breakeven_offset_pct }}%
</div>
</details>
@@ -1,21 +1,21 @@
<details class="tip-collapse order-rule-collapse">
<summary class="tip-collapse-summary">开仓规则说明</summary>
<div class="tip-collapse-body rule-tip" id="order-rule-tip">
规则最多 {{ max_active_positions }} 仓BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x
本交易日开仓 {{ opens_today }}{% if daily_open_hard_limit > 0 %} / 硬上限 {{ daily_open_hard_limit }}{% endif %}AI 提醒 {{ daily_open_alert_threshold }});
{% if can_trade %}可开仓{% else %}不可开仓{% if active_count >= max_active_positions %}持仓 {{ active_count }}/{{ max_active_positions }}{% endif %}{% if daily_open_hard_limit > 0 and opens_today >= daily_open_hard_limit %}单日开仓达上限{% endif %}{% if open_guard_blocks_now %}未到北京时间 {{ reset_hour }}:00{% endif %}{% endif %}
规则:最多 {{ max_active_positions }} 仓;BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x;
本交易日开仓 {{ opens_today }}{% if daily_open_hard_limit > 0 %} / 硬上限 {{ daily_open_hard_limit }}{% endif %}(AI 提醒 {{ daily_open_alert_threshold }});
{% if can_trade %}可开仓{% else %}不可开仓{% if active_count >= max_active_positions %}(持仓 {{ active_count }}/{{ max_active_positions }}){% endif %}{% if daily_open_hard_limit > 0 and opens_today >= daily_open_hard_limit %}(单日开仓达上限){% endif %}{% if open_guard_blocks_now %}(未到北京时间 {{ reset_hour }}:00){% endif %}{% endif %};
人工开仓盈亏比不得低于 {{ manual_min_planned_rr }}:1
</div>
</details>
<details class="tip-collapse order-sizing-collapse">
<summary class="tip-collapse-summary">计仓与保本说明</summary>
<div class="tip-collapse-body rule-tip">
计仓模式<strong>{{ position_sizing_mode_label }}</strong>仅 .env <code>POSITION_SIZING_MODE</code>须无仓后重启
计仓模式:<strong>{{ position_sizing_mode_label }}</strong>(仅 .env <code>POSITION_SIZING_MODE</code>,须无仓后重启)
{% if position_sizing_mode == 'full_margin' %}
|全仓合约可用×{{ full_margin_buffer_ratio }}BTC/ETH {{ btc_leverage }}x其它 {{ alt_leverage }}x单仓张数按交易所精度
|全仓:合约可用×{{ full_margin_buffer_ratio }},BTC/ETH {{ btc_leverage }}x,其它 {{ alt_leverage }}x,单仓;张数按交易所精度
{% else %}
|以损定仓风险 {{ risk_percent }}%
|以损定仓:风险 {{ risk_percent }}%
{% endif %}
|移动保本下单可勾选关闭开启时 {{ breakeven_rr_trigger }}R 触发每 1R 阶梯上移),偏移 {{ breakeven_offset_pct }}%
|移动保本:下单可勾选关闭;开启时 {{ breakeven_rr_trigger }}R 触发(每 1R 阶梯上移),偏移 {{ breakeven_offset_pct }}%
</div>
</details>
@@ -1,5 +1,5 @@
<div id="order-plan-preview" class="order-plan-preview">
<span id="order-risk-preview" class="order-preview-risk">预估风险<strong></strong></span>
<span id="order-profit-preview" class="order-preview-profit">预估盈利<strong></strong></span>
<span id="order-rr-preview" class="order-preview-rr">预估盈亏比<strong></strong></span>
<span id="order-risk-preview" class="order-preview-risk">预估风险:<strong></strong></span>
<span id="order-profit-preview" class="order-preview-profit">预估盈利:<strong></strong></span>
<span id="order-rr-preview" class="order-preview-rr">预估盈亏比:<strong></strong></span>
</div>
@@ -51,8 +51,8 @@
<div class="strategy-records-page card full">
<h2>策略交易记录</h2>
<p class="strategy-records-tip">
数据库保留最近 <strong>{{ strategy_records_limit|default(100) }}</strong> 条结束快照按结束时间排序)。
趋势回调与顺势加仓分栏展示点击行展开详情结束计划保本移交止盈止损会自动写入
数据库保留最近 <strong>{{ strategy_records_limit|default(100) }}</strong> 条结束快照(按结束时间排序).
趋势回调与顺势加仓分栏展示;点击行展开详情.结束计划,保本移交,止盈止损会自动写入.
</p>
<div class="sr-filters" id="sr-filters">
+1 -1
View File
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
@@ -3,18 +3,18 @@
<details class="tip-collapse strategy-roll-rule-collapse" open>
<summary class="tip-collapse-summary">顺势加仓规则说明{% if roll_trend_active %} · 当前有趋势回调计划{% endif %}</summary>
<div class="tip-collapse-body rule-tip">
<strong>仅人工提交</strong>须先在「实盘下单」有同向持仓<strong>以损定仓</strong>模式可用<br>
做多/做空各最多滚仓 <strong>3</strong>仅计已成交腿);止盈<strong>锁定首仓</strong>不变<br>
风险比例读取所选监控单<strong>不可手改</strong>打到新止损时合并持仓亏损 ≈ 1 个风险单位当前基数 × 监控 risk%)。<br>
斐波/突破为<strong>程序监控</strong>交易所 mark 价),触价后市价加仓填写后直接点「执行滚仓」无需预览)。同时仅允许 <strong>1</strong> 条监控中腿提交后<strong>不可修改</strong>可删除<br>
手动平仓后滚仓监控自动结束<strong>已成交腿历史保留</strong>供复盘<br>
<strong>仅人工提交</strong>;须先在「实盘下单」有同向持仓.<strong>以损定仓</strong>模式可用.<br>
做多/做空各最多滚仓 <strong>3</strong>(仅计已成交腿);止盈<strong>锁定首仓</strong>不变.<br>
风险比例读取所选监控单,<strong>不可手改</strong>;打到新止损时合并持仓亏损 ≈ 1 个风险单位(当前基数 × 监控 risk%).<br>
斐波/突破为<strong>程序监控</strong>(交易所 mark 价),触价后市价加仓;填写后直接点「执行滚仓」(无需预览).同时仅允许 <strong>1</strong> 条监控中腿,提交后<strong>不可修改</strong>,可删除.<br>
手动平仓后滚仓监控自动结束;<strong>已成交腿历史保留</strong>供复盘.<br>
<a href="/strategy/roll/docs" target="_blank" rel="noopener" class="roll-doc-link">→ 顺势加仓完整逻辑说明</a><br>
{% if roll_trend_active %}<span style="color:#ff8f8f">当前有运行中的趋势回调计划请先结束后再滚仓</span>{% endif %}
{% if roll_trend_active %}<span style="color:#ff8f8f">当前有运行中的趋势回调计划,请先结束后再滚仓.</span>{% endif %}
</div>
</details>
<div id="roll-risk-banner" class="rule-tip roll-risk-banner">
当前风险请选择持仓币种
当前风险:请选择持仓币种
</div>
<form id="roll-form" action="{{ url_for('strategy_roll_execute') }}" method="post" class="form-row" data-add-mode="market">
@@ -92,7 +92,7 @@
<td>{{ leg.status_label or leg.status }}</td>
<td>
{% if leg.status == 'pending' %}
<form action="{{ url_for('strategy_roll_cancel_leg', leg_id=leg.id) }}" method="post" style="margin:0" onsubmit="return confirm('确认删除本条滚仓监控')">
<form action="{{ url_for('strategy_roll_cancel_leg', leg_id=leg.id) }}" method="post" style="margin:0" onsubmit="return confirm('确认删除本条滚仓监控?')">
<button type="submit" style="padding:2px 8px;font-size:.75rem">删除</button>
</form>
{% else %}—{% endif %}
@@ -14,7 +14,7 @@
<div class="box">
<h1>趋势回调</h1>
<p>{{ trend_note }}</p>
<p style="color:#8892b0;font-size:.9rem">趋势回调含自动补仓档位在三所实例Binance / Gate / OKX中均可启用须配置 LIVE_TRADING_ENABLED=true</p>
<p style="color:#8892b0;font-size:.9rem">趋势回调含自动补仓档位,在三所实例(Binance / Gate / OKX)中均可启用,须配置 LIVE_TRADING_ENABLED=true.</p>
</div>
</body>
</html>
@@ -2,15 +2,15 @@
<div class="card trend-card" style="grid-column:1/-1">
<h2 style="margin-bottom:8px">趋势回调</h2>
<details class="tip-collapse strategy-trend-disabled-collapse">
<summary class="tip-collapse-summary">趋势回调说明本实例未启用</summary>
<summary class="tip-collapse-summary">趋势回调说明(本实例未启用)</summary>
<div class="tip-collapse-body rule-tip">
{{ trend_disabled_note }}<br><br>
趋势回调含自动补仓档位与预览执行<strong>Binance / Gate / OKX</strong> 各实例的「策略交易 → 趋势回调」中运行
请访问对应实例同一菜单或常用地址如 Gate <code>:5000/strategy/trend</code>
趋势回调含自动补仓档位与预览执行,<strong>Binance / Gate / OKX</strong> 各实例的「策略交易 → 趋势回调」中运行.
请访问对应实例同一菜单,或常用地址如 Gate <code>:5000/strategy/trend</code>.
</div>
</details>
<p style="margin-top:12px;font-size:.85rem">
<a href="/trade" style="color:#8fc8ff">返回实盘下单</a>
<a href="/strategy/roll" style="color:#8fc8ff">顺势加仓本实例可用</a>
<a href="/strategy/roll" style="color:#8fc8ff">顺势加仓(本实例可用)</a>
</p>
</div>
@@ -5,19 +5,19 @@
<details class="tip-collapse strategy-trend-rule-collapse">
<summary class="tip-collapse-summary">趋势回调规则说明</summary>
<div class="tip-collapse-body rule-tip">
<strong>生成预览</strong>读取合约 USDT <strong>可用余额快照</strong>并计算计划不下单)。预览有效期 <strong>{{ trend_pullback_preview_ttl }} 秒</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> 会拒绝并要求重新预览
<strong>生成预览</strong>:读取合约 USDT <strong>可用余额快照</strong>并计算计划(不下单).预览有效期 <strong>{{ trend_pullback_preview_ttl }} 秒</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>
</details>
{% if trend_dca_probes %}
{% for p in trend_dca_probes %}
{% if p.trigger_reached and p.block_reason %}
<div class="rule-tip" style="margin-bottom:10px;border-color:#a55;background:#2a1818;color:#ffb4b4">
<strong>计划 #{{ p.plan_id }}</strong> 标记价 {{ p.mark_price }} 已触达补仓触发价 {{ p.next_trigger }}但未自动补仓
{{ p.block_reason }}
<strong>计划 #{{ p.plan_id }}</strong> 标记价 {{ p.mark_price }} 已触达补仓触发价 {{ p.next_trigger }},但未自动补仓:
{{ p.block_reason }}.
{% if not live_trading_enabled %}
请在当前实例 <code>.env</code> 设置 <code>LIVE_TRADING_ENABLED=true</code> 后重启对应 PM2 进程<strong>crypto_gate</strong><strong>crypto_okx</strong><strong>crypto_binance</strong>)。
请在当前实例 <code>.env</code> 设置 <code>LIVE_TRADING_ENABLED=true</code> 后重启对应 PM2 进程(<strong>crypto_gate</strong>,<strong>crypto_okx</strong>,<strong>crypto_binance</strong>).
{% endif %}
</div>
{% endif %}
@@ -30,7 +30,7 @@
{% from 'symbol_live_price_snippet.html' import symbol_live_price_hint %}
{{ symbol_live_price_hint('trend-symbol-live-price', 'trend-symbol', 'trend-direction') }}
<input name="leverage" type="number" min="1" step="1" placeholder="杠杆(必填)" required>
<input name="risk_percent" type="number" min="0.1" step="0.1" value="5" placeholder="风险%相对可用快照" title="默认5最坏亏损约≤可用余额×5%">
<input name="risk_percent" type="number" min="0.1" step="0.1" value="5" placeholder="风险%相对可用快照" title="默认5:最坏亏损约≤可用余额×5%">
<input name="sl" step="any" placeholder="止损价" required>
<input name="add_upper" id="trend-add-upper" step="any" placeholder="补仓上沿价" required>
<input name="take_profit" step="any" placeholder="止盈价(固定)" required>
@@ -55,14 +55,14 @@
{% if trend_preview %}
<div style="margin-top:14px;padding:12px;background:#141a2e;border:1px solid #2a3150;border-radius:8px">
<div style="display:flex;flex-wrap:wrap;justify-content:space-between;gap:8px;margin-bottom:8px">
<strong style="color:#dbe4ff">当前预览剩余 <span id="trend-preview-ttl">{{ trend_pullback_preview_ttl }}</span>s</strong>
<strong style="color:#dbe4ff">当前预览(剩余 <span id="trend-preview-ttl">{{ trend_pullback_preview_ttl }}</span>s)</strong>
<span style="font-size:.8rem;color:#9aa" data-expires-ms="{{ preview_expires_ms }}">倒计时加载中…</span>
</div>
<div style="font-size:.82rem;color:#cfd3ef;line-height:1.55;margin-bottom:10px">
{{ trend_preview.symbol }} {{ '做多' if trend_preview.direction == 'long' else '做空' }} {{ trend_preview.leverage }}x
预览可用快照 <strong>{{ mf(trend_preview.snapshot_available_usdt) }}</strong> U 参考价 {{ price_fmt(trend_preview.symbol, trend_preview.live_price_ref) }}
计划保证金≈{{ mf(trend_preview.plan_margin_capital) }} U 总张≈{{ amt_disp(trend_preview.symbol, trend_preview.target_order_amount) }}首仓 {{ amt_disp(trend_preview.symbol, trend_preview.first_order_amount) }} + 补仓 {{ amt_disp(trend_preview.symbol, trend_preview.remainder_total) }}<br>
止损价 {{ price_fmt(trend_preview.symbol, trend_preview.preview_unified_stop_loss or trend_preview.stop_loss) }} 止损金额 {% if trend_preview.preview_risk_amount_u is not none %}{{ mf(trend_preview.preview_risk_amount_u) }}U{% else %}—{% endif %}快照×风险{{ trend_preview.risk_percent }}% {{ trend_add_zone_label(trend_preview.direction) }} {{ price_fmt(trend_preview.symbol, trend_preview.add_upper) }} 止盈价 {{ price_fmt(trend_preview.symbol, trend_preview.take_profit) }} 首仓盈亏比 {% if trend_preview.preview_target_rr is not none %}{{ '%.2f'|format(trend_preview.preview_target_rr) }}{% else %}—{% endif %}
计划保证金≈{{ mf(trend_preview.plan_margin_capital) }} U 总张≈{{ amt_disp(trend_preview.symbol, trend_preview.target_order_amount) }}(首仓 {{ amt_disp(trend_preview.symbol, trend_preview.first_order_amount) }} + 补仓 {{ amt_disp(trend_preview.symbol, trend_preview.remainder_total) }})<br>
止损价 {{ price_fmt(trend_preview.symbol, trend_preview.preview_unified_stop_loss or trend_preview.stop_loss) }} 止损金额 {% if trend_preview.preview_risk_amount_u is not none %}{{ mf(trend_preview.preview_risk_amount_u) }}U{% else %}—{% endif %}(快照×风险{{ trend_preview.risk_percent }}%) {{ trend_add_zone_label(trend_preview.direction) }} {{ price_fmt(trend_preview.symbol, trend_preview.add_upper) }} 止盈价 {{ price_fmt(trend_preview.symbol, trend_preview.take_profit) }} 首仓盈亏比 {% if trend_preview.preview_target_rr is not none %}{{ '%.2f'|format(trend_preview.preview_target_rr) }}{% else %}—{% endif %}
</div>
<div class="table-wrap" style="margin-bottom:10px">
<table>
@@ -83,7 +83,7 @@
<div class="form-row" style="gap:10px;align-items:center">
<form action="{{ url_for('execute_trend_pullback') }}" method="post" style="display:inline">
<input type="hidden" name="preview_id" value="{{ trend_preview.id }}">
<button type="submit" onclick="return confirm('确认按预览参数实盘下单')">确认执行实盘</button>
<button type="submit" onclick="return confirm('确认按预览参数实盘下单?')">确认执行(实盘)</button>
</form>
<form action="{{ url_for('cancel_trend_pullback_preview') }}" method="post" style="display:inline">
<input type="hidden" name="preview_id" value="{{ trend_preview.id }}">
@@ -98,7 +98,7 @@
const exp = parseInt(el.getAttribute("data-expires-ms")||"0",10);
function tick(){
const left = Math.max(0, Math.floor((exp - Date.now()) / 1000));
el.innerText = left > 0 ? ("剩余 " + left + " 秒") : "已过期请重新生成预览";
el.innerText = left > 0 ? ("剩余 " + left + " 秒") : "已过期,请重新生成预览";
const span = document.getElementById("trend-preview-ttl");
if(span) span.innerText = String(left);
if(left <= 0) return;
@@ -108,7 +108,7 @@
})();
</script>
{% elif trend_preview_expired %}
<div class="rule-tip" style="margin-top:12px;color:#ff8f8f">该预览已过期超过 {{ trend_pullback_preview_ttl }} 秒),请重新点击「生成预览」</div>
<div class="rule-tip" style="margin-top:12px;color:#ff8f8f">该预览已过期(超过 {{ trend_pullback_preview_ttl }} 秒),请重新点击「生成预览」.</div>
{% endif %}
<div class="trend-running-plans">
@@ -126,7 +126,7 @@
<span>#{{ t.id }} {{ sym }}</span>
<span class="badge {{ 'direction-long' if t.direction == 'long' else 'direction-short' }}">{{ '做多' if t.direction == 'long' else '做空' }}</span>
</div>
<a href="/stop_trend_pullback/{{ t.id }}" class="btn-close-plan" onclick="return confirm('结束计划市价平仓并撤掉该合约全部挂单确定')">结束计划</a>
<a href="/stop_trend_pullback/{{ t.id }}" class="btn-close-plan" onclick="return confirm('结束计划:市价平仓并撤掉该合约全部挂单,确定?')">结束计划</a>
</div>
<div class="plan-card-meta">
来源: 趋势回调计划 风险: {% if t.risk_percent is not none %}{{ t.risk_percent }}%{% else %}—{% endif %}
@@ -184,7 +184,7 @@
</div>
{% endif %}
<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('确认保本将结束本趋势计划持仓移交「下单监控」备注趋势回调计划),并在交易所同时挂保本止损与计划止盈后续平仓会写入交易记录');">
<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">
@@ -1,10 +1,10 @@
{# 币种输入旁实时现价须加载 symbol_live_price.js #}
{% macro symbol_live_price_hint(price_id, symbol_input_id, direction_input_id='') -%}
<span
id="{{ price_id }}"
class="symbol-live-price"
data-symbol-input="{{ symbol_input_id }}"
{% if direction_input_id %}data-direction-input="{{ direction_input_id }}"{% endif %}
aria-live="polite"
>现价</span>
{%- endmacro %}
{# 币种输入旁实时现价(须加载 symbol_live_price.js) #}
{% macro symbol_live_price_hint(price_id, symbol_input_id, direction_input_id='') -%}
<span
id="{{ price_id }}"
class="symbol-live-price"
data-symbol-input="{{ symbol_input_id }}"
{% if direction_input_id %}data-direction-input="{{ direction_input_id }}"{% endif %}
aria-live="polite"
>现价:</span>
{%- endmacro %}
+32 -32
View File
@@ -1,32 +1,32 @@
{# 方向 / 币种env 账户级限制三所共用宏);调用方须 with context #}
{% if trade_policy is not defined %}
{% set trade_policy = {'symbol_restrict_enabled': false, 'direction_restrict_enabled': false, 'symbol_whitelist': [], 'allows_long': true, 'allows_short': true, 'direction_mode': 'both', 'badge_text': ''} %}
{% endif %}
{% macro trade_policy_symbol(name, id, value='', required=true, placeholder='BTC 或 BTC/USDT') -%}
{% if trade_policy.symbol_restrict_enabled and trade_policy.symbol_whitelist %}
<select name="{{ name }}" id="{{ id }}" {% if required %}required{% endif %} class="trade-policy-symbol-select">
<option value="">选择币种</option>
{% for sym in trade_policy.symbol_whitelist %}
<option value="{{ sym }}" {% if value and (value|upper == sym or value|upper.startswith(sym ~ '/')) %}selected{% endif %}>{{ sym }}/USDT</option>
{% endfor %}
</select>
{% else %}
<input id="{{ id }}" name="{{ name }}" placeholder="{{ placeholder }}" {% if required %}required{% endif %} value="{{ value }}">
{% endif %}
{%- endmacro %}
{% macro trade_policy_direction(name, id, required=true, include_empty=true) -%}
{% if trade_policy.direction_restrict_enabled and trade_policy.direction_mode == 'long_only' %}
<span class="trade-policy-dir-lock" title="账户配置仅做多">做多</span>
<input type="hidden" name="{{ name }}" id="{{ id }}" value="long">
{% elif trade_policy.direction_restrict_enabled and trade_policy.direction_mode == 'short_only' %}
<span class="trade-policy-dir-lock" title="账户配置仅做空">做空</span>
<input type="hidden" name="{{ name }}" id="{{ id }}" value="short">
{% else %}
<select name="{{ name }}" id="{{ id }}" {% if required %}required{% endif %}>
{% if include_empty %}<option value="">方向</option>{% endif %}
{% if trade_policy.allows_long %}<option value="long">做多</option>{% endif %}
{% if trade_policy.allows_short %}<option value="short">做空</option>{% endif %}
</select>
{% endif %}
{%- endmacro %}
{# 方向 / 币种:env 账户级限制(三所共用宏);调用方须 with context #}
{% if trade_policy is not defined %}
{% set trade_policy = {'symbol_restrict_enabled': false, 'direction_restrict_enabled': false, 'symbol_whitelist': [], 'allows_long': true, 'allows_short': true, 'direction_mode': 'both', 'badge_text': ''} %}
{% endif %}
{% macro trade_policy_symbol(name, id, value='', required=true, placeholder='BTC 或 BTC/USDT') -%}
{% if trade_policy.symbol_restrict_enabled and trade_policy.symbol_whitelist %}
<select name="{{ name }}" id="{{ id }}" {% if required %}required{% endif %} class="trade-policy-symbol-select">
<option value="">选择币种</option>
{% for sym in trade_policy.symbol_whitelist %}
<option value="{{ sym }}" {% if value and (value|upper == sym or value|upper.startswith(sym ~ '/')) %}selected{% endif %}>{{ sym }}/USDT</option>
{% endfor %}
</select>
{% else %}
<input id="{{ id }}" name="{{ name }}" placeholder="{{ placeholder }}" {% if required %}required{% endif %} value="{{ value }}">
{% endif %}
{%- endmacro %}
{% macro trade_policy_direction(name, id, required=true, include_empty=true) -%}
{% if trade_policy.direction_restrict_enabled and trade_policy.direction_mode == 'long_only' %}
<span class="trade-policy-dir-lock" title="账户配置:仅做多">做多</span>
<input type="hidden" name="{{ name }}" id="{{ id }}" value="long">
{% elif trade_policy.direction_restrict_enabled and trade_policy.direction_mode == 'short_only' %}
<span class="trade-policy-dir-lock" title="账户配置:仅做空">做空</span>
<input type="hidden" name="{{ name }}" id="{{ id }}" value="short">
{% else %}
<select name="{{ name }}" id="{{ id }}" {% if required %}required{% endif %}>
{% if include_empty %}<option value="">方向</option>{% endif %}
{% if trade_policy.allows_long %}<option value="long">做多</option>{% endif %}
{% if trade_policy.allows_short %}<option value="short">做空</option>{% endif %}
</select>
{% endif %}
{%- endmacro %}