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:
+232
-232
@@ -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
@@ -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
|
||||
|
||||
@@ -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]: ...
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"))
|
||||
|
||||
+640
-640
File diff suppressed because it is too large
Load Diff
+384
-384
@@ -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
@@ -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_entry、reward_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
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
+695
-695
File diff suppressed because it is too large
Load Diff
+1972
-1972
File diff suppressed because it is too large
Load Diff
+143
-143
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>突破 >{{ r.amp_min_pct }}%;确认在箱外<br>量 >前{{ r.vol_ma_bars }}均×{{ r.vol_ratio_min }}<br>成交 Top{{ r.vol_rank_max }};RR >{{ 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">方向必选;填 H/L<br>方案:标准 / 1R·1.5H / 趋势<br>可勾移动保本</td>
|
||||
<td class="key-rule-cell">{{ r.tf }} 两根闭合 K({{ r.breakout_bar }}/{{ r.confirm_bar }})<br>突破 >{{ r.amp_min_pct }}%;确认在箱外<br>量 >前{{ r.vol_ma_bars }}均×{{ r.vol_ratio_min }}<br>成交 Top{{ r.vol_rank_max }};RR >{{ 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=H−rΔ,SL=L,TP=H<br>空:E=L+rΔ,SL=H,TP=L<br>RR >{{ r.min_rr }};先触 TP 侧失效</td>
|
||||
<td class="key-rule-cell">多:E=H−rΔ,SL=L,TP=H<br>空:E=L+rΔ,SL=H,TP=L<br>RR >{{ 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 >{{ r.min_rr }};做多 SL<E<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 >{{ r.min_rr }};做多 SL<E<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 >{{ r.min_rr }};做多 SL<E<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 >{{ r.min_rr }};做多 SL<E<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>
|
||||
|
||||
@@ -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,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>
|
||||
确认执行时若当前可用余额与预览快照相对偏差 > <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>
|
||||
确认执行时若当前可用余额与预览快照相对偏差 > <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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
Reference in New Issue
Block a user