feat: 非交易时段禁开仓、移动保本与交易结果分类。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -377,6 +377,8 @@ def init_db():
|
|||||||
set_setting("risk_percent", "1")
|
set_setting("risk_percent", "1")
|
||||||
if not get_setting("max_margin_pct"):
|
if not get_setting("max_margin_pct"):
|
||||||
set_setting("max_margin_pct", "30")
|
set_setting("max_margin_pct", "30")
|
||||||
|
if not get_setting("trailing_be_tick_buffer"):
|
||||||
|
set_setting("trailing_be_tick_buffer", "2")
|
||||||
if not get_setting("fee_source_mode"):
|
if not get_setting("fee_source_mode"):
|
||||||
set_setting("fee_source_mode", "ctp")
|
set_setting("fee_source_mode", "ctp")
|
||||||
set_setting("fee_source_mode", "ctp")
|
set_setting("fee_source_mode", "ctp")
|
||||||
@@ -1654,6 +1656,12 @@ def settings():
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
flash("保证金比例无效")
|
flash("保证金比例无效")
|
||||||
return redirect(url_for("settings"))
|
return redirect(url_for("settings"))
|
||||||
|
try:
|
||||||
|
tb = int(float(request.form.get("trailing_be_tick_buffer", "2") or 2))
|
||||||
|
set_setting("trailing_be_tick_buffer", str(max(1, min(20, tb))))
|
||||||
|
except ValueError:
|
||||||
|
flash("移动保本缓冲无效")
|
||||||
|
return redirect(url_for("settings"))
|
||||||
flash("交易模式已保存")
|
flash("交易模式已保存")
|
||||||
elif action == "nav":
|
elif action == "nav":
|
||||||
items = {k: request.form.get(f"nav_{k}") == "on" for k in NAV_TOGGLES}
|
items = {k: request.form.get(f"nav_{k}") == "on" for k in NAV_TOGGLES}
|
||||||
@@ -1694,6 +1702,7 @@ def settings():
|
|||||||
position_sizing_mode=get_setting("position_sizing_mode", "risk"),
|
position_sizing_mode=get_setting("position_sizing_mode", "risk"),
|
||||||
risk_percent=get_setting("risk_percent", "1"),
|
risk_percent=get_setting("risk_percent", "1"),
|
||||||
max_margin_pct=get_setting("max_margin_pct", "30"),
|
max_margin_pct=get_setting("max_margin_pct", "30"),
|
||||||
|
trailing_be_tick_buffer=get_setting("trailing_be_tick_buffer", "2"),
|
||||||
nav_items=get_nav_items(get_setting),
|
nav_items=get_nav_items(get_setting),
|
||||||
nav_toggles=NAV_TOGGLES,
|
nav_toggles=NAV_TOGGLES,
|
||||||
)
|
)
|
||||||
|
|||||||
+94
-10
@@ -38,6 +38,7 @@ from sl_tp_guard import (
|
|||||||
place_monitor_exit_orders,
|
place_monitor_exit_orders,
|
||||||
reconcile_monitors_without_position,
|
reconcile_monitors_without_position,
|
||||||
start_sl_tp_guard_worker,
|
start_sl_tp_guard_worker,
|
||||||
|
write_manual_close_trade_log,
|
||||||
)
|
)
|
||||||
from risk.account_risk_lib import (
|
from risk.account_risk_lib import (
|
||||||
assert_can_open,
|
assert_can_open,
|
||||||
@@ -61,6 +62,7 @@ from trading_context import (
|
|||||||
get_max_margin_pct,
|
get_max_margin_pct,
|
||||||
get_risk_percent,
|
get_risk_percent,
|
||||||
get_sizing_mode,
|
get_sizing_mode,
|
||||||
|
get_trailing_be_tick_buffer,
|
||||||
get_trading_mode,
|
get_trading_mode,
|
||||||
trading_mode_label,
|
trading_mode_label,
|
||||||
)
|
)
|
||||||
@@ -375,6 +377,8 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
"tick_size": tick.get("tick_size"),
|
"tick_size": tick.get("tick_size"),
|
||||||
"can_close": True,
|
"can_close": True,
|
||||||
"pending_orders": pending_for_row,
|
"pending_orders": pending_for_row,
|
||||||
|
"trailing_be": bool(mon.get("trailing_be")) if mon else False,
|
||||||
|
"trailing_r_locked": int(mon.get("trailing_r_locked") or 0) if mon else 0,
|
||||||
})
|
})
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
@@ -535,19 +539,35 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
break
|
break
|
||||||
codes = ths_to_codes(sym)
|
codes = ths_to_codes(sym)
|
||||||
now_s = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
now_s = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
if "trailing_be" in d:
|
||||||
|
trailing_be = 1 if d.get("trailing_be") else 0
|
||||||
|
elif mon:
|
||||||
|
trailing_be = int(mon.get("trailing_be") or 0)
|
||||||
|
else:
|
||||||
|
trailing_be = 0
|
||||||
|
ensure_monitor_order_columns(conn)
|
||||||
if mon:
|
if mon:
|
||||||
|
initial_sl = mon.get("initial_stop_loss")
|
||||||
|
if sl is not None and initial_sl is None:
|
||||||
|
initial_sl = sl
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""UPDATE trade_order_monitors SET stop_loss=?, take_profit=?, lots=?, entry_price=?
|
"""UPDATE trade_order_monitors SET stop_loss=?, take_profit=?, lots=?, entry_price=?,
|
||||||
|
initial_stop_loss=?, trailing_be=?
|
||||||
WHERE id=?""",
|
WHERE id=?""",
|
||||||
(sl, tp, lots, entry or mon.get("entry_price"), mon["id"]),
|
(
|
||||||
|
sl, tp, lots, entry or mon.get("entry_price"),
|
||||||
|
initial_sl, trailing_be,
|
||||||
|
mon["id"],
|
||||||
|
),
|
||||||
)
|
)
|
||||||
mid = mon["id"]
|
mid = mon["id"]
|
||||||
else:
|
else:
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""INSERT INTO trade_order_monitors (
|
"""INSERT INTO trade_order_monitors (
|
||||||
symbol, symbol_name, market_code, direction, lots, entry_price,
|
symbol, symbol_name, market_code, direction, lots, entry_price,
|
||||||
stop_loss, take_profit, open_time, monitor_type, status
|
stop_loss, take_profit, initial_stop_loss, trailing_be,
|
||||||
) VALUES (?,?,?,?,?,?,?,?,?,?, 'active')""",
|
open_time, monitor_type, status
|
||||||
|
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?, 'active')""",
|
||||||
(
|
(
|
||||||
sym,
|
sym,
|
||||||
codes.get("name", sym) if codes else sym,
|
codes.get("name", sym) if codes else sym,
|
||||||
@@ -557,6 +577,8 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
entry,
|
entry,
|
||||||
sl,
|
sl,
|
||||||
tp,
|
tp,
|
||||||
|
sl,
|
||||||
|
trailing_be,
|
||||||
now_s,
|
now_s,
|
||||||
"manual",
|
"manual",
|
||||||
),
|
),
|
||||||
@@ -667,13 +689,59 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
conn.close()
|
conn.close()
|
||||||
return jsonify({"ok": False, "error": "品种或价格无效"}), 400
|
return jsonify({"ok": False, "error": "品种或价格无效"}), 400
|
||||||
offset = "close_long" if direction == "long" else "close_short"
|
offset = "close_long" if direction == "long" else "close_short"
|
||||||
|
capital = _capital(conn)
|
||||||
|
mon = None
|
||||||
|
mid = int(d.get("monitor_id") or 0)
|
||||||
|
if mid:
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT * FROM trade_order_monitors WHERE id=? AND status='active'",
|
||||||
|
(mid,),
|
||||||
|
).fetchone()
|
||||||
|
if row:
|
||||||
|
mon = dict(row)
|
||||||
|
if not mon:
|
||||||
|
for r in conn.execute(
|
||||||
|
"SELECT * FROM trade_order_monitors WHERE status='active'"
|
||||||
|
).fetchall():
|
||||||
|
row = dict(r)
|
||||||
|
if row.get("direction") != direction:
|
||||||
|
continue
|
||||||
|
if _match_ctp_symbol(sym, row.get("symbol") or ""):
|
||||||
|
mon = row
|
||||||
|
mid = int(row["id"])
|
||||||
|
break
|
||||||
|
entry = float(mon.get("entry_price") or 0) if mon else 0.0
|
||||||
|
if entry <= 0:
|
||||||
|
for p in _ctp_positions(mode):
|
||||||
|
if int(p.get("lots") or 0) <= 0:
|
||||||
|
continue
|
||||||
|
if (p.get("direction") or "long") != direction:
|
||||||
|
continue
|
||||||
|
if _match_ctp_symbol(p.get("symbol") or "", sym):
|
||||||
|
entry = float(p.get("avg_price") or price)
|
||||||
|
break
|
||||||
try:
|
try:
|
||||||
execute_order(
|
execute_order(
|
||||||
conn, mode=mode, offset=offset, symbol=sym, direction=direction,
|
conn, mode=mode, offset=offset, symbol=sym, direction=direction,
|
||||||
lots=lots, price=price, settings=_settings_dict(),
|
lots=lots, price=price, settings=_settings_dict(),
|
||||||
|
order_type="market",
|
||||||
|
)
|
||||||
|
write_manual_close_trade_log(
|
||||||
|
conn,
|
||||||
|
mon,
|
||||||
|
symbol=sym,
|
||||||
|
direction=direction,
|
||||||
|
lots=lots,
|
||||||
|
close_price=price,
|
||||||
|
entry_price=entry or price,
|
||||||
|
trading_mode=mode,
|
||||||
|
capital=capital,
|
||||||
|
stop_loss=float(mon["stop_loss"]) if mon and mon.get("stop_loss") is not None else None,
|
||||||
|
take_profit=float(mon["take_profit"]) if mon and mon.get("take_profit") is not None else None,
|
||||||
|
open_time=(mon.get("open_time") or "") if mon else "",
|
||||||
|
symbol_name=(mon.get("symbol_name") or "") if mon else "",
|
||||||
|
market_code=(mon.get("market_code") or "") if mon else "",
|
||||||
)
|
)
|
||||||
if source == "program":
|
|
||||||
mid = int(d.get("monitor_id") or 0)
|
|
||||||
if mid:
|
if mid:
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE trade_order_monitors SET status='closed' WHERE id=?",
|
"UPDATE trade_order_monitors SET status='closed' WHERE id=?",
|
||||||
@@ -681,7 +749,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
return jsonify({"ok": True})
|
return jsonify({"ok": True, "message": "已平仓并记入交易记录(手动平仓)"})
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
conn.close()
|
conn.close()
|
||||||
return jsonify({"ok": False, "error": str(exc)}), 400
|
return jsonify({"ok": False, "error": str(exc)}), 400
|
||||||
@@ -830,6 +898,12 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
mode = get_trading_mode(get_setting)
|
mode = get_trading_mode(get_setting)
|
||||||
if offset.startswith("open"):
|
if offset.startswith("open"):
|
||||||
_sync_trade_monitors_with_ctp(conn, mode)
|
_sync_trade_monitors_with_ctp(conn, mode)
|
||||||
|
if not is_trading_session():
|
||||||
|
conn.close()
|
||||||
|
return jsonify({"ok": False, "error": "不在交易时间段"}), 403
|
||||||
|
if d.get("trailing_be") and not d.get("stop_loss"):
|
||||||
|
conn.close()
|
||||||
|
return jsonify({"ok": False, "error": "开启移动保本须填写止损价"}), 400
|
||||||
err = assert_can_open(conn, active_count=_effective_active_position_count(conn, mode))
|
err = assert_can_open(conn, active_count=_effective_active_position_count(conn, mode))
|
||||||
if err:
|
if err:
|
||||||
conn.close()
|
conn.close()
|
||||||
@@ -886,9 +960,13 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
settings=_settings_dict(),
|
settings=_settings_dict(),
|
||||||
order_type=order_type,
|
order_type=order_type,
|
||||||
)
|
)
|
||||||
|
if offset.startswith("open") and d.get("trailing_be") and not d.get("stop_loss"):
|
||||||
|
conn.close()
|
||||||
|
return jsonify({"ok": False, "error": "开启移动保本须填写止损价"}), 400
|
||||||
if offset.startswith("open"):
|
if offset.startswith("open"):
|
||||||
sl = d.get("stop_loss")
|
sl = d.get("stop_loss")
|
||||||
tp = d.get("take_profit")
|
tp = d.get("take_profit")
|
||||||
|
trailing_be = 1 if d.get("trailing_be") else 0
|
||||||
import time
|
import time
|
||||||
time.sleep(2.0)
|
time.sleep(2.0)
|
||||||
actual_lots = lots
|
actual_lots = lots
|
||||||
@@ -904,11 +982,14 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
break
|
break
|
||||||
if has_pos:
|
if has_pos:
|
||||||
codes = ths_to_codes(sym)
|
codes = ths_to_codes(sym)
|
||||||
|
sl_f = float(sl) if sl else None
|
||||||
|
ensure_monitor_order_columns(conn)
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""INSERT INTO trade_order_monitors (
|
"""INSERT INTO trade_order_monitors (
|
||||||
symbol, symbol_name, market_code, direction, lots, entry_price,
|
symbol, symbol_name, market_code, direction, lots, entry_price,
|
||||||
stop_loss, take_profit, open_time, monitor_type, status
|
stop_loss, take_profit, initial_stop_loss, trailing_be,
|
||||||
) VALUES (?,?,?,?,?,?,?,?,?,?, 'active')""",
|
open_time, monitor_type, status
|
||||||
|
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?, 'active')""",
|
||||||
(
|
(
|
||||||
sym,
|
sym,
|
||||||
codes.get("name", sym) if codes else sym,
|
codes.get("name", sym) if codes else sym,
|
||||||
@@ -916,8 +997,10 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
direction,
|
direction,
|
||||||
actual_lots,
|
actual_lots,
|
||||||
price,
|
price,
|
||||||
float(sl) if sl else None,
|
sl_f,
|
||||||
float(tp) if tp else None,
|
float(tp) if tp else None,
|
||||||
|
sl_f,
|
||||||
|
trailing_be,
|
||||||
datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
"manual",
|
"manual",
|
||||||
),
|
),
|
||||||
@@ -1414,6 +1497,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
get_mode_fn=lambda: get_trading_mode(get_setting),
|
get_mode_fn=lambda: get_trading_mode(get_setting),
|
||||||
init_tables_fn=_init_tables,
|
init_tables_fn=_init_tables,
|
||||||
get_capital_fn=_capital,
|
get_capital_fn=_capital,
|
||||||
|
get_be_tick_buffer_fn=lambda: get_trailing_be_tick_buffer(get_setting),
|
||||||
notify_fn=send_wechat_msg,
|
notify_fn=send_wechat_msg,
|
||||||
interval=1,
|
interval=1,
|
||||||
)
|
)
|
||||||
|
|||||||
+221
-26
@@ -37,8 +37,13 @@ _closing_lock = threading.Lock()
|
|||||||
MONITOR_ORDER_COLUMNS = (
|
MONITOR_ORDER_COLUMNS = (
|
||||||
"ALTER TABLE trade_order_monitors ADD COLUMN sl_vt_order_id TEXT",
|
"ALTER TABLE trade_order_monitors ADD COLUMN sl_vt_order_id TEXT",
|
||||||
"ALTER TABLE trade_order_monitors ADD COLUMN tp_vt_order_id TEXT",
|
"ALTER TABLE trade_order_monitors ADD COLUMN tp_vt_order_id TEXT",
|
||||||
|
"ALTER TABLE trade_order_monitors ADD COLUMN trailing_be INTEGER DEFAULT 0",
|
||||||
|
"ALTER TABLE trade_order_monitors ADD COLUMN initial_stop_loss REAL",
|
||||||
|
"ALTER TABLE trade_order_monitors ADD COLUMN trailing_r_locked INTEGER DEFAULT 0",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
TRADE_RESULTS = ("止损", "止盈", "移动止盈", "保本止盈", "手动平仓")
|
||||||
|
|
||||||
|
|
||||||
def ensure_monitor_order_columns(conn) -> None:
|
def ensure_monitor_order_columns(conn) -> None:
|
||||||
for sql in MONITOR_ORDER_COLUMNS:
|
for sql in MONITOR_ORDER_COLUMNS:
|
||||||
@@ -139,31 +144,55 @@ def _monitor_type_label(raw: str) -> str:
|
|||||||
return mapping.get(raw or "", raw or "程序监控")
|
return mapping.get(raw or "", raw or "程序监控")
|
||||||
|
|
||||||
|
|
||||||
def _write_trade_log(
|
def _result_for_close(mon: dict, reason: str) -> str:
|
||||||
|
"""平仓结果:止损 / 止盈 / 移动止盈 / 保本止盈 / 手动平仓。"""
|
||||||
|
if reason == "manual":
|
||||||
|
return "手动平仓"
|
||||||
|
if reason == "take_profit":
|
||||||
|
return "止盈"
|
||||||
|
if not mon.get("trailing_be"):
|
||||||
|
return "止损"
|
||||||
|
locked = int(mon.get("trailing_r_locked") or 0)
|
||||||
|
if locked >= 2:
|
||||||
|
return "移动止盈"
|
||||||
|
if locked >= 1:
|
||||||
|
return "保本止盈"
|
||||||
|
return "止损"
|
||||||
|
|
||||||
|
|
||||||
|
def write_trade_log(
|
||||||
conn,
|
conn,
|
||||||
mon: dict,
|
|
||||||
*,
|
*,
|
||||||
|
symbol: str,
|
||||||
|
direction: str,
|
||||||
|
entry_price: float,
|
||||||
close_price: float,
|
close_price: float,
|
||||||
reason: str,
|
lots: float,
|
||||||
|
result: str,
|
||||||
trading_mode: str,
|
trading_mode: str,
|
||||||
|
stop_loss: Optional[float] = None,
|
||||||
|
take_profit: Optional[float] = None,
|
||||||
|
open_time: str = "",
|
||||||
|
symbol_name: str = "",
|
||||||
|
market_code: str = "",
|
||||||
|
sina_code: str = "",
|
||||||
|
monitor_type: str = "期货下单",
|
||||||
capital: float = 0.0,
|
capital: float = 0.0,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""止盈/止损触发平仓后写入 trade_logs。"""
|
"""写入 trade_logs(程序平仓 / 手动平仓)。"""
|
||||||
sym = (mon.get("symbol") or "").strip()
|
sym = (symbol or "").strip()
|
||||||
direction = (mon.get("direction") or "long").strip().lower()
|
direction = (direction or "long").strip().lower()
|
||||||
entry = float(mon.get("entry_price") or close_price)
|
entry = float(entry_price or close_price)
|
||||||
sl_raw = mon.get("stop_loss")
|
sl = float(stop_loss) if stop_loss is not None else entry
|
||||||
tp_raw = mon.get("take_profit")
|
tp = float(take_profit) if take_profit is not None else entry
|
||||||
sl = float(sl_raw) if sl_raw is not None else entry
|
|
||||||
tp = float(tp_raw) if tp_raw is not None else entry
|
|
||||||
lots = float(mon.get("lots") or 1)
|
|
||||||
open_time = (mon.get("open_time") or "").strip()
|
|
||||||
close_time = datetime.now(TZ).strftime("%Y-%m-%dT%H:%M")
|
close_time = datetime.now(TZ).strftime("%Y-%m-%dT%H:%M")
|
||||||
|
|
||||||
|
if not sina_code or not market_code:
|
||||||
codes = ths_to_codes(sym) or {}
|
codes = ths_to_codes(sym) or {}
|
||||||
sina_code = codes.get("sina_code") or ""
|
sina_code = sina_code or codes.get("sina_code") or ""
|
||||||
symbol_name = mon.get("symbol_name") or sym
|
market_code = market_code or codes.get("market_code") or ""
|
||||||
market_code = mon.get("market_code") or codes.get("market_code") or ""
|
if not symbol_name:
|
||||||
|
symbol_name = sym
|
||||||
|
|
||||||
metrics = calc_position_metrics(
|
metrics = calc_position_metrics(
|
||||||
direction, entry, sl, tp, lots, close_price, capital, sym,
|
direction, entry, sl, tp, lots, close_price, capital, sym,
|
||||||
@@ -173,7 +202,6 @@ def _write_trade_log(
|
|||||||
sym, entry, close_price, lots, open_time, close_time, trading_mode=trading_mode,
|
sym, entry, close_price, lots, open_time, close_time, trading_mode=trading_mode,
|
||||||
)
|
)
|
||||||
pnl_net = round(pnl - fee, 2)
|
pnl_net = round(pnl - fee, 2)
|
||||||
result = "止盈" if reason == "take_profit" else "止损"
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from app import holding_to_minutes
|
from app import holding_to_minutes
|
||||||
@@ -192,11 +220,11 @@ def _write_trade_log(
|
|||||||
symbol_name,
|
symbol_name,
|
||||||
market_code,
|
market_code,
|
||||||
sina_code,
|
sina_code,
|
||||||
_monitor_type_label(mon.get("monitor_type") or ""),
|
monitor_type,
|
||||||
direction,
|
direction,
|
||||||
entry,
|
entry,
|
||||||
sl_raw if sl_raw is not None else sl,
|
stop_loss if stop_loss is not None else sl,
|
||||||
tp_raw if tp_raw is not None else tp,
|
take_profit if take_profit is not None else tp,
|
||||||
close_price,
|
close_price,
|
||||||
lots,
|
lots,
|
||||||
metrics.get("margin"),
|
metrics.get("margin"),
|
||||||
@@ -206,14 +234,169 @@ def _write_trade_log(
|
|||||||
pnl,
|
pnl,
|
||||||
fee,
|
fee,
|
||||||
pnl_net,
|
pnl_net,
|
||||||
result,
|
result if result in TRADE_RESULTS else "手动平仓",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
from stats_engine import refresh_stats_cache
|
from stats_engine import refresh_stats_cache
|
||||||
refresh_stats_cache(conn, capital)
|
refresh_stats_cache(conn, capital)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.debug("stats refresh after SL/TP close: %s", exc)
|
logger.debug("stats refresh after close: %s", exc)
|
||||||
|
|
||||||
|
|
||||||
|
def _write_trade_log(
|
||||||
|
conn,
|
||||||
|
mon: dict,
|
||||||
|
*,
|
||||||
|
close_price: float,
|
||||||
|
reason: str,
|
||||||
|
trading_mode: str,
|
||||||
|
capital: float = 0.0,
|
||||||
|
) -> None:
|
||||||
|
sym = (mon.get("symbol") or "").strip()
|
||||||
|
sl_raw = mon.get("stop_loss")
|
||||||
|
tp_raw = mon.get("take_profit")
|
||||||
|
initial_sl = mon.get("initial_stop_loss")
|
||||||
|
write_trade_log(
|
||||||
|
conn,
|
||||||
|
symbol=sym,
|
||||||
|
direction=mon.get("direction") or "long",
|
||||||
|
entry_price=float(mon.get("entry_price") or close_price),
|
||||||
|
close_price=close_price,
|
||||||
|
lots=float(mon.get("lots") or 1),
|
||||||
|
result=_result_for_close(mon, reason),
|
||||||
|
trading_mode=trading_mode,
|
||||||
|
stop_loss=float(initial_sl) if initial_sl is not None else (
|
||||||
|
float(sl_raw) if sl_raw is not None else None
|
||||||
|
),
|
||||||
|
take_profit=float(tp_raw) if tp_raw is not None else None,
|
||||||
|
open_time=(mon.get("open_time") or "").strip(),
|
||||||
|
symbol_name=mon.get("symbol_name") or sym,
|
||||||
|
market_code=mon.get("market_code") or "",
|
||||||
|
monitor_type=_monitor_type_label(mon.get("monitor_type") or ""),
|
||||||
|
capital=capital,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def write_manual_close_trade_log(
|
||||||
|
conn,
|
||||||
|
mon: Optional[dict],
|
||||||
|
*,
|
||||||
|
symbol: str,
|
||||||
|
direction: str,
|
||||||
|
lots: float,
|
||||||
|
close_price: float,
|
||||||
|
entry_price: float,
|
||||||
|
trading_mode: str,
|
||||||
|
capital: float = 0.0,
|
||||||
|
stop_loss: Optional[float] = None,
|
||||||
|
take_profit: Optional[float] = None,
|
||||||
|
open_time: str = "",
|
||||||
|
symbol_name: str = "",
|
||||||
|
market_code: str = "",
|
||||||
|
) -> None:
|
||||||
|
"""程序内点击平仓按钮 → 手动平仓。"""
|
||||||
|
if mon:
|
||||||
|
write_trade_log(
|
||||||
|
conn,
|
||||||
|
symbol=(mon.get("symbol") or symbol).strip(),
|
||||||
|
direction=mon.get("direction") or direction,
|
||||||
|
entry_price=float(mon.get("entry_price") or entry_price),
|
||||||
|
close_price=close_price,
|
||||||
|
lots=float(mon.get("lots") or lots),
|
||||||
|
result="手动平仓",
|
||||||
|
trading_mode=trading_mode,
|
||||||
|
stop_loss=float(mon["initial_stop_loss"]) if mon.get("initial_stop_loss") is not None else (
|
||||||
|
float(mon["stop_loss"]) if mon.get("stop_loss") is not None else stop_loss
|
||||||
|
),
|
||||||
|
take_profit=float(mon["take_profit"]) if mon.get("take_profit") is not None else take_profit,
|
||||||
|
open_time=(mon.get("open_time") or open_time).strip(),
|
||||||
|
symbol_name=mon.get("symbol_name") or symbol_name,
|
||||||
|
market_code=mon.get("market_code") or market_code,
|
||||||
|
monitor_type=_monitor_type_label(mon.get("monitor_type") or ""),
|
||||||
|
capital=capital,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
write_trade_log(
|
||||||
|
conn,
|
||||||
|
symbol=symbol,
|
||||||
|
direction=direction,
|
||||||
|
entry_price=entry_price,
|
||||||
|
close_price=close_price,
|
||||||
|
lots=lots,
|
||||||
|
result="手动平仓",
|
||||||
|
trading_mode=trading_mode,
|
||||||
|
stop_loss=stop_loss,
|
||||||
|
take_profit=take_profit,
|
||||||
|
open_time=open_time,
|
||||||
|
symbol_name=symbol_name,
|
||||||
|
market_code=market_code,
|
||||||
|
capital=capital,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _update_trailing_stop_loss(
|
||||||
|
conn,
|
||||||
|
mon: dict,
|
||||||
|
mark: float,
|
||||||
|
*,
|
||||||
|
be_tick_mult: int,
|
||||||
|
) -> dict:
|
||||||
|
"""达 1R 移保本(开仓±N跳),达 2R 移 1R,依次类推。"""
|
||||||
|
if not mon.get("trailing_be"):
|
||||||
|
return mon
|
||||||
|
entry = float(mon.get("entry_price") or 0)
|
||||||
|
initial_sl = mon.get("initial_stop_loss")
|
||||||
|
if initial_sl is None:
|
||||||
|
initial_sl = mon.get("stop_loss")
|
||||||
|
try:
|
||||||
|
initial_sl_f = float(initial_sl) if initial_sl is not None else None
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return mon
|
||||||
|
if not entry or initial_sl_f is None:
|
||||||
|
return mon
|
||||||
|
|
||||||
|
direction = (mon.get("direction") or "long").strip().lower()
|
||||||
|
sym = (mon.get("symbol") or "").strip()
|
||||||
|
tick = _tick_size(sym)
|
||||||
|
r = abs(entry - initial_sl_f)
|
||||||
|
if r < tick * 0.5:
|
||||||
|
return mon
|
||||||
|
|
||||||
|
profit_r = (mark - entry) / r if direction == "long" else (entry - mark) / r
|
||||||
|
if profit_r < 1.0:
|
||||||
|
return mon
|
||||||
|
|
||||||
|
level = int(profit_r)
|
||||||
|
locked = int(mon.get("trailing_r_locked") or 0)
|
||||||
|
if level <= locked:
|
||||||
|
return mon
|
||||||
|
|
||||||
|
if level == 1:
|
||||||
|
new_sl = entry + be_tick_mult * tick if direction == "long" else entry - be_tick_mult * tick
|
||||||
|
else:
|
||||||
|
new_sl = entry + (level - 1) * r if direction == "long" else entry - (level - 1) * r
|
||||||
|
new_sl = round(new_sl, 4)
|
||||||
|
|
||||||
|
try:
|
||||||
|
current_sl = float(mon.get("stop_loss") or 0)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
current_sl = 0.0
|
||||||
|
if direction == "long" and new_sl <= current_sl + tick * 0.01:
|
||||||
|
return mon
|
||||||
|
if direction == "short" and new_sl >= current_sl - tick * 0.01:
|
||||||
|
return mon
|
||||||
|
|
||||||
|
mid = mon.get("id")
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE trade_order_monitors SET stop_loss=?, trailing_r_locked=? WHERE id=?",
|
||||||
|
(new_sl, level, mid),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
mon["stop_loss"] = new_sl
|
||||||
|
mon["trailing_r_locked"] = level
|
||||||
|
logger.info("移动保本 monitor=%s %dR 止损→%s", mid, level, new_sl)
|
||||||
|
return mon
|
||||||
|
|
||||||
|
|
||||||
def _sl_triggered(direction: str, sl: float, mark: float, tick: float) -> bool:
|
def _sl_triggered(direction: str, sl: float, mark: float, tick: float) -> bool:
|
||||||
@@ -359,14 +542,14 @@ def _execute_local_close(
|
|||||||
)
|
)
|
||||||
conn.execute("UPDATE trade_order_monitors SET status='closed' WHERE id=?", (mon["id"],))
|
conn.execute("UPDATE trade_order_monitors SET status='closed' WHERE id=?", (mon["id"],))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
label = "止盈" if reason == "take_profit" else "止损"
|
result_label = _result_for_close(mon, reason)
|
||||||
logger.info(
|
logger.info(
|
||||||
"止盈止损本地触发 monitor=%s reason=%s %s %s %d手 @%s",
|
"止盈止损本地触发 monitor=%s result=%s %s %s %d手 @%s",
|
||||||
mon.get("id"), reason, sym, direction, lots, mark,
|
mon.get("id"), result_label, sym, direction, lots, mark,
|
||||||
)
|
)
|
||||||
if notify_fn:
|
if notify_fn:
|
||||||
try:
|
try:
|
||||||
notify_fn(f"{label}平仓 {sym} {direction} {lots}手 @{mark},已记入交易记录")
|
notify_fn(f"{result_label} {sym} {direction} {lots}手 @{mark},已记入交易记录")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.debug("SL/TP notify failed: %s", exc)
|
logger.debug("SL/TP notify failed: %s", exc)
|
||||||
|
|
||||||
@@ -377,6 +560,7 @@ def check_monitors_locally(
|
|||||||
*,
|
*,
|
||||||
capital: float = 0.0,
|
capital: float = 0.0,
|
||||||
notify_fn: Callable[[str], None] | None = None,
|
notify_fn: Callable[[str], None] | None = None,
|
||||||
|
be_tick_mult: int = 2,
|
||||||
) -> int:
|
) -> int:
|
||||||
"""扫描 active 监控,本地比对行情;触发止盈/止损(含跳空穿透)后立刻市价平仓并记交易记录。"""
|
"""扫描 active 监控,本地比对行情;触发止盈/止损(含跳空穿透)后立刻市价平仓并记交易记录。"""
|
||||||
ensure_monitor_order_columns(conn)
|
ensure_monitor_order_columns(conn)
|
||||||
@@ -417,6 +601,13 @@ def check_monitors_locally(
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
tick = _tick_size(sym)
|
tick = _tick_size(sym)
|
||||||
|
if mon.get("trailing_be"):
|
||||||
|
mon = _update_trailing_stop_loss(conn, mon, mark, be_tick_mult=be_tick_mult)
|
||||||
|
try:
|
||||||
|
sl_f = float(mon["stop_loss"]) if mon.get("stop_loss") is not None else sl_f
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
reason = None
|
reason = None
|
||||||
if tp_f is not None and _tp_triggered(direction, tp_f, mark, tick):
|
if tp_f is not None and _tp_triggered(direction, tp_f, mark, tick):
|
||||||
reason = "take_profit"
|
reason = "take_profit"
|
||||||
@@ -507,6 +698,7 @@ def start_sl_tp_guard_worker(
|
|||||||
get_mode_fn: Callable[[], str],
|
get_mode_fn: Callable[[], str],
|
||||||
init_tables_fn: Callable | None = None,
|
init_tables_fn: Callable | None = None,
|
||||||
get_capital_fn: Callable | None = None,
|
get_capital_fn: Callable | None = None,
|
||||||
|
get_be_tick_buffer_fn: Callable[[], int] | None = None,
|
||||||
notify_fn: Callable[[str], None] | None = None,
|
notify_fn: Callable[[str], None] | None = None,
|
||||||
interval: int = CHECK_INTERVAL_SEC,
|
interval: int = CHECK_INTERVAL_SEC,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -547,6 +739,9 @@ def start_sl_tp_guard_worker(
|
|||||||
mode,
|
mode,
|
||||||
capital=capital,
|
capital=capital,
|
||||||
notify_fn=notify_fn,
|
notify_fn=notify_fn,
|
||||||
|
be_tick_mult=(
|
||||||
|
get_be_tick_buffer_fn() if get_be_tick_buffer_fn else 2
|
||||||
|
),
|
||||||
)
|
)
|
||||||
if n:
|
if n:
|
||||||
logger.info("止盈止损本地监控: 触发平仓 %d 笔", n)
|
logger.info("止盈止损本地监控: 触发平仓 %d 笔", n)
|
||||||
|
|||||||
@@ -32,7 +32,11 @@
|
|||||||
.market-hint{font-size:.7rem;margin-top:.25rem}
|
.market-hint{font-size:.7rem;margin-top:.25rem}
|
||||||
.trade-action-row{display:flex;flex-direction:column;gap:.45rem;margin:.85rem 0 .55rem}
|
.trade-action-row{display:flex;flex-direction:column;gap:.45rem;margin:.85rem 0 .55rem}
|
||||||
.trade-action-row .btn-open{padding:.65rem .75rem;font-size:.9rem;width:100%}
|
.trade-action-row .btn-open{padding:.65rem .75rem;font-size:.9rem;width:100%}
|
||||||
.trade-action-row .btn-open:disabled{opacity:.65;cursor:wait}
|
.trade-action-row .btn-open:disabled{opacity:.45;cursor:not-allowed;filter:grayscale(.25)}
|
||||||
|
.trade-action-row .btn-open.btn-session-off{background:var(--text-muted);border-color:var(--text-muted)}
|
||||||
|
.trailing-be-toggle{display:flex;align-items:center;gap:.4rem;font-size:.78rem;color:var(--text-label);margin-bottom:.45rem;cursor:pointer;user-select:none}
|
||||||
|
.trailing-be-toggle input{width:auto;margin:0}
|
||||||
|
.session-hint{font-size:.72rem;margin:.35rem 0 0;text-align:center}
|
||||||
.trade-order-msg{font-size:.82rem;text-align:center;margin:0;padding:.35rem}
|
.trade-order-msg{font-size:.82rem;text-align:center;margin:0;padding:.35rem}
|
||||||
.trade-order-msg.ok{color:var(--profit)}
|
.trade-order-msg.ok{color:var(--profit)}
|
||||||
.trade-order-msg.err{color:var(--loss)}
|
.trade-order-msg.err{color:var(--loss)}
|
||||||
|
|||||||
+30
-3
@@ -116,6 +116,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateSessionUi() {
|
||||||
|
var btnOpen = document.getElementById('btn-open');
|
||||||
|
var sessionHint = document.getElementById('session-hint');
|
||||||
|
if (btnOpen) {
|
||||||
|
btnOpen.disabled = !isTradingSession;
|
||||||
|
btnOpen.classList.toggle('btn-session-off', !isTradingSession);
|
||||||
|
}
|
||||||
|
if (sessionHint) {
|
||||||
|
sessionHint.hidden = !!isTradingSession;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function entryPrice() {
|
function entryPrice() {
|
||||||
if (priceType === 'market') return lastQuotePrice;
|
if (priceType === 'market') return lastQuotePrice;
|
||||||
return parseFloat(priceInput && priceInput.value) || 0;
|
return parseFloat(priceInput && priceInput.value) || 0;
|
||||||
@@ -330,7 +342,17 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var lots = effectiveLots();
|
var lots = effectiveLots();
|
||||||
|
var trailingBeEl = document.getElementById('trailing-be');
|
||||||
if (offset === 'open') {
|
if (offset === 'open') {
|
||||||
|
if (!isTradingSession) {
|
||||||
|
showOrderMsg('不在交易时间段', false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var trailingOn = !!(trailingBeEl && trailingBeEl.checked);
|
||||||
|
if (trailingOn && !(slInput && slInput.value)) {
|
||||||
|
showOrderMsg('开启移动保本须填写止损价', false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (isRiskMode() && lots <= 0) {
|
if (isRiskMode() && lots <= 0) {
|
||||||
showOrderMsg('请填写止损,系统将自动计算手数', false);
|
showOrderMsg('请填写止损,系统将自动计算手数', false);
|
||||||
return;
|
return;
|
||||||
@@ -359,7 +381,8 @@
|
|||||||
price: price,
|
price: price,
|
||||||
order_type: priceType,
|
order_type: priceType,
|
||||||
stop_loss: slInput && slInput.value ? parseFloat(slInput.value) : null,
|
stop_loss: slInput && slInput.value ? parseFloat(slInput.value) : null,
|
||||||
take_profit: tpInput && tpInput.value ? parseFloat(tpInput.value) : null
|
take_profit: tpInput && tpInput.value ? parseFloat(tpInput.value) : null,
|
||||||
|
trailing_be: !!(trailingBeEl && trailingBeEl.checked)
|
||||||
};
|
};
|
||||||
fetch('/api/trade/order', {
|
fetch('/api/trade/order', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -379,8 +402,8 @@
|
|||||||
showOrderMsg('网络错误,请重试', false);
|
showOrderMsg('网络错误,请重试', false);
|
||||||
}).finally(function () {
|
}).finally(function () {
|
||||||
if (btnOpen) {
|
if (btnOpen) {
|
||||||
btnOpen.disabled = false;
|
|
||||||
btnOpen.textContent = '开仓';
|
btnOpen.textContent = '开仓';
|
||||||
|
updateSessionUi();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -471,7 +494,9 @@
|
|||||||
'<div class="pos-card-meta">来源 <strong>' + (row.source_label || 'CTP') + '</strong> · 柜台浮盈' +
|
'<div class="pos-card-meta">来源 <strong>' + (row.source_label || 'CTP') + '</strong> · 柜台浮盈' +
|
||||||
(slTpBtn ? ' · ' + slTpBtn : '') +
|
(slTpBtn ? ' · ' + slTpBtn : '') +
|
||||||
(row.sl_order_active ? ' · <span class="text-profit">止损监控中</span>' : '') +
|
(row.sl_order_active ? ' · <span class="text-profit">止损监控中</span>' : '') +
|
||||||
(row.tp_order_active ? ' · <span class="text-profit">止盈监控中</span>' : '') + '</div>' +
|
(row.tp_order_active ? ' · <span class="text-profit">止盈监控中</span>' : '') +
|
||||||
|
(row.trailing_be ? ' · <span class="text-accent">移动保本' +
|
||||||
|
(row.trailing_r_locked ? '(锁' + row.trailing_r_locked + 'R)' : '') + '</span>' : '') + '</div>' +
|
||||||
'<div class="pos-metrics">' +
|
'<div class="pos-metrics">' +
|
||||||
'<div class="cell"><label>持仓均价</label><div>' + fmtNum(row.entry_price) + '</div></div>' +
|
'<div class="cell"><label>持仓均价</label><div>' + fmtNum(row.entry_price) + '</div></div>' +
|
||||||
'<div class="cell"><label>当前价格</label><div>' + (row.current_price != null ? fmtNum(row.current_price) : '--') + '</div></div>' +
|
'<div class="cell"><label>当前价格</label><div>' + (row.current_price != null ? fmtNum(row.current_price) : '--') + '</div></div>' +
|
||||||
@@ -646,6 +671,7 @@
|
|||||||
return row.stop_loss != null || row.take_profit != null;
|
return row.stop_loss != null || row.take_profit != null;
|
||||||
});
|
});
|
||||||
schedulePositionPoll();
|
schedulePositionPoll();
|
||||||
|
updateSessionUi();
|
||||||
if (!connected) {
|
if (!connected) {
|
||||||
if (connecting) {
|
if (connecting) {
|
||||||
list.innerHTML = '<div class="empty-hint">CTP 连接中,请稍候…</div>';
|
list.innerHTML = '<div class="empty-hint">CTP 连接中,请稍候…</div>';
|
||||||
@@ -794,6 +820,7 @@
|
|||||||
document.addEventListener('visibilitychange', function () {
|
document.addEventListener('visibilitychange', function () {
|
||||||
if (document.visibilityState === 'visible') pollPositions();
|
if (document.visibilityState === 'visible') pollPositions();
|
||||||
});
|
});
|
||||||
|
updateSessionUi();
|
||||||
scheduleQuote();
|
scheduleQuote();
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -84,6 +84,8 @@
|
|||||||
<span class="cell-readonly cell-edit-hide">
|
<span class="cell-readonly cell-edit-hide">
|
||||||
{% if t.result == '止盈' %}<span class="badge profit">{{ t.result }}</span>
|
{% if t.result == '止盈' %}<span class="badge profit">{{ t.result }}</span>
|
||||||
{% elif t.result == '止损' %}<span class="badge loss">{{ t.result }}</span>
|
{% elif t.result == '止损' %}<span class="badge loss">{{ t.result }}</span>
|
||||||
|
{% elif t.result == '移动止盈' %}<span class="badge profit">{{ t.result }}</span>
|
||||||
|
{% elif t.result == '保本止盈' %}<span class="badge profit">{{ t.result }}</span>
|
||||||
{% elif t.result == '手动平仓' %}<span class="badge result-manual">{{ t.result }}</span>
|
{% elif t.result == '手动平仓' %}<span class="badge result-manual">{{ t.result }}</span>
|
||||||
{% else %}<span class="badge result-external">{{ t.result }}</span>{% endif %}
|
{% else %}<span class="badge result-external">{{ t.result }}</span>{% endif %}
|
||||||
{% if t.verified %}<span class="badge active" style="margin-left:.25rem">已核对</span>{% endif %}
|
{% if t.verified %}<span class="badge active" style="margin-left:.25rem">已核对</span>{% endif %}
|
||||||
@@ -92,8 +94,8 @@
|
|||||||
<option value="手动平仓" {% if t.result=='手动平仓' %}selected{% endif %}>手动平仓</option>
|
<option value="手动平仓" {% if t.result=='手动平仓' %}selected{% endif %}>手动平仓</option>
|
||||||
<option value="止盈" {% if t.result=='止盈' %}selected{% endif %}>止盈</option>
|
<option value="止盈" {% if t.result=='止盈' %}selected{% endif %}>止盈</option>
|
||||||
<option value="止损" {% if t.result=='止损' %}selected{% endif %}>止损</option>
|
<option value="止损" {% if t.result=='止损' %}selected{% endif %}>止损</option>
|
||||||
<option value="外部平仓" {% if t.result=='外部平仓' %}selected{% endif %}>外部平仓</option>
|
<option value="移动止盈" {% if t.result=='移动止盈' %}selected{% endif %}>移动止盈</option>
|
||||||
<option value="时间平仓" {% if t.result=='时间平仓' %}selected{% endif %}>时间平仓</option>
|
<option value="保本止盈" {% if t.result=='保本止盈' %}selected{% endif %}>保本止盈</option>
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -67,10 +67,14 @@
|
|||||||
<label>保证金占用上限(%)</label>
|
<label>保证金占用上限(%)</label>
|
||||||
<input name="max_margin_pct" type="number" step="1" min="1" max="100" value="{{ max_margin_pct }}">
|
<input name="max_margin_pct" type="number" step="1" min="1" max="100" value="{{ max_margin_pct }}">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>移动保本缓冲(最小变动价位倍数)</label>
|
||||||
|
<input name="trailing_be_tick_buffer" type="number" step="1" min="1" max="20" value="{{ trailing_be_tick_buffer }}">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn-primary" style="margin-top:.75rem">保存交易设置</button>
|
<button type="submit" class="btn-primary" style="margin-top:.75rem">保存交易设置</button>
|
||||||
<p class="hint" style="margin-top:.75rem;margin-bottom:0">
|
<p class="hint" style="margin-top:.75rem;margin-bottom:0">
|
||||||
保证金上限用于开仓校验与品种最大手数估算(默认 30%)。在 <code>.env</code> 配置 <code>SIMNOW_USER</code>,于「持仓监控」连接 CTP;权益与行情优先来自柜台。
|
保证金上限用于开仓校验与品种最大手数估算(默认 30%)。<strong>移动保本</strong>:达 1R 后止损移至开仓价 ± N 跳(玉米 N=2 即 +2 点,棉花 N=2 即 +10 点);达 2R 移至 1R,依次类推。在 <code>.env</code> 配置 <code>SIMNOW_USER</code>,于「持仓监控」连接 CTP。
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -81,7 +81,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="trade-action-row">
|
<div class="trade-action-row">
|
||||||
|
<label class="trailing-be-toggle">
|
||||||
|
<input type="checkbox" id="trailing-be" checked>
|
||||||
|
<span>移动保本</span>
|
||||||
|
</label>
|
||||||
<button type="button" class="btn-primary btn-open" id="btn-open">开仓</button>
|
<button type="button" class="btn-primary btn-open" id="btn-open">开仓</button>
|
||||||
|
<p class="hint session-hint text-muted" id="session-hint" hidden>不在交易时间段</p>
|
||||||
<p class="trade-order-msg" id="order-msg" hidden></p>
|
<p class="trade-order-msg" id="order-msg" hidden></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,14 @@ def get_max_margin_pct(get_setting: Callable[[str, str], str]) -> float:
|
|||||||
return 30.0
|
return 30.0
|
||||||
|
|
||||||
|
|
||||||
|
def get_trailing_be_tick_buffer(get_setting: Callable[[str, str], str]) -> int:
|
||||||
|
"""移动保本:止损移至开仓价 ± N 个最小变动价位(默认 2)。"""
|
||||||
|
try:
|
||||||
|
return max(1, min(20, int(float(get_setting("trailing_be_tick_buffer", "2") or 2))))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return 2
|
||||||
|
|
||||||
|
|
||||||
def get_account_capital(conn, get_setting: Callable[[str, str], str]) -> float:
|
def get_account_capital(conn, get_setting: Callable[[str, str], str]) -> float:
|
||||||
"""优先 SimNow/期货公司 CTP 权益;未连接时用设置中的参考资金。"""
|
"""优先 SimNow/期货公司 CTP 权益;未连接时用设置中的参考资金。"""
|
||||||
del conn
|
del conn
|
||||||
|
|||||||
Reference in New Issue
Block a user