删除永续下单监控与划转强平,保留关键位监控并完成回归审计。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,79 @@
|
|||||||
|
# 永续删除 / 关键位保留 — 审计报告(2026-10-07)
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
- **保留**:关键位监控(`key_monitor` 页面、`/key_*` 与 `/api/key_*` 路由、`check_key_monitors()`、`lib/key_monitor/*`、相关 DB 表)。
|
||||||
|
- **删除**:永续下单/平仓、下单监控 CRUD、自动划转/强平、仅服务于永续的 lib 与模板;期权与 hub bridge 路径不受影响。
|
||||||
|
|
||||||
|
## 已删除或下线的能力
|
||||||
|
|
||||||
|
### app.py
|
||||||
|
|
||||||
|
- 移除:`check_order_monitors`、`force_close_before_reset`、`auto_transfer_once_per_day`、`reconcile_external_close(s)`、OKX 永续开平仓与 TP/SL 同步大段逻辑。
|
||||||
|
- `place_exchange_order` / `close_exchange_order` **已物理删除**(调用方与 sim 包装一并移除)。
|
||||||
|
- `background_task()` 仅调用 `check_key_monitors()`;`if __name__` 下恢复 `threading.Thread(..., daemon=True).start()`。
|
||||||
|
- `/api/price_snapshot`:`order_prices` 恒为 `[]`,不再查询 `order_monitors`。
|
||||||
|
- `render_main_page`:`order_list` 恒为 `[]`;开仓门禁改为本地 stub(`can_trade=False`)。
|
||||||
|
- 永续相关路由改为 `redirect('/options')` 或 410 JSON:`/add_order`、`/del_order`、`/order_focus`、`/api/order_*`、`/sync_positions`、`/manual_transfer` 等。
|
||||||
|
|
||||||
|
### 删除的文件(lib / templates)
|
||||||
|
|
||||||
|
| 路径 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `lib/trade/force_close_lib.py` | 强平 UI/逻辑 |
|
||||||
|
| `lib/trade/open_trade_gate_lib.py` | 永续开仓门禁 |
|
||||||
|
| `lib/trade/daily_open_limit_lib.py` | 日开仓上限 |
|
||||||
|
| `lib/trade/order_monitor_display_lib.py` | 下单监控展示 |
|
||||||
|
| `lib/trade/manual_sltp_lib.py` | 手动 TP/SL |
|
||||||
|
| `lib/trade/position_sizing_lib.py` | 仓位 sizing |
|
||||||
|
| `lib/trade/trade_fee_lib.py` | 手续费净盈亏 |
|
||||||
|
| `lib/trade/trade_exchange_stats_lib.py` | 成交统计回填 |
|
||||||
|
| `lib/trade/trade_stats_calendar_lib.py` | 日历统计 |
|
||||||
|
| `lib/trade/compensating_close_lib.py` | 补偿平仓 |
|
||||||
|
| `lib/common/auto_transfer_daily_lib.py` | 日自动划转 |
|
||||||
|
| `lib/exchange/gate_transfer_lib.py` | Gate 划转 |
|
||||||
|
| `lib/exchange/okx_orders_lib.py` | OKX 永续挂单查询 |
|
||||||
|
| `lib/market/reconcile_flat_lib.py` | 外部平仓对账 |
|
||||||
|
| `order_monitor_*`、`force_close_*`、`instance_transfer_panel.html`、`gate_transfer_block.html` | 永续 UI 模板 |
|
||||||
|
| `order_focus_v2.html`、`order_plan_preview_bar.html`、`order_leverage_fields.html`、`order_entry_model_fields.html`、`templates/order_focus.html` | 永续下单/聚焦页残留模板 |
|
||||||
|
|
||||||
|
### 保留的 trade 模块
|
||||||
|
|
||||||
|
- `trade_policy_lib.py`、`trade_policy_app_lib.py`、`time_close_lib.py`
|
||||||
|
- `trade_labels_lib.py`、`trade_result_lib.py`(新增 `snapshot_stop_loss`)、`entry_model_lib.py`、`account_risk_lib.py`(日记/风控仍用)
|
||||||
|
|
||||||
|
## Sim / Options
|
||||||
|
|
||||||
|
- `lib/sim/hooks.py`:移除对 `place_exchange_order` / `close_exchange_order` 的包装;保留期权/现货桥/钱包 sim 路径。
|
||||||
|
- `lib/sim/register.py`:不再向 options cfg 注入永续下单函数。
|
||||||
|
|
||||||
|
## 测试
|
||||||
|
|
||||||
|
在项目根目录执行:
|
||||||
|
|
||||||
|
```text
|
||||||
|
python -m unittest tests.test_spot_dust_and_sell_cap tests.test_options_partial_close_accum tests.test_options_profit_exit_recycle tests.test_options_btc_pnl_usdt tests.test_hub_bridge_auth -v
|
||||||
|
```
|
||||||
|
|
||||||
|
**结果**:
|
||||||
|
|
||||||
|
| 批次 | 用例数 | 结果 |
|
||||||
|
|------|--------|------|
|
||||||
|
| 期权粉尘/卖回封顶、部分平仓、翻倍出场、BTC 盈亏、hub auth、去对冲冒烟 | 22 | **OK**(2026-10-07) |
|
||||||
|
|
||||||
|
`python -c "import app"` 可导入(`APP_AUTH_DISABLED=true`)。
|
||||||
|
|
||||||
|
## 残留风险与后续可选清理
|
||||||
|
|
||||||
|
1. **DB schema**:`order_monitors` 等表仍在 `init_db()` 中创建,历史数据未迁移;仅不再读写业务逻辑。
|
||||||
|
2. **模板/静态**:`index.html` 等仍可能含永续 UI 片段(无后端路由时多为死链);可按 display_prefs 再收一层 UI。
|
||||||
|
3. **统计口径**:`compute_stats_bundle` 仍可能统计历史 `order_monitors` 开仓次数;与「零 active 监控」展示一致,但不等于删表。
|
||||||
|
4. **设置页**:「transfer」子页改为说明文案;期权/模拟划转仍在各自模块。
|
||||||
|
5. **account_risk / entry_model / trade_labels**:日记与设置仍引用,未整包删除。
|
||||||
|
|
||||||
|
## 关键位验证建议(手工)
|
||||||
|
|
||||||
|
1. 添加 BTC/ETH 关键支撑阻力 → `/key_monitor` 列表可见。
|
||||||
|
2. 观察后台线程日志无 `[key_monitor_loop]` 以外永续轮询错误。
|
||||||
|
3. `/api/price_snapshot` 返回 `key_prices` 非空(有关键位时)、`order_prices: []`。
|
||||||
|
4. 微信提醒依赖环境变量与行情;需在实盘/模拟各测一次 RS 突破通知(若已配置 webhook)。
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
"""
|
|
||||||
每日自动划转:北京时间指定整点小时内,将交易账户(AUTO_TRANSFER_TO)余额调整至目标额.
|
|
||||||
|
|
||||||
- 交易账户 < 目标:从资金账户划入差额
|
|
||||||
- 交易账户 > 目标:将多余划回资金账户
|
|
||||||
- 有 active 持仓:不划转,写账簿并企业微信说明
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any, Callable
|
|
||||||
|
|
||||||
|
|
||||||
def run_auto_transfer_once_per_day(
|
|
||||||
*,
|
|
||||||
enabled: bool,
|
|
||||||
bj_hour: int,
|
|
||||||
target_amount: float,
|
|
||||||
from_account: str,
|
|
||||||
to_account: str,
|
|
||||||
funds_decimals: int,
|
|
||||||
get_db: Callable[[], Any],
|
|
||||||
get_active_position_count: Callable[[Any], int],
|
|
||||||
get_account_usdt_total: Callable[[str], float | None],
|
|
||||||
execute_transfer_usdt: Callable[[float, str, str], tuple[bool, str, Any]],
|
|
||||||
send_wechat_msg: Callable[[str], None],
|
|
||||||
utc_now_dt: Callable[[], Any],
|
|
||||||
app_tz: Any,
|
|
||||||
utc_calendar_date_str: Callable[[], str],
|
|
||||||
app_now_str: Callable[[], str],
|
|
||||||
min_transfer: float = 0.01,
|
|
||||||
) -> None:
|
|
||||||
if not enabled:
|
|
||||||
return
|
|
||||||
utc_dt = utc_now_dt()
|
|
||||||
bj = utc_dt.astimezone(app_tz)
|
|
||||||
if bj.hour != bj_hour:
|
|
||||||
return
|
|
||||||
|
|
||||||
transfer_day = utc_calendar_date_str()
|
|
||||||
conn = get_db()
|
|
||||||
exists = conn.execute(
|
|
||||||
"SELECT id FROM transfer_logs WHERE transfer_type=? AND transfer_day=?",
|
|
||||||
("auto_daily", transfer_day),
|
|
||||||
).fetchone()
|
|
||||||
if exists:
|
|
||||||
conn.close()
|
|
||||||
return
|
|
||||||
|
|
||||||
def _log(
|
|
||||||
amount: float,
|
|
||||||
fr: str,
|
|
||||||
to: str,
|
|
||||||
status: str,
|
|
||||||
message: str,
|
|
||||||
*,
|
|
||||||
commit_close: bool = True,
|
|
||||||
) -> None:
|
|
||||||
conn.execute(
|
|
||||||
"INSERT INTO transfer_logs (transfer_type, transfer_day, amount, from_account, to_account, status, message) VALUES (?,?,?,?,?,?,?)",
|
|
||||||
("auto_daily", transfer_day, amount, fr, to, status, message[:500]),
|
|
||||||
)
|
|
||||||
conn.commit()
|
|
||||||
if commit_close:
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
active = get_active_position_count(conn)
|
|
||||||
if active > 0:
|
|
||||||
msg = f"持仓中({active}笔),本次资金无划转"
|
|
||||||
_log(0, from_account, to_account, "skipped", msg)
|
|
||||||
send_wechat_msg(
|
|
||||||
f"自动划转:{msg}\n"
|
|
||||||
f"目标:{to_account} 调整至 {round(float(target_amount), funds_decimals)}U\n"
|
|
||||||
f"账簿日(UTC):{transfer_day}|触发时刻(北京):{app_now_str()}"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
target = round(float(target_amount), funds_decimals)
|
|
||||||
trade_bal = get_account_usdt_total(to_account)
|
|
||||||
if trade_bal is None:
|
|
||||||
_log(
|
|
||||||
0,
|
|
||||||
from_account,
|
|
||||||
to_account,
|
|
||||||
"failed",
|
|
||||||
f"读取{to_account}账户USDT失败",
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
trade = round(float(trade_bal), funds_decimals)
|
|
||||||
diff = round(target - trade, funds_decimals)
|
|
||||||
|
|
||||||
if abs(diff) < min_transfer:
|
|
||||||
_log(
|
|
||||||
0,
|
|
||||||
from_account,
|
|
||||||
to_account,
|
|
||||||
"skipped",
|
|
||||||
f"{to_account}账户已为{trade}U(目标{target}U)",
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
if diff > 0:
|
|
||||||
fr, to, amount = from_account, to_account, diff
|
|
||||||
action = "划入"
|
|
||||||
else:
|
|
||||||
fr, to, amount = to_account, from_account, round(abs(diff), funds_decimals)
|
|
||||||
action = "划出"
|
|
||||||
|
|
||||||
from_bal = get_account_usdt_total(fr)
|
|
||||||
if from_bal is not None and round(float(from_bal), funds_decimals) < amount:
|
|
||||||
cur = round(float(from_bal), funds_decimals)
|
|
||||||
_log(amount, fr, to, "failed", f"{fr}账户USDT不足,需{amount}U,当前{cur}U")
|
|
||||||
send_wechat_msg(
|
|
||||||
f"自动划转失败:{fr}余额不足,需{amount}U,当前{cur}U({action}至{to_account}目标{target}U)\n"
|
|
||||||
f"账簿日(UTC):{transfer_day}|触发时刻(北京):{app_now_str()}"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
ok, msg, _ = execute_transfer_usdt(amount, fr, to)
|
|
||||||
_log(amount, fr, to, "success" if ok else "failed", msg)
|
|
||||||
if ok:
|
|
||||||
send_wechat_msg(
|
|
||||||
f"自动划转成功:{to_account} {trade}U→目标{target}U,{action}{amount}U {fr}->{to}\n"
|
|
||||||
f"账簿日(UTC):{transfer_day}|触发时刻(北京):{app_now_str()}"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
send_wechat_msg(
|
|
||||||
f"自动划转失败:计划{action}{amount}U {fr}->{to}(目标{target}U)\n原因:{msg}\n"
|
|
||||||
f"账簿日(UTC):{transfer_day}|触发时刻(北京):{app_now_str()}"
|
|
||||||
)
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
"""Gate.io 资金划转(crypto_monitor_gate 共用)."""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any, Callable, Optional
|
|
||||||
|
|
||||||
INVALID_KEY_HINT = (
|
|
||||||
".常见原因:① GATE_API_SECRET 错误或 .env 里多了空格/换行;② IP 白名单未包含当前服务器出口 IP;"
|
|
||||||
"③ Gate「交易账户」类 API Key 若不支持钱包接口则无法走账户内划转 POST /wallet/transfers(需在官网确认该 Key 类型是否开放划转);"
|
|
||||||
"④ Key 已重置或权限变更.你已勾选现货/统一账户仍报错时,优先核对 Secret 与白名单."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def execute_transfer_usdt(
|
|
||||||
exchange,
|
|
||||||
amount: float,
|
|
||||||
from_account: str,
|
|
||||||
to_account: str,
|
|
||||||
*,
|
|
||||||
transfer_ccy: str = "USDT",
|
|
||||||
ensure_live_ready: Callable[[], tuple[bool, str]],
|
|
||||||
ensure_markets_loaded: Optional[Callable[[], None]] = None,
|
|
||||||
) -> tuple[bool, str, Any]:
|
|
||||||
if amount <= 0:
|
|
||||||
return False, "划转金额必须大于0", None
|
|
||||||
ccy = (transfer_ccy or "USDT").strip().upper() or "USDT"
|
|
||||||
ok_live, reason = ensure_live_ready()
|
|
||||||
if not ok_live:
|
|
||||||
return False, reason, None
|
|
||||||
if ensure_markets_loaded:
|
|
||||||
try:
|
|
||||||
ensure_markets_loaded()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
resp = exchange.transfer(ccy, float(amount), from_account, to_account)
|
|
||||||
return True, "划转成功", resp
|
|
||||||
except Exception as e:
|
|
||||||
msg = str(e)
|
|
||||||
if "INVALID_KEY" in msg or "Invalid key" in msg:
|
|
||||||
msg += INVALID_KEY_HINT
|
|
||||||
return False, msg, None
|
|
||||||
|
|
||||||
|
|
||||||
def count_auto_transfer_blockers(conn, *, count_order_monitors: Callable[[Any], int]) -> int:
|
|
||||||
"""自动划转持仓守卫:order_monitors active + 趋势回调已开仓计划."""
|
|
||||||
n = int(count_order_monitors(conn) or 0)
|
|
||||||
if n > 0:
|
|
||||||
return n
|
|
||||||
try:
|
|
||||||
row = conn.execute(
|
|
||||||
"SELECT COUNT(*) FROM trend_pullback_plans "
|
|
||||||
"WHERE status='active' AND COALESCE(first_order_done, 0) != 0"
|
|
||||||
).fetchone()
|
|
||||||
return int(row[0] or 0) if row else 0
|
|
||||||
except Exception:
|
|
||||||
return n
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
"""
|
|
||||||
OKX 挂单聚合:普通委托 + 算法单(conditional / oco / trigger).
|
|
||||||
交易所 App「止盈止损」页多为 orders-algo-pending,仅 fetch_open_orders 默认拿不到.
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
def _order_dedupe_key(order: dict) -> str:
|
|
||||||
info = order.get("info") or {}
|
|
||||||
if not isinstance(info, dict):
|
|
||||||
info = {}
|
|
||||||
return str(order.get("id") or info.get("algoId") or info.get("ordId") or "")
|
|
||||||
|
|
||||||
|
|
||||||
def _okx_algo_cancel_id(order_id: str) -> str:
|
|
||||||
oid = str(order_id or "")
|
|
||||||
if ":" in oid:
|
|
||||||
return oid.split(":", 1)[0]
|
|
||||||
return oid
|
|
||||||
|
|
||||||
|
|
||||||
def _okx_order_needs_stop_cancel_param(order: dict) -> bool:
|
|
||||||
"""OKX 条件/算法单撤单须 params.stop=True,否则 cancel_order 走普通单接口会静默失败."""
|
|
||||||
if not isinstance(order, dict):
|
|
||||||
return False
|
|
||||||
info = order.get("info") or {}
|
|
||||||
if not isinstance(info, dict):
|
|
||||||
info = {}
|
|
||||||
if order.get("stopLossPrice") is not None or order.get("takeProfitPrice") is not None:
|
|
||||||
return True
|
|
||||||
if info.get("algoId") or info.get("slTriggerPx") or info.get("tpTriggerPx"):
|
|
||||||
return True
|
|
||||||
typ = str(order.get("type") or info.get("ordType") or "").lower()
|
|
||||||
for token in ("conditional", "oco", "trigger", "move_order_stop", "iceberg"):
|
|
||||||
if token in typ:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_okx_all_open_orders(ex, exchange_symbol: str) -> list[dict]:
|
|
||||||
"""合并 OKX 普通挂单与算法挂单(去重)."""
|
|
||||||
if not exchange_symbol:
|
|
||||||
return []
|
|
||||||
ex.load_markets()
|
|
||||||
sym = exchange_symbol
|
|
||||||
try:
|
|
||||||
sym = ex.market(exchange_symbol)["symbol"]
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
seen: set[str] = set()
|
|
||||||
out: list[dict] = []
|
|
||||||
|
|
||||||
def add_batch(batch: list | None) -> None:
|
|
||||||
for o in batch or []:
|
|
||||||
if not isinstance(o, dict):
|
|
||||||
continue
|
|
||||||
k = _order_dedupe_key(o)
|
|
||||||
if not k or k in seen:
|
|
||||||
continue
|
|
||||||
seen.add(k)
|
|
||||||
out.append(o)
|
|
||||||
|
|
||||||
try:
|
|
||||||
add_batch(ex.fetch_open_orders(sym))
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
for params in (
|
|
||||||
{"ordType": "conditional"},
|
|
||||||
{"ordType": "oco"},
|
|
||||||
{"trigger": True},
|
|
||||||
):
|
|
||||||
try:
|
|
||||||
add_batch(ex.fetch_open_orders(sym, params=dict(params)))
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def cancel_okx_all_open_orders(ex, exchange_symbol: str) -> int:
|
|
||||||
"""
|
|
||||||
撤销某合约全部挂单(普通 + 条件/算法).
|
|
||||||
OKX 止盈止损在 orders-algo-pending,必须用 stop=True 才能撤掉.
|
|
||||||
"""
|
|
||||||
if not exchange_symbol:
|
|
||||||
return 0
|
|
||||||
ex.load_markets()
|
|
||||||
sym = exchange_symbol
|
|
||||||
try:
|
|
||||||
sym = ex.market(exchange_symbol)["symbol"]
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
n = 0
|
|
||||||
for o in fetch_okx_all_open_orders(ex, sym):
|
|
||||||
oid = _order_dedupe_key(o)
|
|
||||||
if not oid:
|
|
||||||
continue
|
|
||||||
cancel_id = _okx_algo_cancel_id(oid)
|
|
||||||
params = {"stop": True} if _okx_order_needs_stop_cancel_param(o) else None
|
|
||||||
try:
|
|
||||||
ex.cancel_order(cancel_id, sym, params)
|
|
||||||
n += 1
|
|
||||||
continue
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
ex.cancel_order(oid, sym, params)
|
|
||||||
n += 1
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
ex.cancel_all_orders(sym)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return n
|
|
||||||
@@ -8,10 +8,12 @@ from lib.market.ohlcv_lib import (
|
|||||||
price_tick_from_market,
|
price_tick_from_market,
|
||||||
round_ohlcv_bars_to_tick,
|
round_ohlcv_bars_to_tick,
|
||||||
)
|
)
|
||||||
from lib.trade.order_monitor_display_lib import (
|
def apply_order_price_display_fields(payload, **kwargs):
|
||||||
apply_order_live_price_display,
|
return payload
|
||||||
apply_order_price_display_fields,
|
|
||||||
)
|
|
||||||
|
def apply_order_live_price_display(payload, *args, **kwargs):
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
def resolve_kline_price_tick(
|
def resolve_kline_price_tick(
|
||||||
|
|||||||
@@ -237,12 +237,8 @@ def _table_exists(conn, name: str) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def collect_orders(conn) -> list[dict[str, Any]]:
|
def collect_orders(conn) -> list[dict[str, Any]]:
|
||||||
if not _table_exists(conn, "order_monitors"):
|
del conn
|
||||||
return []
|
return []
|
||||||
rows = conn.execute(
|
|
||||||
"SELECT * FROM order_monitors WHERE status='active' ORDER BY id DESC"
|
|
||||||
).fetchall()
|
|
||||||
return [_format_order_item(_row_dict(r)) for r in rows]
|
|
||||||
|
|
||||||
|
|
||||||
def collect_keys(conn) -> list[dict[str, Any]]:
|
def collect_keys(conn) -> list[dict[str, Any]]:
|
||||||
@@ -265,17 +261,8 @@ def collect_trends(conn) -> list[dict[str, Any]]:
|
|||||||
|
|
||||||
|
|
||||||
def collect_rolls(conn) -> list[dict[str, Any]]:
|
def collect_rolls(conn) -> list[dict[str, Any]]:
|
||||||
if not _table_exists(conn, "roll_groups") or not _table_exists(conn, "order_monitors"):
|
del conn
|
||||||
return []
|
return []
|
||||||
try:
|
|
||||||
rows = 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()
|
|
||||||
except Exception:
|
|
||||||
return []
|
|
||||||
return [_format_roll_item(_row_dict(r)) for r in rows]
|
|
||||||
|
|
||||||
|
|
||||||
def collect_options_items(
|
def collect_options_items(
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ DISPLAY_RUNTIME_PREFIX = "display."
|
|||||||
DEFAULT_INSTANCE_DISPLAY: dict[str, bool] = {
|
DEFAULT_INSTANCE_DISPLAY: dict[str, bool] = {
|
||||||
"show_nav_dashboard": False,
|
"show_nav_dashboard": False,
|
||||||
"show_nav_account_ledger": False,
|
"show_nav_account_ledger": False,
|
||||||
"show_nav_key_monitor": False, # 永续关键位已下线
|
"show_nav_key_monitor": True, # 关键位监控保留
|
||||||
"show_nav_trade": False, # 永续实盘下单已下线
|
"show_nav_trade": False, # 永续实盘下单已下线
|
||||||
"show_nav_strategy": False,
|
"show_nav_strategy": False,
|
||||||
"show_nav_strategy_records": False,
|
"show_nav_strategy_records": False,
|
||||||
@@ -111,8 +111,8 @@ def display_prefs_template_context(get_db: Callable) -> dict[str, Any]:
|
|||||||
def tab_allowed(tab: str, display: Optional[dict[str, bool]] = None) -> bool:
|
def tab_allowed(tab: str, display: Optional[dict[str, bool]] = None) -> bool:
|
||||||
prefs = normalize_display_prefs(display or {})
|
prefs = normalize_display_prefs(display or {})
|
||||||
t = (tab or "").strip()
|
t = (tab or "").strip()
|
||||||
if t in ("trade", "records", "stats", "hedge_plan", "key_monitor"):
|
if t in ("trade", "records", "stats", "hedge_plan"):
|
||||||
return False # 永续相关页面已下线
|
return False # 永续下单/记录/统计已下线;关键位监控保留
|
||||||
key = NAV_TAB_ALLOWED.get(t)
|
key = NAV_TAB_ALLOWED.get(t)
|
||||||
if not key:
|
if not key:
|
||||||
return True
|
return True
|
||||||
@@ -123,6 +123,7 @@ def display_meta_for_ui() -> list[dict[str, Any]]:
|
|||||||
nav_keys = [
|
nav_keys = [
|
||||||
"show_nav_dashboard",
|
"show_nav_dashboard",
|
||||||
"show_nav_account_ledger",
|
"show_nav_account_ledger",
|
||||||
|
"show_nav_key_monitor",
|
||||||
"show_nav_risk_policy",
|
"show_nav_risk_policy",
|
||||||
"show_nav_live_guide",
|
"show_nav_live_guide",
|
||||||
"show_nav_system_guide",
|
"show_nav_system_guide",
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
{% if force_close.enabled %}
|
|
||||||
<span class="force-close-badge" id="force-close-header-badge" role="status"
|
|
||||||
title="北京时间 {{ force_close.hour_label }} 整点未平仓将市价强制清仓(result=强制清仓)"
|
|
||||||
data-force-close-at-ms="{{ force_close.next_at_ms or '' }}"
|
|
||||||
data-force-close-active="{{ '1' if force_close.active else '0' }}">
|
|
||||||
{{ force_close.label }} 已开启 · <span class="force-close-header-cd">{{ force_close.countdown or '--:--:--' }}</span>
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{% if force_close.enabled %}
|
|
||||||
<span class="pos-symbol-force-close pos-force-close-meta" id="order-force-close-wrap-{{ o.id }}"
|
|
||||||
data-force-close-at-ms="{{ o.force_close_at_ms or force_close.next_at_ms or '' }}"
|
|
||||||
data-force-close-active="{{ '1' if (o.force_close_active or force_close.active) else '0' }}">
|
|
||||||
<span class="pos-force-close-label">{{ o.force_close_label or force_close.label }}</span>
|
|
||||||
· <span class="pos-force-close-cd" id="order-force-close-cd-{{ o.id }}">{{ o.force_close_countdown or force_close.countdown or '--:--:--' }}</span>
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<details class="tip-collapse gate-top-tips-collapse">
|
|
||||||
<summary class="tip-collapse-summary">
|
|
||||||
实时价格更新:<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>并微信通知)
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
<form action="/manual_transfer" method="post" class="form-row gate-transfer-form" autocomplete="off">
|
|
||||||
<input type="text" name="username" autocomplete="username" tabindex="-1" aria-hidden="true"
|
|
||||||
style="position:absolute;left:-9999px;width:1px;height:1px;opacity:0" value="">
|
|
||||||
<input name="amount" type="number" min="0.01" step="0.01" placeholder="手动划转金额U" required
|
|
||||||
autocomplete="off" inputmode="decimal" data-lpignore="true" data-1p-ignore="true" data-bwignore="true" data-form-type="other" readonly>
|
|
||||||
<select name="from_account" autocomplete="off">
|
|
||||||
<option value="funding" {% if auto_transfer_from == 'funding' %}selected{% endif %}>from: funding</option>
|
|
||||||
<option value="swap" {% if auto_transfer_from == 'swap' %}selected{% endif %}>from: swap</option>
|
|
||||||
<option value="spot" {% if auto_transfer_from == 'spot' %}selected{% endif %}>from: spot</option>
|
|
||||||
</select>
|
|
||||||
<select name="to_account" autocomplete="off">
|
|
||||||
<option value="swap" {% if auto_transfer_to == 'swap' %}selected{% endif %}>to: swap</option>
|
|
||||||
<option value="funding" {% if auto_transfer_to == 'funding' %}selected{% endif %}>to: funding</option>
|
|
||||||
<option value="spot" {% if auto_transfer_to == 'spot' %}selected{% endif %}>to: spot</option>
|
|
||||||
</select>
|
|
||||||
<button type="submit">手动划转</button>
|
|
||||||
</form>
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
{# 系统设置 · 资金划转(三所共用) #}
|
|
||||||
<div class="settings-transfer-panel">
|
|
||||||
<p class="rule-tip settings-transfer-auto">
|
|
||||||
自动划转 <strong>{{ '开启' if auto_transfer_enabled else '关闭' }}</strong>:
|
|
||||||
每天<strong>北京时间 {{ auto_transfer_bj_hour }}:00</strong> 起该整点小时内尝试;
|
|
||||||
账簿按 <strong>UTC 自然日</strong> 去重;
|
|
||||||
将 <code>{{ auto_transfer_to }}</code> 调整至 <strong>{{ transfer_amount_fmt|default(funds_fmt(auto_transfer_amount)) }}U</strong>:
|
|
||||||
不足从 <code>{{ auto_transfer_from }}</code> 划入,超出划回 <code>{{ auto_transfer_from }}</code>;
|
|
||||||
<strong>持仓中不划转</strong>并微信通知.
|
|
||||||
</p>
|
|
||||||
<form action="/manual_transfer" method="post" class="form-row gate-transfer-form settings-transfer-form" autocomplete="off">
|
|
||||||
<input type="text" name="username" autocomplete="username" tabindex="-1" aria-hidden="true"
|
|
||||||
style="position:absolute;left:-9999px;width:1px;height:1px;opacity:0" value="">
|
|
||||||
<input name="amount" id="manual-xfer-amount" type="number" min="0.01" step="0.01" placeholder="手动划转金额 U" required
|
|
||||||
autocomplete="off" inputmode="decimal" data-lpignore="true" data-1p-ignore="true" data-bwignore="true" data-form-type="other" readonly>
|
|
||||||
<select name="from_account" aria-label="划出账户" autocomplete="off">
|
|
||||||
<option value="funding" {% if auto_transfer_from == 'funding' %}selected{% endif %}>from: funding</option>
|
|
||||||
<option value="swap" {% if auto_transfer_from == 'swap' %}selected{% endif %}>from: swap</option>
|
|
||||||
<option value="spot" {% if auto_transfer_from == 'spot' %}selected{% endif %}>from: spot</option>
|
|
||||||
</select>
|
|
||||||
<select name="to_account" aria-label="划入账户" autocomplete="off">
|
|
||||||
<option value="swap" {% if auto_transfer_to == 'swap' %}selected{% endif %}>to: swap</option>
|
|
||||||
<option value="funding" {% if auto_transfer_to == 'funding' %}selected{% endif %}>to: funding</option>
|
|
||||||
<option value="spot" {% if auto_transfer_to == 'spot' %}selected{% endif %}>to: spot</option>
|
|
||||||
</select>
|
|
||||||
<button type="submit">执行手动划转</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
{# 趋势户:两级开仓类型 → 自动 trade_style;日内户:假破 / 结构突破 #}
|
|
||||||
|
|
||||||
{% macro order_entry_type_fields() -%}
|
|
||||||
|
|
||||||
{% if order_entry_profile == 'trend_div' %}
|
|
||||||
|
|
||||||
<div class="order-entry-model-row">
|
|
||||||
|
|
||||||
<select id="order-entry-category" class="order-entry-category" required title="反转 / 顺势 / 波段" aria-label="开仓性质">
|
|
||||||
|
|
||||||
<option value="">性质</option>
|
|
||||||
|
|
||||||
{% for cat in entry_model_categories %}
|
|
||||||
|
|
||||||
<option value="{{ cat.key }}">{{ cat.label }}</option>
|
|
||||||
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select name="entry_model" id="order-entry-model" class="order-entry-model-sub" required disabled title="启动A/B,大分歧A/B,小分歧" aria-label="开仓类型">
|
|
||||||
|
|
||||||
<option value="">类型</option>
|
|
||||||
|
|
||||||
{% for cat in entry_model_categories %}
|
|
||||||
|
|
||||||
{% for opt in cat.options %}
|
|
||||||
|
|
||||||
<option value="{{ opt.code }}" data-entry-category="{{ cat.key }}" data-trade-style="{{ opt.trade_style }}"{% if opt.help %} title="{{ opt.help }}"{% endif %} hidden disabled>{{ opt.label }}</option>
|
|
||||||
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<input type="hidden" name="trade_style" id="order-trade-style-hidden" value="trend">
|
|
||||||
|
|
||||||
<span id="order-trade-style-hint" class="order-trade-style-hint" title="由开仓类型自动设定">趋势单</span>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% elif order_entry_profile == 'intraday' %}
|
|
||||||
|
|
||||||
<select name="entry_model" id="order-entry-model" class="order-entry-intraday" required title="日内开仓类型" aria-label="开仓类型">
|
|
||||||
|
|
||||||
<option value="">开仓类型</option>
|
|
||||||
|
|
||||||
{% for opt in intraday_entry_model_options %}
|
|
||||||
|
|
||||||
<option value="{{ opt.code }}"{% if opt.help %} title="{{ opt.help }}"{% endif %}>{{ opt.label }}</option>
|
|
||||||
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<input type="hidden" name="trade_style" value="trend">
|
|
||||||
|
|
||||||
{% else %}
|
|
||||||
|
|
||||||
<select name="trade_style" required>
|
|
||||||
|
|
||||||
<option value="trend">趋势单</option>
|
|
||||||
|
|
||||||
<option value="swing">波段单</option>
|
|
||||||
|
|
||||||
</select>
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{%- endmacro %}
|
|
||||||
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<script src="/static/instance_theme.js?v=5"></script>
|
|
||||||
<title>{{ exchange_display }} | 实盘下单放大</title>
|
|
||||||
<link rel="stylesheet" href="/static/instance_theme.css?v=5">
|
|
||||||
<link rel="stylesheet" href="/static/focus_chart_page.css?v=1">
|
|
||||||
</head>
|
|
||||||
<body class="focus-page">
|
|
||||||
<div class="container">
|
|
||||||
<div class="card">
|
|
||||||
<div class="row" style="justify-content:space-between">
|
|
||||||
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
|
|
||||||
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
|
|
||||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
|
|
||||||
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
|
|
||||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
|
||||||
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<a class="btn" href="/">返回首页</a>
|
|
||||||
<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>
|
|
||||||
{% if orders %}
|
|
||||||
<div class="row" style="margin-top:10px">
|
|
||||||
<label>订单</label>
|
|
||||||
<select id="order-id">
|
|
||||||
{% for o in orders %}
|
|
||||||
<option value="{{ o.id }}" {% if selected_order and o.id == selected_order.id %}selected{% endif %}>
|
|
||||||
#{{ o.id }} {{ o.symbol }} {{ '做多' if o.direction == 'long' else '做空' }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
<label>周期</label>
|
|
||||||
<select id="timeframe">
|
|
||||||
{% for tf in ['1m','3m','5m','15m','30m','1h','4h','1d'] %}
|
|
||||||
<option value="{{ tf }}" {% if tf == default_timeframe %}selected{% endif %}>{{ tf }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
<button id="manual-refresh" type="button">刷新</button>
|
|
||||||
<span id="load-status" class="status"></span>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="empty">当前没有激活订单,无法展示放大K线.</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if orders %}
|
|
||||||
<div class="card">
|
|
||||||
<div class="meta">
|
|
||||||
<div class="meta-item meta-item--emph"><div class="k">交易对</div><div class="v" id="m-symbol">-</div></div>
|
|
||||||
<div class="meta-item meta-item--emph"><div class="k">方向</div><div class="v" id="m-direction">-</div></div>
|
|
||||||
<div class="meta-item"><div class="k">成交价</div><div class="v" id="m-entry">-</div></div>
|
|
||||||
<div class="meta-item"><div class="k">止损</div><div class="v" id="m-sl">-</div></div>
|
|
||||||
<div class="meta-item"><div class="k">止盈</div><div class="v" id="m-tp">-</div></div>
|
|
||||||
<div class="meta-item"><div class="k">盈亏比</div><div class="v" id="m-rr">-</div></div>
|
|
||||||
<div class="meta-item"><div class="k">移动保本</div><div class="v" id="m-breakeven">-</div></div>
|
|
||||||
<div class="meta-item"><div class="k">现价</div><div class="v" id="m-price">-</div></div>
|
|
||||||
<div class="meta-item meta-item--emph meta-item--pnl"><div class="k">浮盈亏</div><div class="v" id="m-pnl">-</div></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<div id="chart-wrap"><div id="chart"></div></div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if orders %}
|
|
||||||
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
|
||||||
<script src="/static/focus_chart_page.js?v=2"></script>
|
|
||||||
<script>
|
|
||||||
const refreshMs = Math.max({{ price_refresh_seconds * 1000 }}, 5000);
|
|
||||||
const orderSelect = document.getElementById("order-id");
|
|
||||||
const tfSelect = document.getElementById("timeframe");
|
|
||||||
const statusEl = document.getElementById("load-status");
|
|
||||||
const updatedAtEl = document.getElementById("updated-at");
|
|
||||||
const chartHost = document.getElementById("chart");
|
|
||||||
const FCP = window.FocusChartPage;
|
|
||||||
let fc = null;
|
|
||||||
|
|
||||||
function ensureChart(){
|
|
||||||
if(fc && fc.ensureSeries()) return true;
|
|
||||||
if(!window.LightweightCharts){
|
|
||||||
statusEl.className = "status err";
|
|
||||||
statusEl.innerText = "图表库加载失败";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
fc = FCP.createFocusChart(chartHost);
|
|
||||||
if(!fc || !fc.ensureSeries()){
|
|
||||||
statusEl.className = "status err";
|
|
||||||
statusEl.innerText = "K线序列初始化失败";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadOrderKline(){
|
|
||||||
if(!ensureChart()) return;
|
|
||||||
const orderId = orderSelect.value;
|
|
||||||
const timeframe = tfSelect.value;
|
|
||||||
if(!orderId) return;
|
|
||||||
statusEl.className = "status";
|
|
||||||
statusEl.innerText = "加载中...";
|
|
||||||
try{
|
|
||||||
const resp = await fetch(`/api/order_kline?order_id=${encodeURIComponent(orderId)}&timeframe=${encodeURIComponent(timeframe)}`);
|
|
||||||
const data = await resp.json();
|
|
||||||
if(!resp.ok || !data.ok) throw new Error(data.msg || "请求失败");
|
|
||||||
if(fc && typeof fc.setPriceTick === "function") fc.setPriceTick(data.price_tick);
|
|
||||||
else FCP.setActivePriceTick(data.price_tick);
|
|
||||||
const candles = Array.isArray(data.candles) ? data.candles : [];
|
|
||||||
if(!candles.length){
|
|
||||||
statusEl.className = "status err";
|
|
||||||
statusEl.innerText = "暂无K线数据";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
fc.candleSeries.setData(candles);
|
|
||||||
fc.resetPriceLines();
|
|
||||||
const o = data.order || {};
|
|
||||||
fc.addLine(o.trigger_price, FCP.lineTitle("成交价", o.trigger_price_display), "#42a5f5");
|
|
||||||
fc.addLine(o.stop_loss, FCP.lineTitle("止损", o.stop_loss_display), "#ff6666");
|
|
||||||
fc.addLine(o.take_profit, FCP.lineTitle("止盈", o.take_profit_display), "#4cd97f");
|
|
||||||
const markPx = o.current_price;
|
|
||||||
if(markPx) fc.addLine(markPx, FCP.lineTitle("现价", o.current_price_display), "#ffb74d");
|
|
||||||
fc.chart.timeScale().fitContent();
|
|
||||||
FCP.paintOrderMeta(o);
|
|
||||||
updatedAtEl.innerText = data.updated_at || "--";
|
|
||||||
statusEl.className = "status";
|
|
||||||
statusEl.innerText = `已加载 ${candles.length} 根K线`;
|
|
||||||
}catch(err){
|
|
||||||
statusEl.className = "status err";
|
|
||||||
statusEl.innerText = err && err.message ? err.message : "加载失败";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById("manual-refresh").addEventListener("click", loadOrderKline);
|
|
||||||
orderSelect.addEventListener("change", loadOrderKline);
|
|
||||||
tfSelect.addEventListener("change", loadOrderKline);
|
|
||||||
loadOrderKline();
|
|
||||||
setInterval(loadOrderKline, refreshMs);
|
|
||||||
</script>
|
|
||||||
{% endif %}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{# 以损定仓:杠杆按币种默认(BTC/ETH 10x,其它 5x),不可选手输 #}
|
|
||||||
{% macro order_leverage_fields() -%}
|
|
||||||
{% if position_sizing_mode != 'full_margin' %}
|
|
||||||
<input type="hidden" id="order-leverage" name="leverage" value="">
|
|
||||||
<span id="order-leverage-hint" class="order-leverage-hint" title="BTC/ETH 默认10x,其它默认5x">杠杆 —</span>
|
|
||||||
{% endif %}
|
|
||||||
{%- endmacro %}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
{# 实盘下单监控 · 开仓表单(实例页与中控嵌入共用) #}
|
|
||||||
<form id="add-order-form" action="/add_order" method="post" class="order-monitor-form" data-risk-percent="{{ risk_percent }}">
|
|
||||||
<div class="om-row om-row-policy">
|
|
||||||
{% from 'trade_policy_fields.html' import trade_policy_symbol, trade_policy_direction with context %}
|
|
||||||
{{ trade_policy_symbol('symbol', 'order-symbol') }}
|
|
||||||
{{ trade_policy_direction('direction', 'order-direction') }}
|
|
||||||
<select id="sltp-mode" name="sltp_mode" title="止盈止损模式">
|
|
||||||
<option value="fixed_rr" selected>止盈止损:固定盈亏比</option>
|
|
||||||
<option value="price">止盈止损:价格模式</option>
|
|
||||||
<option value="pct">止盈止损:百分比模式</option>
|
|
||||||
</select>
|
|
||||||
{% from 'order_entry_model_fields.html' import order_entry_type_fields with context %}
|
|
||||||
{{ order_entry_type_fields() }}
|
|
||||||
{% from 'order_leverage_fields.html' import order_leverage_fields with context %}
|
|
||||||
{{ order_leverage_fields() }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="om-row om-row-levels">
|
|
||||||
<label class="om-field" id="om-field-sl">
|
|
||||||
<span class="om-field-lab">止损价格</span>
|
|
||||||
<input id="order-sl" name="sl" step="any" placeholder="必填" required>
|
|
||||||
</label>
|
|
||||||
<label class="om-field" id="om-field-rr">
|
|
||||||
<span class="om-field-lab">盈亏比</span>
|
|
||||||
<input id="order-fixed-rr" name="fixed_rr" type="number" min="0.01" step="0.01" placeholder="默认1.5" value="1.5" title="止盈距离=止损距离×盈亏比">
|
|
||||||
</label>
|
|
||||||
<label class="om-field" id="om-field-tp" style="display:none">
|
|
||||||
<span class="om-field-lab">止盈价格</span>
|
|
||||||
<input id="order-tp" name="tgt" step="any" placeholder="止盈价格">
|
|
||||||
</label>
|
|
||||||
<label class="om-field" id="om-field-sl-pct" style="display:none">
|
|
||||||
<span class="om-field-lab">止损%</span>
|
|
||||||
<input id="order-sl-pct" name="sl_pct" type="number" min="0.01" step="0.01" placeholder="止损%">
|
|
||||||
</label>
|
|
||||||
<label class="om-field" id="om-field-tp-pct" style="display:none">
|
|
||||||
<span class="om-field-lab">止盈%</span>
|
|
||||||
<input id="order-tp-pct" name="tp_pct" type="number" min="0.01" step="0.01" placeholder="止盈%">
|
|
||||||
</label>
|
|
||||||
<div class="om-live-meta">
|
|
||||||
{% from 'symbol_live_price_snippet.html' import symbol_live_price_hint %}
|
|
||||||
{{ symbol_live_price_hint('order-symbol-live-price', 'order-symbol', 'order-direction') }}
|
|
||||||
<span class="symbol-live-price-note">成交价以交易所回报为准</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="om-row om-row-opts">
|
|
||||||
{% if not intraday_discipline %}
|
|
||||||
<label class="om-check">
|
|
||||||
<input type="checkbox" name="breakeven_enabled" value="1" checked>
|
|
||||||
<span>移动保本</span>
|
|
||||||
</label>
|
|
||||||
<span id="order-time-close-wrap" class="order-time-close-wrap om-time-close">
|
|
||||||
<label class="om-check" style="margin:0">
|
|
||||||
<input type="checkbox" name="time_close_enabled" value="1" id="order-time-close-cb">
|
|
||||||
<span>时间平仓</span>
|
|
||||||
</label>
|
|
||||||
<select name="time_close_hours" id="order-time-close-hours" title="持仓满该时长后自动平仓">
|
|
||||||
<option value="1">1h</option>
|
|
||||||
<option value="2">2h</option>
|
|
||||||
<option value="4" selected>4h</option>
|
|
||||||
</select>
|
|
||||||
</span>
|
|
||||||
{% else %}
|
|
||||||
<input type="hidden" name="breakeven_enabled" value="0">
|
|
||||||
{% endif %}
|
|
||||||
<label class="om-check" title="开仓后生成多周期K线图(各周期100根,含开平仓标记)">
|
|
||||||
<input type="checkbox" name="order_chart" value="true">
|
|
||||||
<span>开仓后生成多周期K线</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="om-row om-row-action">
|
|
||||||
<button type="submit" class="om-submit{% if not can_trade %} is-blocked{% endif %}" id="om-submit-btn"{% if not can_trade %} disabled aria-disabled="true"{% endif %}>{{ open_position_button_label }}</button>
|
|
||||||
<span id="om-open-block-note" class="om-open-block-note"{% if can_trade or not (open_block_note|default('')) %} hidden{% endif %}>{% if not can_trade and (open_block_note|default('')) %}{{ open_block_note }}{% endif %}</span>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{% include 'order_plan_preview_bar.html' %}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
<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 %};
|
|
||||||
人工开仓盈亏比不得低于 {{ manual_min_planned_rr }}:1
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
<details class="tip-collapse order-exit-collapse">
|
|
||||||
<summary class="tip-collapse-summary">平仓 / 委托 / 强制清仓</summary>
|
|
||||||
<div class="tip-collapse-body rule-tip">
|
|
||||||
<ul style="margin:0;padding-left:1.1em;line-height:1.55">
|
|
||||||
<li><strong>止盈止损委托</strong>:开仓后挂交易所条件止盈/止损;持仓卡可查看状态,「委托」可重挂,「撤单」只撤对应条件单、不平仓。</li>
|
|
||||||
<li><strong>手动平仓</strong>:持仓卡「平仓」会市价平仓并撤销该合约条件单,交易记录记为「手动平仓」。</li>
|
|
||||||
<li><strong>强制清仓</strong>:
|
|
||||||
{% if force_close is defined and force_close.enabled %}
|
|
||||||
已开启 · 北京时间 <strong>{{ force_close.hour_label }}</strong> 整点起 <strong>{{ force_close.grace_minutes|default(5) }}</strong> 分钟内,本地状态为 active 的下单监控会被市价清仓,结果记「强制清仓」;该窗口内开仓按钮灰显不可点。
|
|
||||||
{% else %}
|
|
||||||
当前<strong>已关闭</strong>(<code>FORCE_CLOSE_ENABLED=false</code>);开启后按 <code>FORCE_CLOSE_BJ_HOUR</code> 在北京时间该整点起 <code>FORCE_CLOSE_GRACE_MINUTES</code>(默认 5)分钟内清掉 active 监控仓。
|
|
||||||
{% endif %}
|
|
||||||
冷静期/日冻结期间开仓按钮同样灰显不可点。仅影响本系统监控中的仓。
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</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>,须无仓后重启)
|
|
||||||
{% if position_sizing_mode == 'full_margin' %}
|
|
||||||
|全仓:合约可用×{{ full_margin_buffer_ratio }},BTC/ETH {{ btc_leverage }}x,其它 {{ alt_leverage }}x,单仓;张数按交易所精度
|
|
||||||
{% else %}
|
|
||||||
|以损定仓:风险 {{ risk_percent }}%
|
|
||||||
{% endif %}
|
|
||||||
|移动保本:下单可勾选关闭;开启时 {{ breakeven_rr_trigger }}R 触发(每 1R 阶梯上移),偏移 {{ breakeven_offset_pct }}%
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
<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 %};
|
|
||||||
人工开仓盈亏比不得低于 {{ manual_min_planned_rr }}:1
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
<details class="tip-collapse order-exit-collapse">
|
|
||||||
<summary class="tip-collapse-summary">平仓 / 委托 / 强制清仓</summary>
|
|
||||||
<div class="tip-collapse-body rule-tip">
|
|
||||||
<ul style="margin:0;padding-left:1.1em;line-height:1.55">
|
|
||||||
<li><strong>止盈止损委托</strong>:开仓后挂交易所条件止盈/止损;持仓卡可查看状态,「委托」可重挂,「撤单」只撤对应条件单、不平仓。</li>
|
|
||||||
<li><strong>手动平仓</strong>:持仓卡「平仓」会市价平仓并撤销该合约条件单,交易记录记为「手动平仓」。</li>
|
|
||||||
<li><strong>强制清仓</strong>:
|
|
||||||
{% if force_close is defined and force_close.enabled %}
|
|
||||||
已开启 · 北京时间 <strong>{{ force_close.hour_label }}</strong> 整点起 <strong>{{ force_close.grace_minutes|default(5) }}</strong> 分钟内,本地状态为 active 的下单监控会被市价清仓,结果记「强制清仓」;该窗口内开仓按钮灰显不可点。
|
|
||||||
{% else %}
|
|
||||||
当前<strong>已关闭</strong>(<code>FORCE_CLOSE_ENABLED=false</code>);开启后按 <code>FORCE_CLOSE_BJ_HOUR</code> 在北京时间该整点起 <code>FORCE_CLOSE_GRACE_MINUTES</code>(默认 5)分钟内清掉 active 监控仓。
|
|
||||||
{% endif %}
|
|
||||||
冷静期/日冻结期间开仓按钮同样灰显不可点。仅影响本系统监控中的仓;交易所裸仓且无本地监控时不会被此逻辑平掉。
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</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>,须无仓后重启)
|
|
||||||
{% if position_sizing_mode == 'full_margin' %}
|
|
||||||
|全仓:合约可用×{{ full_margin_buffer_ratio }},BTC/ETH {{ btc_leverage }}x,其它 {{ alt_leverage }}x,单仓;张数按交易所精度
|
|
||||||
{% else %}
|
|
||||||
|以损定仓:风险 {{ risk_percent }}%
|
|
||||||
{% endif %}
|
|
||||||
|移动保本:下单可勾选关闭;开启时 {{ breakeven_rr_trigger }}R 触发(每 1R 阶梯上移),偏移 {{ breakeven_offset_pct }}%
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
<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 %};
|
|
||||||
人工开仓盈亏比不得低于 {{ 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>,须无仓后重启)
|
|
||||||
{% if position_sizing_mode == 'full_margin' %}
|
|
||||||
|全仓:合约可用×{{ full_margin_buffer_ratio }},BTC/ETH {{ btc_leverage }}x,其它 {{ alt_leverage }}x,单仓;张数按交易所精度
|
|
||||||
{% else %}
|
|
||||||
|以损定仓:风险 {{ risk_percent }}%
|
|
||||||
{% endif %}
|
|
||||||
|移动保本:下单可勾选关闭;开启时 {{ breakeven_rr_trigger }}R 触发(每 1R 阶梯上移),偏移 {{ breakeven_offset_pct }}%
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
<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 %};
|
|
||||||
人工开仓盈亏比不得低于 {{ manual_min_planned_rr }}:1
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
<details class="tip-collapse order-exit-collapse">
|
|
||||||
<summary class="tip-collapse-summary">平仓 / 委托 / 强制清仓</summary>
|
|
||||||
<div class="tip-collapse-body rule-tip">
|
|
||||||
<ul style="margin:0;padding-left:1.1em;line-height:1.55">
|
|
||||||
<li><strong>止盈止损委托</strong>:开仓后挂交易所条件止盈/止损;持仓卡可查看状态,「委托」可重挂,「撤单」只撤对应条件单、不平仓。</li>
|
|
||||||
<li><strong>手动平仓</strong>:持仓卡「平仓」会市价平仓并撤销该合约条件单,交易记录记为「手动平仓」。</li>
|
|
||||||
<li><strong>强制清仓</strong>:
|
|
||||||
{% if force_close is defined and force_close.enabled %}
|
|
||||||
已开启 · 北京时间 <strong>{{ force_close.hour_label }}</strong> 整点起 <strong>{{ force_close.grace_minutes|default(5) }}</strong> 分钟内,本地状态为 active 的下单监控会被市价清仓,结果记「强制清仓」;该窗口内开仓按钮灰显不可点。
|
|
||||||
{% else %}
|
|
||||||
当前<strong>已关闭</strong>(<code>FORCE_CLOSE_ENABLED=false</code>);开启后按 <code>FORCE_CLOSE_BJ_HOUR</code> 在北京时间该整点起 <code>FORCE_CLOSE_GRACE_MINUTES</code>(默认 5)分钟内清掉 active 监控仓。
|
|
||||||
{% endif %}
|
|
||||||
冷静期/日冻结期间开仓按钮同样灰显不可点。仅影响本系统监控中的仓。
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</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>,须无仓后重启)
|
|
||||||
{% if position_sizing_mode == 'full_margin' %}
|
|
||||||
|全仓:合约可用×{{ full_margin_buffer_ratio }},BTC/ETH {{ btc_leverage }}x,其它 {{ alt_leverage }}x,单仓;张数按交易所精度
|
|
||||||
{% else %}
|
|
||||||
|以损定仓:风险 {{ risk_percent }}%
|
|
||||||
{% endif %}
|
|
||||||
|移动保本:下单可勾选关闭;开启时 {{ breakeven_rr_trigger }}R 触发(每 1R 阶梯上移),偏移 {{ breakeven_offset_pct }}%
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<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>
|
|
||||||
</div>
|
|
||||||
@@ -37,9 +37,8 @@
|
|||||||
{% elif tab.key == 'password' %}
|
{% elif tab.key == 'password' %}
|
||||||
{% include 'password_settings_panel.html' %}
|
{% include 'password_settings_panel.html' %}
|
||||||
{% elif tab.key == 'transfer' %}
|
{% elif tab.key == 'transfer' %}
|
||||||
<h2>永续资金划转</h2>
|
<h2>资金划转</h2>
|
||||||
<p class="settings-subcard-desc">账户内:资金账户与交易账户之间划转 USDT.</p>
|
<p class="settings-subcard-desc muted">永续账户划转已下线;期权/模拟盘请使用对应页内划转.</p>
|
||||||
{% include 'instance_transfer_panel.html' %}
|
|
||||||
{% elif tab.key == 'export' %}
|
{% elif tab.key == 'export' %}
|
||||||
<h2>数据导出</h2>
|
<h2>数据导出</h2>
|
||||||
<p class="settings-subcard-desc muted">CSV · v{{ instance_settings.data_export_version }}</p>
|
<p class="settings-subcard-desc muted">CSV · v{{ instance_settings.data_export_version }}</p>
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
"""Sync order_monitors after an external/market flat."""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import time
|
|
||||||
from typing import Any, Callable
|
|
||||||
|
|
||||||
|
|
||||||
def reconcile_hub_external_close_impl(
|
|
||||||
conn,
|
|
||||||
symbol: str,
|
|
||||||
direction: str,
|
|
||||||
*,
|
|
||||||
exchange_configured: Callable[[], bool],
|
|
||||||
not_configured_msg: str,
|
|
||||||
symbols_match: Callable[[str, str], bool],
|
|
||||||
get_opened_at_value: Callable[[Any], str],
|
|
||||||
resolve_monitor_exchange_symbol: Callable[[Any], str],
|
|
||||||
get_live_position_contracts: Callable[[str, str], float | None],
|
|
||||||
cancel_conditional_orders: Callable[[str], None],
|
|
||||||
resolve_synced_flat_close: Callable[..., tuple],
|
|
||||||
finalize_stopped_monitor: Callable[..., None],
|
|
||||||
sync_trade_records: Callable[..., None] | None = None,
|
|
||||||
reconcile_flat_streak: dict | None = None,
|
|
||||||
to_ms_with_fallback: Callable[..., int | None] | None = None,
|
|
||||||
prefer_manual_resolve: bool = False,
|
|
||||||
order_row_monitor_type: Callable[[Any], str] | None = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
if not exchange_configured():
|
|
||||||
return {"ok": False, "msg": not_configured_msg, "synced": 0}
|
|
||||||
sym_req = (symbol or "").strip()
|
|
||||||
dir_l = (direction or "").strip().lower()
|
|
||||||
if dir_l not in ("long", "short"):
|
|
||||||
return {"ok": False, "msg": "side 须为 long 或 short", "synced": 0}
|
|
||||||
synced = 0
|
|
||||||
streak = reconcile_flat_streak if reconcile_flat_streak is not None else {}
|
|
||||||
rows = conn.execute(
|
|
||||||
"SELECT * FROM order_monitors WHERE status IN ('active', 'error')"
|
|
||||||
).fetchall()
|
|
||||||
for r in rows:
|
|
||||||
if not symbols_match(str(r["symbol"] or ""), sym_req):
|
|
||||||
continue
|
|
||||||
if (r["direction"] or "").strip().lower() != dir_l:
|
|
||||||
continue
|
|
||||||
oid = int(r["id"])
|
|
||||||
if r["status"] == "error":
|
|
||||||
opened_at_chk = get_opened_at_value(r)
|
|
||||||
mtype = order_row_monitor_type(r) if order_row_monitor_type else r["monitor_type"]
|
|
||||||
existing = conn.execute(
|
|
||||||
"SELECT id FROM trade_records WHERE symbol=? AND opened_at=? AND monitor_type=? LIMIT 1",
|
|
||||||
(r["symbol"], opened_at_chk, mtype),
|
|
||||||
).fetchone()
|
|
||||||
if existing:
|
|
||||||
conn.execute("UPDATE order_monitors SET status='stopped' WHERE id=?", (oid,))
|
|
||||||
synced += 1
|
|
||||||
continue
|
|
||||||
exchange_symbol = resolve_monitor_exchange_symbol(r)
|
|
||||||
live_contracts = get_live_position_contracts(exchange_symbol, r["direction"])
|
|
||||||
if live_contracts is None:
|
|
||||||
continue
|
|
||||||
if live_contracts > 0:
|
|
||||||
time.sleep(0.6)
|
|
||||||
live_contracts = get_live_position_contracts(exchange_symbol, r["direction"])
|
|
||||||
if live_contracts is None or live_contracts > 0:
|
|
||||||
continue
|
|
||||||
streak.pop(oid, None)
|
|
||||||
cancel_conditional_orders(exchange_symbol)
|
|
||||||
opened_at = get_opened_at_value(r)
|
|
||||||
opened_at_ms = None
|
|
||||||
if to_ms_with_fallback is not None:
|
|
||||||
keys = r.keys() if hasattr(r, "keys") else ()
|
|
||||||
opened_at_ms = to_ms_with_fallback(
|
|
||||||
r["opened_at_ms"] if "opened_at_ms" in keys else None,
|
|
||||||
opened_at,
|
|
||||||
)
|
|
||||||
resolve_kw = {"opened_at_ms": opened_at_ms}
|
|
||||||
if prefer_manual_resolve:
|
|
||||||
resolve_kw["prefer_manual"] = True
|
|
||||||
result, pnl_amount, closed_at, miss_reason = resolve_synced_flat_close(
|
|
||||||
r, opened_at, **resolve_kw
|
|
||||||
)
|
|
||||||
finalize_stopped_monitor(
|
|
||||||
conn,
|
|
||||||
r,
|
|
||||||
result=result,
|
|
||||||
pnl_amount=pnl_amount,
|
|
||||||
closed_at=closed_at,
|
|
||||||
miss_reason=miss_reason,
|
|
||||||
)
|
|
||||||
synced += 1
|
|
||||||
if sync_trade_records is not None:
|
|
||||||
try:
|
|
||||||
sync_trade_records(conn, force=True)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return {"ok": True, "synced": synced}
|
|
||||||
|
|
||||||
|
|
||||||
reconcile_external_close_impl = reconcile_hub_external_close_impl
|
|
||||||
+3
-66
@@ -40,9 +40,7 @@ def apply_sim_hooks(app_module: Any) -> None:
|
|||||||
_orig_ensure = app_module.ensure_okx_live_ready
|
_orig_ensure = app_module.ensure_okx_live_ready
|
||||||
_orig_capitals = app_module.get_exchange_capitals
|
_orig_capitals = app_module.get_exchange_capitals
|
||||||
_orig_avail = app_module.get_available_trading_usdt
|
_orig_avail = app_module.get_available_trading_usdt
|
||||||
_orig_place = app_module.place_exchange_order
|
_orig_live_contracts = getattr(app_module, "get_live_position_contracts", None)
|
||||||
_orig_close = app_module.close_exchange_order
|
|
||||||
_orig_live_contracts = app_module.get_live_position_contracts
|
|
||||||
|
|
||||||
def ensure_okx_live_ready():
|
def ensure_okx_live_ready():
|
||||||
if is_sim_mode(app_module.get_db):
|
if is_sim_mode(app_module.get_db):
|
||||||
@@ -60,67 +58,6 @@ def apply_sim_hooks(app_module: Any) -> None:
|
|||||||
return float(broker().balances_header()["trading_usdt"])
|
return float(broker().balances_header()["trading_usdt"])
|
||||||
return _orig_avail()
|
return _orig_avail()
|
||||||
|
|
||||||
def place_exchange_order(
|
|
||||||
exchange_symbol, direction, amount, leverage, stop_loss=None, take_profit=None
|
|
||||||
):
|
|
||||||
if is_sim_mode(app_module.get_db):
|
|
||||||
ex = getattr(app_module, "exchange", None)
|
|
||||||
if ex is None:
|
|
||||||
raise RuntimeError("sim: exchange 未就绪(公开行情)")
|
|
||||||
ensure = getattr(app_module, "ensure_markets_loaded", None)
|
|
||||||
if callable(ensure):
|
|
||||||
try:
|
|
||||||
ensure()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return broker().place_perp_market(
|
|
||||||
ex,
|
|
||||||
symbol=exchange_symbol,
|
|
||||||
direction=direction,
|
|
||||||
contracts=float(amount),
|
|
||||||
leverage=int(leverage or 1),
|
|
||||||
fee_rate=sim_fee_rate(),
|
|
||||||
stop_loss=stop_loss,
|
|
||||||
take_profit=take_profit,
|
|
||||||
)
|
|
||||||
return _orig_place(
|
|
||||||
exchange_symbol, direction, amount, leverage, stop_loss=stop_loss, take_profit=take_profit
|
|
||||||
)
|
|
||||||
|
|
||||||
def close_exchange_order(order_row):
|
|
||||||
if is_sim_mode(app_module.get_db):
|
|
||||||
ex = getattr(app_module, "exchange", None)
|
|
||||||
if ex is None:
|
|
||||||
raise RuntimeError("sim: exchange 未就绪")
|
|
||||||
ensure = getattr(app_module, "ensure_markets_loaded", None)
|
|
||||||
if callable(ensure):
|
|
||||||
try:
|
|
||||||
ensure()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
normalize = getattr(app_module, "normalize_okx_symbol", None) or getattr(
|
|
||||||
app_module, "normalize_exchange_symbol", None
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
symbol = order_row["exchange_symbol"] or None
|
|
||||||
except Exception:
|
|
||||||
symbol = None
|
|
||||||
if not symbol:
|
|
||||||
try:
|
|
||||||
symbol = order_row["symbol"]
|
|
||||||
except Exception:
|
|
||||||
symbol = None
|
|
||||||
if callable(normalize):
|
|
||||||
symbol = normalize(symbol)
|
|
||||||
direction = order_row["direction"]
|
|
||||||
db_amt = float(order_row["order_amount"] or 0)
|
|
||||||
live = broker().get_perp_contracts(symbol, direction)
|
|
||||||
amt = live if live and live > 0 else db_amt
|
|
||||||
return broker().close_perp_market(
|
|
||||||
ex, symbol=symbol, direction=direction, contracts=amt, fee_rate=sim_fee_rate()
|
|
||||||
)
|
|
||||||
return _orig_close(order_row)
|
|
||||||
|
|
||||||
def get_live_position_contracts(exchange_symbol, direction):
|
def get_live_position_contracts(exchange_symbol, direction):
|
||||||
if is_sim_mode(app_module.get_db):
|
if is_sim_mode(app_module.get_db):
|
||||||
normalize = getattr(app_module, "normalize_okx_symbol", None)
|
normalize = getattr(app_module, "normalize_okx_symbol", None)
|
||||||
@@ -128,13 +65,13 @@ def apply_sim_hooks(app_module: Any) -> None:
|
|||||||
if callable(normalize):
|
if callable(normalize):
|
||||||
sym = normalize(exchange_symbol or "")
|
sym = normalize(exchange_symbol or "")
|
||||||
return broker().get_perp_contracts(sym, direction)
|
return broker().get_perp_contracts(sym, direction)
|
||||||
|
if callable(_orig_live_contracts):
|
||||||
return _orig_live_contracts(exchange_symbol, direction)
|
return _orig_live_contracts(exchange_symbol, direction)
|
||||||
|
return 0.0
|
||||||
|
|
||||||
app_module.ensure_okx_live_ready = ensure_okx_live_ready
|
app_module.ensure_okx_live_ready = ensure_okx_live_ready
|
||||||
app_module.get_exchange_capitals = get_exchange_capitals
|
app_module.get_exchange_capitals = get_exchange_capitals
|
||||||
app_module.get_available_trading_usdt = get_available_trading_usdt
|
app_module.get_available_trading_usdt = get_available_trading_usdt
|
||||||
app_module.place_exchange_order = place_exchange_order
|
|
||||||
app_module.close_exchange_order = close_exchange_order
|
|
||||||
app_module.get_live_position_contracts = get_live_position_contracts
|
app_module.get_live_position_contracts = get_live_position_contracts
|
||||||
|
|
||||||
_orig_xfer = getattr(app_module, "execute_transfer_usdt", None)
|
_orig_xfer = getattr(app_module, "execute_transfer_usdt", None)
|
||||||
|
|||||||
@@ -56,8 +56,6 @@ def install_sim_trading(app: Flask, repo_root: str, app_module: Any = None) -> N
|
|||||||
if isinstance(cfg, dict):
|
if isinstance(cfg, dict):
|
||||||
patch_options_cfg(cfg)
|
patch_options_cfg(cfg)
|
||||||
for fn_name in (
|
for fn_name in (
|
||||||
"place_exchange_order",
|
|
||||||
"close_exchange_order",
|
|
||||||
"get_exchange_capitals",
|
"get_exchange_capitals",
|
||||||
"get_available_trading_usdt",
|
"get_available_trading_usdt",
|
||||||
"ensure_okx_live_ready",
|
"ensure_okx_live_ready",
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
"""开仓后挂 TP/SL 失败时的补偿平仓(避免裸仓)."""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Callable
|
|
||||||
|
|
||||||
|
|
||||||
def log_compensating_close_error(prefix: str, exc: BaseException) -> None:
|
|
||||||
print(f"[{prefix}] {exc}", flush=True)
|
|
||||||
|
|
||||||
|
|
||||||
def run_compensating_close(close_fn: Callable[[], None], *, log_prefix: str = "compensating_close") -> None:
|
|
||||||
"""执行补偿平仓;二次失败只打日志,不掩盖原始异常."""
|
|
||||||
try:
|
|
||||||
close_fn()
|
|
||||||
except Exception as e:
|
|
||||||
log_compensating_close_error(log_prefix, e)
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
"""单日开仓次数:软提醒阈值 + 硬上限(三所实例共用)."""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import os
|
|
||||||
from typing import Any, Optional
|
|
||||||
|
|
||||||
|
|
||||||
def parse_daily_open_alert_threshold(raw: Any = None, *, default: int = 5) -> int:
|
|
||||||
"""AI 克制提醒阈值;至少 1."""
|
|
||||||
try:
|
|
||||||
v = int(raw if raw is not None and str(raw).strip() != "" else default)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
v = default
|
|
||||||
return max(1, v)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_daily_open_hard_limit(raw: Any = None, *, default: int = 0) -> int:
|
|
||||||
"""硬上限;0 表示不启用.至少 0."""
|
|
||||||
try:
|
|
||||||
v = int(raw if raw is not None and str(raw).strip() != "" else default)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
v = default
|
|
||||||
return max(0, v)
|
|
||||||
|
|
||||||
|
|
||||||
def load_daily_open_limits_from_env(
|
|
||||||
env: Optional[dict[str, str]] = None,
|
|
||||||
) -> tuple[int, int]:
|
|
||||||
"""从环境变量读取 (alert_threshold, hard_limit)."""
|
|
||||||
src = env if env is not None else os.environ
|
|
||||||
alert = parse_daily_open_alert_threshold(src.get("DAILY_OPEN_ALERT_THRESHOLD"))
|
|
||||||
hard = parse_daily_open_hard_limit(src.get("DAILY_OPEN_HARD_LIMIT"))
|
|
||||||
return alert, hard
|
|
||||||
|
|
||||||
|
|
||||||
def count_opens_for_trading_day(conn, trading_day: str) -> int:
|
|
||||||
"""本交易日已成功写入 order_monitors 的开仓次数."""
|
|
||||||
td = (trading_day or "").strip()
|
|
||||||
if not td:
|
|
||||||
return 0
|
|
||||||
row = conn.execute(
|
|
||||||
"SELECT COUNT(*) FROM order_monitors WHERE session_date=?",
|
|
||||||
(td,),
|
|
||||||
).fetchone()
|
|
||||||
return int(row[0] if row else 0)
|
|
||||||
|
|
||||||
|
|
||||||
def daily_open_hard_limit_blocks(opens_today: int, hard_limit: int) -> bool:
|
|
||||||
return int(hard_limit) > 0 and int(opens_today) >= int(hard_limit)
|
|
||||||
|
|
||||||
|
|
||||||
def hard_limit_block_reason(opens_today: int, hard_limit: int, reset_hour: int) -> str:
|
|
||||||
return (
|
|
||||||
f"本交易日开仓次数已达上限({int(opens_today)}/{int(hard_limit)}),"
|
|
||||||
f"次日北京时间 {int(reset_hour)}:00 后恢复"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def check_daily_open_hard_limit(
|
|
||||||
conn,
|
|
||||||
trading_day: str,
|
|
||||||
hard_limit: int,
|
|
||||||
reset_hour: int,
|
|
||||||
) -> tuple[bool, str, int]:
|
|
||||||
"""返回 (允许继续开仓, 拒绝原因, 当日已开次数)."""
|
|
||||||
opens_today = count_opens_for_trading_day(conn, trading_day)
|
|
||||||
if daily_open_hard_limit_blocks(opens_today, hard_limit):
|
|
||||||
return False, hard_limit_block_reason(opens_today, hard_limit, reset_hour), opens_today
|
|
||||||
return True, "", opens_today
|
|
||||||
|
|
||||||
|
|
||||||
def can_trade_new_open(
|
|
||||||
*,
|
|
||||||
time_allows: bool,
|
|
||||||
active_count: int,
|
|
||||||
max_active_positions: int,
|
|
||||||
opens_today: int,
|
|
||||||
hard_limit: int,
|
|
||||||
extra_blocks: bool = False,
|
|
||||||
) -> bool:
|
|
||||||
if extra_blocks:
|
|
||||||
return False
|
|
||||||
if not time_allows:
|
|
||||||
return False
|
|
||||||
if int(active_count) >= int(max_active_positions):
|
|
||||||
return False
|
|
||||||
if daily_open_hard_limit_blocks(opens_today, hard_limit):
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def should_send_daily_open_alert(before: int, after: int, alert_threshold: int) -> bool:
|
|
||||||
return int(before) < int(alert_threshold) <= int(after)
|
|
||||||
|
|
||||||
|
|
||||||
def build_daily_open_alert_prompt(
|
|
||||||
trading_day: str,
|
|
||||||
opens_after: int,
|
|
||||||
alert_threshold: int,
|
|
||||||
*,
|
|
||||||
hard_limit: int = 0,
|
|
||||||
detail_line: str = "",
|
|
||||||
) -> str:
|
|
||||||
hard_txt = (
|
|
||||||
f"硬上限 {hard_limit} 次(已达后将禁止新开仓直至下一交易日)."
|
|
||||||
if int(hard_limit) > 0
|
|
||||||
else "未配置单日硬上限."
|
|
||||||
)
|
|
||||||
extra = f" {detail_line}" if detail_line else ""
|
|
||||||
return (
|
|
||||||
f"用户在北京时间交易日 {trading_day} 已累计开仓 {opens_after} 次"
|
|
||||||
f"(AI 提醒阈值 {alert_threshold};{hard_txt})"
|
|
||||||
f"{extra}"
|
|
||||||
f"用户自述“上头了”.请给克制提醒."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def format_daily_open_counter_line(
|
|
||||||
opens_today: int,
|
|
||||||
alert_threshold: int,
|
|
||||||
hard_limit: int,
|
|
||||||
) -> str:
|
|
||||||
if int(hard_limit) > 0:
|
|
||||||
return (
|
|
||||||
f"📅 当日开仓次数:{int(opens_today)} / 硬上限 {int(hard_limit)} 次"
|
|
||||||
f"(AI 提醒阈值 {int(alert_threshold)})"
|
|
||||||
)
|
|
||||||
return (
|
|
||||||
f"📅 当日开仓次数:{int(opens_today)} / AI 提醒阈值 {int(alert_threshold)} 次"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def format_daily_open_summary_short(
|
|
||||||
opens_today: int,
|
|
||||||
alert_threshold: int,
|
|
||||||
hard_limit: int,
|
|
||||||
) -> str:
|
|
||||||
if int(hard_limit) > 0:
|
|
||||||
return f"本交易日累计开仓:{int(opens_today)}(硬上限 {int(hard_limit)},提醒 {int(alert_threshold)})"
|
|
||||||
return f"本交易日累计开仓:{int(opens_today)}(提醒阈值 {int(alert_threshold)})"
|
|
||||||
@@ -441,10 +441,13 @@ def enrich_entry_model_display(item: dict) -> dict:
|
|||||||
return item
|
return item
|
||||||
|
|
||||||
|
|
||||||
def open_position_button_label(policy: TradePolicy, sizing_mode: str) -> str:
|
def _sizing_mode_label_zh(mode: str) -> str:
|
||||||
from lib.trade.position_sizing_lib import mode_label_zh
|
m = (mode or "").strip().lower()
|
||||||
|
return {"risk": "按风险", "full_margin": "满保证金"}.get(m, m or "-")
|
||||||
|
|
||||||
mode_txt = mode_label_zh(sizing_mode)
|
|
||||||
|
def open_position_button_label(policy: TradePolicy, sizing_mode: str) -> str:
|
||||||
|
mode_txt = _sizing_mode_label_zh(sizing_mode)
|
||||||
if is_intraday_trading_profile(policy):
|
if is_intraday_trading_profile(policy):
|
||||||
return f"开仓(日内·{mode_txt})"
|
return f"开仓(日内·{mode_txt})"
|
||||||
return f"开仓({mode_txt})"
|
return f"开仓({mode_txt})"
|
||||||
|
|||||||
@@ -1,366 +0,0 @@
|
|||||||
"""整点强制清仓(FORCE_CLOSE_*):UI 标识与持仓倒计时."""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from typing import Any, Optional
|
|
||||||
from zoneinfo import ZoneInfo
|
|
||||||
|
|
||||||
FORCE_CLOSE_RESULT = "强制清仓"
|
|
||||||
# 默认宽限(分钟);运行时优先读 FORCE_CLOSE_GRACE_MINUTES
|
|
||||||
FORCE_CLOSE_GRACE_MINUTES = 5
|
|
||||||
|
|
||||||
|
|
||||||
def force_close_grace_minutes(override: Any = None) -> int:
|
|
||||||
"""整点强制清仓执行窗口长度(分钟)."""
|
|
||||||
if override is not None and str(override).strip() != "":
|
|
||||||
raw = override
|
|
||||||
else:
|
|
||||||
raw = os.getenv("FORCE_CLOSE_GRACE_MINUTES")
|
|
||||||
if raw is None or str(raw).strip() == "":
|
|
||||||
raw = FORCE_CLOSE_GRACE_MINUTES
|
|
||||||
try:
|
|
||||||
return max(1, int(raw))
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
return max(1, int(FORCE_CLOSE_GRACE_MINUTES))
|
|
||||||
|
|
||||||
|
|
||||||
def app_timezone_name() -> str:
|
|
||||||
return (os.getenv("APP_TIMEZONE") or os.getenv("TZ") or "Asia/Shanghai").strip() or "Asia/Shanghai"
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_force_close_bj_hour(value: Any) -> int:
|
|
||||||
try:
|
|
||||||
h = int(value)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
return 0
|
|
||||||
return max(0, min(23, h))
|
|
||||||
|
|
||||||
|
|
||||||
def _now_dt(*, now_ms: Optional[int] = None, tz_name: Optional[str] = None) -> datetime:
|
|
||||||
tz = ZoneInfo(tz_name or app_timezone_name())
|
|
||||||
if now_ms is None:
|
|
||||||
return datetime.now(tz)
|
|
||||||
return datetime.fromtimestamp(int(now_ms) / 1000, tz=tz)
|
|
||||||
|
|
||||||
|
|
||||||
def force_close_hour_label(bj_hour: Any) -> str:
|
|
||||||
return f"{normalize_force_close_bj_hour(bj_hour):02d}:00"
|
|
||||||
|
|
||||||
|
|
||||||
def force_close_label(bj_hour: Any) -> str:
|
|
||||||
return f"强制清仓 {force_close_hour_label(bj_hour)}"
|
|
||||||
|
|
||||||
|
|
||||||
def is_force_close_active_hour(
|
|
||||||
bj_hour: Any,
|
|
||||||
*,
|
|
||||||
now_ms: Optional[int] = None,
|
|
||||||
tz_name: Optional[str] = None,
|
|
||||||
grace_minutes: Optional[int] = None,
|
|
||||||
) -> bool:
|
|
||||||
"""当前是否处于整点强制清仓执行窗口(整点起 grace 分钟内)."""
|
|
||||||
return is_force_close_executing(
|
|
||||||
bj_hour,
|
|
||||||
now_ms=now_ms,
|
|
||||||
tz_name=tz_name,
|
|
||||||
grace_minutes=grace_minutes,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def is_force_close_executing(
|
|
||||||
bj_hour: Any,
|
|
||||||
*,
|
|
||||||
now_ms: Optional[int] = None,
|
|
||||||
tz_name: Optional[str] = None,
|
|
||||||
grace_minutes: Optional[int] = None,
|
|
||||||
) -> bool:
|
|
||||||
hour = normalize_force_close_bj_hour(bj_hour)
|
|
||||||
now = _now_dt(now_ms=now_ms, tz_name=tz_name)
|
|
||||||
target = now.replace(hour=hour, minute=0, second=0, microsecond=0)
|
|
||||||
if now < target:
|
|
||||||
return False
|
|
||||||
grace = force_close_grace_minutes(grace_minutes)
|
|
||||||
end = target + timedelta(minutes=grace)
|
|
||||||
return now < end
|
|
||||||
|
|
||||||
|
|
||||||
def force_close_blocks_new_open(
|
|
||||||
enabled: bool,
|
|
||||||
bj_hour: Any,
|
|
||||||
*,
|
|
||||||
now_ms: Optional[int] = None,
|
|
||||||
tz_name: Optional[str] = None,
|
|
||||||
grace_minutes: Optional[int] = None,
|
|
||||||
) -> tuple[bool, str]:
|
|
||||||
"""强制清仓执行窗口内禁止新开仓.返回 (是否拦截, 说明文案)."""
|
|
||||||
if not enabled:
|
|
||||||
return False, ""
|
|
||||||
grace = force_close_grace_minutes(grace_minutes)
|
|
||||||
if not is_force_close_executing(
|
|
||||||
bj_hour, now_ms=now_ms, tz_name=tz_name, grace_minutes=grace
|
|
||||||
):
|
|
||||||
return False, ""
|
|
||||||
label = force_close_hour_label(bj_hour)
|
|
||||||
return (
|
|
||||||
True,
|
|
||||||
f"强制清仓窗口内(北京时间 {label} 起 {grace} 分钟),暂不可开仓",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_closed_at_dt(
|
|
||||||
closed_at: Any,
|
|
||||||
*,
|
|
||||||
tz_name: Optional[str] = None,
|
|
||||||
) -> Optional[datetime]:
|
|
||||||
if closed_at is None:
|
|
||||||
return None
|
|
||||||
text = str(closed_at).strip()
|
|
||||||
if not text:
|
|
||||||
return None
|
|
||||||
tz = ZoneInfo(tz_name or app_timezone_name())
|
|
||||||
for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M"):
|
|
||||||
try:
|
|
||||||
return datetime.strptime(text, fmt).replace(tzinfo=tz)
|
|
||||||
except ValueError:
|
|
||||||
continue
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def is_close_at_force_close_window(
|
|
||||||
closed_at: Any,
|
|
||||||
bj_hour: Any,
|
|
||||||
*,
|
|
||||||
grace_minutes: Optional[int] = None,
|
|
||||||
tz_name: Optional[str] = None,
|
|
||||||
) -> bool:
|
|
||||||
"""平仓时刻是否落在北京时间整点强制清仓窗口内."""
|
|
||||||
dt = parse_closed_at_dt(closed_at, tz_name=tz_name)
|
|
||||||
if dt is None:
|
|
||||||
return False
|
|
||||||
hour = normalize_force_close_bj_hour(bj_hour)
|
|
||||||
if dt.hour != hour:
|
|
||||||
return False
|
|
||||||
return dt.minute < force_close_grace_minutes(grace_minutes)
|
|
||||||
|
|
||||||
|
|
||||||
def infer_force_close_result(
|
|
||||||
closed_at: Any,
|
|
||||||
*,
|
|
||||||
enabled: bool,
|
|
||||||
bj_hour: Any,
|
|
||||||
grace_minutes: Optional[int] = None,
|
|
||||||
tz_name: Optional[str] = None,
|
|
||||||
) -> Optional[str]:
|
|
||||||
if not enabled:
|
|
||||||
return None
|
|
||||||
if is_close_at_force_close_window(
|
|
||||||
closed_at, bj_hour, grace_minutes=grace_minutes, tz_name=tz_name
|
|
||||||
):
|
|
||||||
return FORCE_CLOSE_RESULT
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def coerce_force_close_result(
|
|
||||||
result: Optional[str],
|
|
||||||
closed_at: Any,
|
|
||||||
*,
|
|
||||||
enabled: bool,
|
|
||||||
bj_hour: Any,
|
|
||||||
miss_reason: Optional[str] = None,
|
|
||||||
grace_minutes: Optional[int] = None,
|
|
||||||
tz_name: Optional[str] = None,
|
|
||||||
) -> tuple[str, str]:
|
|
||||||
"""同步平仓归类:整点窗口内优先记为强制清仓."""
|
|
||||||
res = (result or "").strip()
|
|
||||||
note = (miss_reason or "").strip()
|
|
||||||
if res == FORCE_CLOSE_RESULT:
|
|
||||||
return res, note
|
|
||||||
fc = infer_force_close_result(
|
|
||||||
closed_at,
|
|
||||||
enabled=enabled,
|
|
||||||
bj_hour=bj_hour,
|
|
||||||
grace_minutes=grace_minutes,
|
|
||||||
tz_name=tz_name,
|
|
||||||
)
|
|
||||||
if not fc:
|
|
||||||
return res, note
|
|
||||||
if not note:
|
|
||||||
note = f"北京时间 {force_close_hour_label(bj_hour)} 整点风控清仓"
|
|
||||||
return fc, note
|
|
||||||
|
|
||||||
|
|
||||||
def apply_force_close_display_result(
|
|
||||||
result: Optional[str],
|
|
||||||
closed_at: Any,
|
|
||||||
*,
|
|
||||||
enabled: bool,
|
|
||||||
bj_hour: Any,
|
|
||||||
grace_minutes: Optional[int] = None,
|
|
||||||
tz_name: Optional[str] = None,
|
|
||||||
) -> str:
|
|
||||||
"""展示层:外部平仓/手动平仓若落在整点窗口,显示为强制清仓."""
|
|
||||||
res = (result or "").strip()
|
|
||||||
if res == FORCE_CLOSE_RESULT:
|
|
||||||
return res
|
|
||||||
fc = infer_force_close_result(
|
|
||||||
closed_at,
|
|
||||||
enabled=enabled,
|
|
||||||
bj_hour=bj_hour,
|
|
||||||
grace_minutes=grace_minutes,
|
|
||||||
tz_name=tz_name,
|
|
||||||
)
|
|
||||||
if fc and (res in ("", "外部平仓", "手动平仓") or res.startswith("外部平仓")):
|
|
||||||
return fc
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
def compute_next_force_close_at_ms(
|
|
||||||
*,
|
|
||||||
bj_hour: Any,
|
|
||||||
now_ms: Optional[int] = None,
|
|
||||||
tz_name: Optional[str] = None,
|
|
||||||
) -> Optional[int]:
|
|
||||||
"""下一次强制清仓时刻(北京时间整点)的 epoch 毫秒."""
|
|
||||||
hour = normalize_force_close_bj_hour(bj_hour)
|
|
||||||
now = _now_dt(now_ms=now_ms, tz_name=tz_name)
|
|
||||||
target = now.replace(hour=hour, minute=0, second=0, microsecond=0)
|
|
||||||
if now >= target:
|
|
||||||
target += timedelta(days=1)
|
|
||||||
return int(target.timestamp() * 1000)
|
|
||||||
|
|
||||||
|
|
||||||
def force_close_remaining_seconds(
|
|
||||||
close_at_ms: Any,
|
|
||||||
*,
|
|
||||||
now_ms: Optional[int] = None,
|
|
||||||
) -> Optional[int]:
|
|
||||||
try:
|
|
||||||
close_at = int(close_at_ms)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
return None
|
|
||||||
now = int(now_ms if now_ms is not None else time.time() * 1000)
|
|
||||||
return max(0, int((close_at - now) / 1000))
|
|
||||||
|
|
||||||
|
|
||||||
def format_force_close_countdown(seconds: Any, *, active: bool = False) -> str:
|
|
||||||
if active:
|
|
||||||
return "执行中"
|
|
||||||
try:
|
|
||||||
sec = max(0, int(seconds))
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
return "--:--:--"
|
|
||||||
h = sec // 3600
|
|
||||||
m = (sec % 3600) // 60
|
|
||||||
s = sec % 60
|
|
||||||
return f"{h:02d}:{m:02d}:{s:02d}"
|
|
||||||
|
|
||||||
|
|
||||||
def build_force_close_state(
|
|
||||||
enabled: bool,
|
|
||||||
bj_hour: Any,
|
|
||||||
*,
|
|
||||||
now_ms: Optional[int] = None,
|
|
||||||
tz_name: Optional[str] = None,
|
|
||||||
has_active_positions: Optional[bool] = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""实例级强制清仓状态(模板 / API 共用)."""
|
|
||||||
grace = force_close_grace_minutes()
|
|
||||||
if not enabled:
|
|
||||||
return {
|
|
||||||
"enabled": False,
|
|
||||||
"bj_hour": normalize_force_close_bj_hour(bj_hour),
|
|
||||||
"hour_label": force_close_hour_label(bj_hour),
|
|
||||||
"label": force_close_label(bj_hour),
|
|
||||||
"grace_minutes": grace,
|
|
||||||
"next_at_ms": None,
|
|
||||||
"remaining_sec": None,
|
|
||||||
"countdown": "",
|
|
||||||
"active": False,
|
|
||||||
"executing": False,
|
|
||||||
}
|
|
||||||
hour = normalize_force_close_bj_hour(bj_hour)
|
|
||||||
executing = is_force_close_executing(
|
|
||||||
hour, now_ms=now_ms, tz_name=tz_name, grace_minutes=grace
|
|
||||||
)
|
|
||||||
active = executing and (has_active_positions is not False)
|
|
||||||
next_at_ms = compute_next_force_close_at_ms(bj_hour=hour, now_ms=now_ms, tz_name=tz_name)
|
|
||||||
rem = force_close_remaining_seconds(next_at_ms, now_ms=now_ms) if next_at_ms else None
|
|
||||||
return {
|
|
||||||
"enabled": True,
|
|
||||||
"bj_hour": hour,
|
|
||||||
"hour_label": force_close_hour_label(hour),
|
|
||||||
"label": force_close_label(hour),
|
|
||||||
"grace_minutes": grace,
|
|
||||||
"next_at_ms": next_at_ms,
|
|
||||||
"remaining_sec": rem,
|
|
||||||
"countdown": format_force_close_countdown(rem, active=active),
|
|
||||||
"active": active,
|
|
||||||
"executing": executing,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def force_close_template_context(
|
|
||||||
enabled: bool,
|
|
||||||
bj_hour: Any,
|
|
||||||
*,
|
|
||||||
now_ms: Optional[int] = None,
|
|
||||||
tz_name: Optional[str] = None,
|
|
||||||
has_active_positions: Optional[bool] = None,
|
|
||||||
) -> dict[str, dict[str, Any]]:
|
|
||||||
return {
|
|
||||||
"force_close": build_force_close_state(
|
|
||||||
enabled,
|
|
||||||
bj_hour,
|
|
||||||
now_ms=now_ms,
|
|
||||||
tz_name=tz_name,
|
|
||||||
has_active_positions=has_active_positions,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def apply_force_close_to_payload(
|
|
||||||
payload: dict[str, Any],
|
|
||||||
*,
|
|
||||||
enabled: bool,
|
|
||||||
bj_hour: Any,
|
|
||||||
now_ms: Optional[int] = None,
|
|
||||||
tz_name: Optional[str] = None,
|
|
||||||
) -> None:
|
|
||||||
"""为 active 持仓 JSON 附加整点强制清仓倒计时."""
|
|
||||||
state = build_force_close_state(
|
|
||||||
enabled,
|
|
||||||
bj_hour,
|
|
||||||
now_ms=now_ms,
|
|
||||||
tz_name=tz_name,
|
|
||||||
has_active_positions=True,
|
|
||||||
)
|
|
||||||
payload["force_close_enabled"] = bool(state["enabled"])
|
|
||||||
payload["force_close_bj_hour"] = state["bj_hour"]
|
|
||||||
payload["force_close_at_ms"] = state["next_at_ms"]
|
|
||||||
payload["force_close_label"] = state["label"] if state["enabled"] else ""
|
|
||||||
payload["force_close_remaining_sec"] = state["remaining_sec"]
|
|
||||||
payload["force_close_countdown"] = state["countdown"]
|
|
||||||
payload["force_close_active"] = bool(state["active"])
|
|
||||||
|
|
||||||
|
|
||||||
def enrich_orders_force_close(
|
|
||||||
orders: list[dict[str, Any]],
|
|
||||||
enabled: bool,
|
|
||||||
bj_hour: Any,
|
|
||||||
*,
|
|
||||||
now_ms: Optional[int] = None,
|
|
||||||
tz_name: Optional[str] = None,
|
|
||||||
) -> None:
|
|
||||||
if not enabled or not orders:
|
|
||||||
return
|
|
||||||
for item in orders:
|
|
||||||
if isinstance(item, dict):
|
|
||||||
apply_force_close_to_payload(
|
|
||||||
item,
|
|
||||||
enabled=enabled,
|
|
||||||
bj_hour=bj_hour,
|
|
||||||
now_ms=now_ms,
|
|
||||||
tz_name=tz_name,
|
|
||||||
)
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
"""实盘人工下单:止盈止损模式(价格 / 百分比 / 固定盈亏比)."""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any, Optional, Tuple
|
|
||||||
|
|
||||||
MANUAL_FIXED_RR_DEFAULT = 1.5
|
|
||||||
|
|
||||||
SLTP_MODE_PRICE = "price"
|
|
||||||
SLTP_MODE_PCT = "pct"
|
|
||||||
SLTP_MODE_FIXED_RR = "fixed_rr"
|
|
||||||
|
|
||||||
OPEN_SLTP_MODES = frozenset({SLTP_MODE_PRICE, SLTP_MODE_PCT, SLTP_MODE_FIXED_RR})
|
|
||||||
ENTRUST_SLTP_MODES = frozenset({SLTP_MODE_PRICE, SLTP_MODE_PCT})
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_open_sltp_mode(raw: Optional[str]) -> str:
|
|
||||||
mode = (raw or SLTP_MODE_FIXED_RR).strip().lower()
|
|
||||||
if mode in OPEN_SLTP_MODES:
|
|
||||||
return mode
|
|
||||||
return SLTP_MODE_PRICE
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_entrust_sltp_mode(raw: Optional[str]) -> str:
|
|
||||||
mode = (raw or SLTP_MODE_PRICE).strip().lower()
|
|
||||||
if mode in ENTRUST_SLTP_MODES:
|
|
||||||
return mode
|
|
||||||
return SLTP_MODE_PRICE
|
|
||||||
|
|
||||||
|
|
||||||
def parse_fixed_rr(raw: Any, *, default: float = MANUAL_FIXED_RR_DEFAULT) -> float:
|
|
||||||
try:
|
|
||||||
v = float(raw)
|
|
||||||
if v > 0:
|
|
||||||
return v
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
pass
|
|
||||||
return float(default)
|
|
||||||
|
|
||||||
|
|
||||||
def calc_tp_from_fixed_rr(
|
|
||||||
direction: str,
|
|
||||||
entry_price: float,
|
|
||||||
stop_loss: float,
|
|
||||||
rr_ratio: float,
|
|
||||||
) -> float:
|
|
||||||
entry = float(entry_price)
|
|
||||||
sl = float(stop_loss)
|
|
||||||
rr = float(rr_ratio)
|
|
||||||
if entry <= 0 or sl <= 0 or rr <= 0:
|
|
||||||
raise ValueError("固定盈亏比参数无效")
|
|
||||||
side = (direction or "long").strip().lower()
|
|
||||||
if side == "short":
|
|
||||||
risk = sl - entry
|
|
||||||
if risk <= 0:
|
|
||||||
raise ValueError("止损方向不合法:做空时止损须高于入场价")
|
|
||||||
return entry - risk * rr
|
|
||||||
risk = entry - sl
|
|
||||||
if risk <= 0:
|
|
||||||
raise ValueError("止损方向不合法:做多时止损须低于入场价")
|
|
||||||
return entry + risk * rr
|
|
||||||
|
|
||||||
|
|
||||||
def _resolve_pct_sltp(direction: str, live_price: float, data: dict[str, Any]) -> Tuple[float, float]:
|
|
||||||
sl_pct = float(data.get("sl_pct") or 0)
|
|
||||||
tp_pct = float(data.get("tp_pct") or 0)
|
|
||||||
if sl_pct <= 0 or tp_pct <= 0:
|
|
||||||
raise ValueError("百分比止盈止损须为正数")
|
|
||||||
sl_ratio = sl_pct / 100.0
|
|
||||||
tp_ratio = tp_pct / 100.0
|
|
||||||
entry = float(live_price)
|
|
||||||
if (direction or "long").strip().lower() == "short":
|
|
||||||
stop_loss = entry * (1 + sl_ratio)
|
|
||||||
take_profit = entry * (1 - tp_ratio)
|
|
||||||
else:
|
|
||||||
stop_loss = entry * (1 - sl_ratio)
|
|
||||||
take_profit = entry * (1 + tp_ratio)
|
|
||||||
return stop_loss, take_profit
|
|
||||||
|
|
||||||
|
|
||||||
def _resolve_price_sltp(
|
|
||||||
data: dict[str, Any],
|
|
||||||
*,
|
|
||||||
fallback_sl: Optional[float] = None,
|
|
||||||
fallback_tp: Optional[float] = None,
|
|
||||||
require_tp: bool = True,
|
|
||||||
) -> Tuple[float, float]:
|
|
||||||
stop_loss = float(data.get("sl") or data.get("stop_loss") or 0)
|
|
||||||
take_profit = float(data.get("tp") or data.get("take_profit") or data.get("tgt") or 0)
|
|
||||||
if stop_loss <= 0 and fallback_sl is not None:
|
|
||||||
stop_loss = float(fallback_sl)
|
|
||||||
if take_profit <= 0 and fallback_tp is not None:
|
|
||||||
take_profit = float(fallback_tp)
|
|
||||||
if stop_loss <= 0:
|
|
||||||
raise ValueError("止损价格须大于 0" if require_tp else "请填写止损价格")
|
|
||||||
if require_tp and take_profit <= 0:
|
|
||||||
raise ValueError("止盈止损价格须大于 0" if fallback_tp is None else "请填写止盈价格,或保留原计划止盈")
|
|
||||||
return stop_loss, take_profit
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_open_sltp_prices(
|
|
||||||
direction: str,
|
|
||||||
live_price: float,
|
|
||||||
sltp_mode: Optional[str],
|
|
||||||
data: dict[str, Any],
|
|
||||||
) -> Tuple[float, float]:
|
|
||||||
"""新开仓 /add_order:支持 price,pct,fixed_rr."""
|
|
||||||
mode = normalize_open_sltp_mode(sltp_mode)
|
|
||||||
if mode == SLTP_MODE_PCT:
|
|
||||||
return _resolve_pct_sltp(direction, live_price, data)
|
|
||||||
if mode == SLTP_MODE_FIXED_RR:
|
|
||||||
stop_loss, _ = _resolve_price_sltp(data, require_tp=False)
|
|
||||||
rr = parse_fixed_rr(data.get("fixed_rr"))
|
|
||||||
take_profit = calc_tp_from_fixed_rr(direction, live_price, stop_loss, rr)
|
|
||||||
return stop_loss, take_profit
|
|
||||||
return _resolve_price_sltp(data, require_tp=True)
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_entrust_sltp_prices(
|
|
||||||
direction: str,
|
|
||||||
live_price: float,
|
|
||||||
sltp_mode: Optional[str],
|
|
||||||
data: dict[str, Any],
|
|
||||||
*,
|
|
||||||
fallback_sl: Optional[float] = None,
|
|
||||||
fallback_tp: Optional[float] = None,
|
|
||||||
) -> Tuple[float, float]:
|
|
||||||
"""持仓委托弹窗:仅 price / pct,不校验盈亏比."""
|
|
||||||
mode = normalize_entrust_sltp_mode(sltp_mode)
|
|
||||||
if mode == SLTP_MODE_PCT:
|
|
||||||
return _resolve_pct_sltp(direction, live_price, data)
|
|
||||||
return _resolve_price_sltp(
|
|
||||||
data,
|
|
||||||
fallback_sl=fallback_sl,
|
|
||||||
fallback_tp=fallback_tp,
|
|
||||||
require_tp=True,
|
|
||||||
)
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
"""三所开仓门禁:账户风控 + 强制清仓窗口 + 仓位/日开仓上限."""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any, Optional
|
|
||||||
|
|
||||||
from lib.trade.daily_open_limit_lib import can_trade_new_open
|
|
||||||
from lib.trade.force_close_lib import force_close_blocks_new_open
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_manual_open_gate(
|
|
||||||
*,
|
|
||||||
time_allows: bool,
|
|
||||||
active_count: int,
|
|
||||||
max_active_positions: int,
|
|
||||||
opens_today: int,
|
|
||||||
hard_limit: int,
|
|
||||||
risk_status: Optional[dict[str, Any]],
|
|
||||||
force_close_enabled: bool,
|
|
||||||
force_close_bj_hour: Any,
|
|
||||||
now_ms: Optional[int] = None,
|
|
||||||
reset_hour: int = 8,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""汇总是否可开仓及按钮旁说明文案."""
|
|
||||||
rs = risk_status if isinstance(risk_status, dict) else {}
|
|
||||||
risk_can = bool(rs.get("can_trade", True))
|
|
||||||
fc_block, fc_note = force_close_blocks_new_open(
|
|
||||||
bool(force_close_enabled),
|
|
||||||
force_close_bj_hour,
|
|
||||||
now_ms=now_ms,
|
|
||||||
)
|
|
||||||
can_trade = can_trade_new_open(
|
|
||||||
time_allows=time_allows,
|
|
||||||
active_count=active_count,
|
|
||||||
max_active_positions=max_active_positions,
|
|
||||||
opens_today=opens_today,
|
|
||||||
hard_limit=hard_limit,
|
|
||||||
extra_blocks=(not risk_can) or fc_block,
|
|
||||||
)
|
|
||||||
note = ""
|
|
||||||
if fc_block and fc_note:
|
|
||||||
note = fc_note
|
|
||||||
elif not risk_can:
|
|
||||||
note = str(rs.get("reason") or "账户冷静期/日冻结中,暂不可开仓")
|
|
||||||
elif not time_allows:
|
|
||||||
note = f"未到北京时间 {int(reset_hour)}:00,暂不可开仓"
|
|
||||||
elif int(active_count) >= int(max_active_positions):
|
|
||||||
note = f"已达最大持仓数({int(active_count)}/{int(max_active_positions)}),暂不可开仓"
|
|
||||||
elif int(hard_limit) > 0 and int(opens_today) >= int(hard_limit):
|
|
||||||
note = (
|
|
||||||
f"本交易日开仓已达上限({int(opens_today)}/{int(hard_limit)}),"
|
|
||||||
f"次日北京时间 {int(reset_hour)}:00 后恢复"
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
"can_trade": can_trade,
|
|
||||||
"open_block_note": note if not can_trade else "",
|
|
||||||
"force_close_blocks": fc_block,
|
|
||||||
}
|
|
||||||
@@ -1,461 +0,0 @@
|
|||||||
"""实时持仓展示:开仓快照盈亏比,交易所止损是否已保本."""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any, Callable, Optional
|
|
||||||
|
|
||||||
|
|
||||||
def _positive_float(value: Any) -> Optional[float]:
|
|
||||||
try:
|
|
||||||
v = float(value)
|
|
||||||
return v if v > 0 else None
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def snapshot_stop_loss(initial_stop_loss: Any, stop_loss: Any) -> Optional[float]:
|
|
||||||
"""展示盈亏比 / 交易记录时优先用开仓时止损快照,不用后续改单后的止损."""
|
|
||||||
sl = _positive_float(initial_stop_loss)
|
|
||||||
if sl is not None:
|
|
||||||
return sl
|
|
||||||
return _positive_float(stop_loss)
|
|
||||||
|
|
||||||
|
|
||||||
def monitor_open_stop_loss(row: Any) -> Optional[float]:
|
|
||||||
"""从 order_monitors 行取开仓止损快照."""
|
|
||||||
try:
|
|
||||||
keys = row.keys() if hasattr(row, "keys") else ()
|
|
||||||
except Exception:
|
|
||||||
keys = ()
|
|
||||||
init = row["initial_stop_loss"] if "initial_stop_loss" in keys else None
|
|
||||||
cur = row["stop_loss"] if "stop_loss" in keys else None
|
|
||||||
if init is None and isinstance(row, dict):
|
|
||||||
init = row.get("initial_stop_loss")
|
|
||||||
cur = row.get("stop_loss")
|
|
||||||
return snapshot_stop_loss(init, cur)
|
|
||||||
|
|
||||||
|
|
||||||
def snapshot_rr(
|
|
||||||
calc_rr_ratio_fn: Callable[..., Optional[float]],
|
|
||||||
direction: str,
|
|
||||||
trigger_price: Any,
|
|
||||||
initial_stop_loss: Any,
|
|
||||||
stop_loss: Any,
|
|
||||||
take_profit: Any,
|
|
||||||
) -> Optional[float]:
|
|
||||||
entry = _positive_float(trigger_price)
|
|
||||||
sl = snapshot_stop_loss(initial_stop_loss, stop_loss)
|
|
||||||
tp = _positive_float(take_profit)
|
|
||||||
if entry is None or sl is None or tp is None:
|
|
||||||
return None
|
|
||||||
return calc_rr_ratio_fn(direction or "long", entry, sl, tp)
|
|
||||||
|
|
||||||
|
|
||||||
def tpsl_slot_trigger_price(slot: Any) -> Optional[float]:
|
|
||||||
if not isinstance(slot, dict):
|
|
||||||
return None
|
|
||||||
for key in ("trigger_price", "trigger_display"):
|
|
||||||
v = _positive_float(slot.get(key))
|
|
||||||
if v is not None:
|
|
||||||
return v
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def stop_is_profit_protecting(direction: str, entry_price: Any, stop_loss: Any) -> bool:
|
|
||||||
"""
|
|
||||||
止损是否已在盈利侧(保本/锁盈),不再适用「开仓盈亏比」风控.
|
|
||||||
做空:止损 < 成交价;做多:止损 > 成交价.
|
|
||||||
"""
|
|
||||||
entry = _positive_float(entry_price)
|
|
||||||
sl = _positive_float(stop_loss)
|
|
||||||
if entry is None or sl is None:
|
|
||||||
return False
|
|
||||||
d = (direction or "long").strip().lower()
|
|
||||||
if d == "short":
|
|
||||||
return sl < entry
|
|
||||||
return sl > entry
|
|
||||||
|
|
||||||
|
|
||||||
def tpsl_update_passes_rr_gate(
|
|
||||||
direction: str,
|
|
||||||
entry_price: Any,
|
|
||||||
stop_loss: Any,
|
|
||||||
take_profit: Any,
|
|
||||||
min_rr: float,
|
|
||||||
calc_rr_ratio_fn: Callable[..., Optional[float]],
|
|
||||||
) -> tuple[bool, Optional[str]]:
|
|
||||||
"""持仓委托改价:盈利侧止损跳过最低盈亏比;否则按开仓价几何校验."""
|
|
||||||
if stop_is_profit_protecting(direction, entry_price, stop_loss):
|
|
||||||
return True, None
|
|
||||||
rr = calc_rr_ratio_fn(direction or "long", entry_price, stop_loss, take_profit)
|
|
||||||
if rr is not None and rr >= float(min_rr):
|
|
||||||
return True, None
|
|
||||||
rr_txt = f"{rr:.4f}" if rr is not None else "无法计算"
|
|
||||||
return False, f"计划盈亏比 {rr_txt}:1 低于最低要求 {min_rr}:1(盈利侧保本止损不受此限)"
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_breakeven_entry_price(entry_price: Any, avg_entry_price: Any = None) -> Optional[float]:
|
|
||||||
"""保本判断基准价:有持仓加权均价时优先(滚仓后),否则用首仓成交价."""
|
|
||||||
avg = _positive_float(avg_entry_price)
|
|
||||||
if avg is not None:
|
|
||||||
return avg
|
|
||||||
return _positive_float(entry_price)
|
|
||||||
|
|
||||||
|
|
||||||
def stale_breakeven_armed(direction: str, entry_price: Any, stop_loss: Any, breakeven_armed: Any) -> bool:
|
|
||||||
"""止损已回到亏损侧时 breakeven_armed 视为过期(如滚仓下移止损)."""
|
|
||||||
try:
|
|
||||||
armed = int(breakeven_armed or 0) != 0
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
return False
|
|
||||||
if not armed:
|
|
||||||
return False
|
|
||||||
return not stop_is_profit_protecting(direction, entry_price, stop_loss)
|
|
||||||
|
|
||||||
|
|
||||||
def is_sl_breakeven_secured(direction: str, entry_price: Any, exchange_sl_price: Any) -> bool:
|
|
||||||
"""
|
|
||||||
交易所当前止损相对开仓成交价是否已保本.
|
|
||||||
做多:止损 >= 成交价;做空:止损 <= 成交价.
|
|
||||||
"""
|
|
||||||
entry = _positive_float(entry_price)
|
|
||||||
sl = _positive_float(exchange_sl_price)
|
|
||||||
if entry is None or sl is None:
|
|
||||||
return False
|
|
||||||
d = (direction or "long").strip().lower()
|
|
||||||
if d == "short":
|
|
||||||
return sl <= entry
|
|
||||||
return sl >= entry
|
|
||||||
|
|
||||||
|
|
||||||
def sl_breakeven_from_exchange_tpsl(
|
|
||||||
direction: str,
|
|
||||||
entry_price: Any,
|
|
||||||
exchange_tpsl: Any,
|
|
||||||
) -> bool:
|
|
||||||
if not isinstance(exchange_tpsl, dict):
|
|
||||||
return False
|
|
||||||
sl_px = tpsl_slot_trigger_price(exchange_tpsl.get("sl"))
|
|
||||||
if sl_px is None:
|
|
||||||
return False
|
|
||||||
return is_sl_breakeven_secured(direction, entry_price, sl_px)
|
|
||||||
|
|
||||||
|
|
||||||
def enrich_order_display_fields(item: dict[str, Any], calc_rr_ratio_fn: Callable[..., Optional[float]]) -> dict[str, Any]:
|
|
||||||
item["rr_ratio"] = snapshot_rr(
|
|
||||||
calc_rr_ratio_fn,
|
|
||||||
item.get("direction") or "long",
|
|
||||||
item.get("trigger_price"),
|
|
||||||
item.get("initial_stop_loss"),
|
|
||||||
item.get("stop_loss"),
|
|
||||||
item.get("take_profit"),
|
|
||||||
)
|
|
||||||
return item
|
|
||||||
|
|
||||||
|
|
||||||
def apply_order_live_price_display(
|
|
||||||
payload: dict[str, Any],
|
|
||||||
symbol: Any,
|
|
||||||
ticker_price: Any,
|
|
||||||
exchange_mark_price: Any,
|
|
||||||
format_price_fn: Callable[[Any, Any], str],
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""标记价/现价展示:与交易所 price_to_precision 对齐,避免前端 toFixed(8)."""
|
|
||||||
px_for_fmt = ticker_price
|
|
||||||
mark_raw = exchange_mark_price
|
|
||||||
if mark_raw is not None:
|
|
||||||
try:
|
|
||||||
px_for_fmt = float(mark_raw)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
pass
|
|
||||||
px_disp = format_price_fn(symbol, px_for_fmt)
|
|
||||||
payload["price_display"] = px_disp
|
|
||||||
if mark_raw is not None:
|
|
||||||
try:
|
|
||||||
payload["exchange_mark_price_display"] = format_price_fn(symbol, float(mark_raw))
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
payload["exchange_mark_price_display"] = px_disp
|
|
||||||
else:
|
|
||||||
payload["exchange_mark_price_display"] = None
|
|
||||||
return payload
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_live_tpsl_prices(
|
|
||||||
plan_sl: Any,
|
|
||||||
plan_tp: Any,
|
|
||||||
exchange_tpsl: Any,
|
|
||||||
) -> tuple[Optional[float], Optional[float], Optional[float], Optional[float]]:
|
|
||||||
"""返回 (展示用止损, 展示用止盈, 交易所止损, 交易所止盈)."""
|
|
||||||
ex_sl = ex_tp = None
|
|
||||||
if isinstance(exchange_tpsl, dict):
|
|
||||||
ex_sl = tpsl_slot_trigger_price(exchange_tpsl.get("sl"))
|
|
||||||
ex_tp = tpsl_slot_trigger_price(exchange_tpsl.get("tp"))
|
|
||||||
disp_sl = ex_sl if ex_sl is not None else _positive_float(plan_sl)
|
|
||||||
disp_tp = ex_tp if ex_tp is not None else _positive_float(plan_tp)
|
|
||||||
return disp_sl, disp_tp, ex_sl, ex_tp
|
|
||||||
|
|
||||||
|
|
||||||
def calc_risk_fraction(direction: str, entry_price: Any, stop_loss: Any) -> Optional[float]:
|
|
||||||
"""|入场-止损|/入场;盈利侧止损返回 0."""
|
|
||||||
entry = _positive_float(entry_price)
|
|
||||||
sl = _positive_float(stop_loss)
|
|
||||||
if entry is None or sl is None:
|
|
||||||
return None
|
|
||||||
d = (direction or "long").strip().lower()
|
|
||||||
if d == "short":
|
|
||||||
risk = sl - entry
|
|
||||||
else:
|
|
||||||
risk = entry - sl
|
|
||||||
if risk <= 0:
|
|
||||||
return 0.0
|
|
||||||
return risk / entry
|
|
||||||
|
|
||||||
|
|
||||||
def calc_latest_risk_amount(
|
|
||||||
direction: str,
|
|
||||||
entry_price: Any,
|
|
||||||
stop_loss: Any,
|
|
||||||
*,
|
|
||||||
margin_capital: Any = None,
|
|
||||||
leverage: Any = None,
|
|
||||||
exchange_notional: Any = None,
|
|
||||||
contracts: Any = None,
|
|
||||||
contract_size: Any = None,
|
|
||||||
mark_price: Any = None,
|
|
||||||
funds_decimals: int = 2,
|
|
||||||
) -> Optional[float]:
|
|
||||||
"""按当前止损与持仓名义价值估算最新风险(U)."""
|
|
||||||
rf = calc_risk_fraction(direction, entry_price, stop_loss)
|
|
||||||
if rf is None:
|
|
||||||
return None
|
|
||||||
if rf <= 0:
|
|
||||||
return 0.0
|
|
||||||
notional = _positive_float(exchange_notional)
|
|
||||||
if notional is None:
|
|
||||||
try:
|
|
||||||
mc = float(margin_capital or 0)
|
|
||||||
lev = float(leverage or 0)
|
|
||||||
if mc > 0 and lev > 0:
|
|
||||||
notional = mc * lev
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
pass
|
|
||||||
if notional is None:
|
|
||||||
try:
|
|
||||||
c = abs(float(contracts or 0))
|
|
||||||
cs = float(contract_size or 1)
|
|
||||||
if cs <= 0:
|
|
||||||
cs = 1.0
|
|
||||||
px = _positive_float(mark_price) or _positive_float(entry_price)
|
|
||||||
if c > 0 and px is not None:
|
|
||||||
notional = c * cs * px
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
pass
|
|
||||||
if notional is None or notional <= 0:
|
|
||||||
return None
|
|
||||||
return round(notional * rf, funds_decimals)
|
|
||||||
|
|
||||||
|
|
||||||
def order_monitor_tpsl_needs_sync(
|
|
||||||
plan_sl: Any,
|
|
||||||
plan_tp: Any,
|
|
||||||
exchange_tpsl: Any,
|
|
||||||
*,
|
|
||||||
eps: float = 1e-12,
|
|
||||||
) -> tuple[Optional[float], Optional[float], bool]:
|
|
||||||
"""若交易所 TP/SL 与库中不一致,返回应写回的 (sl, tp) 及是否需更新."""
|
|
||||||
_, _, ex_sl, ex_tp = resolve_live_tpsl_prices(plan_sl, plan_tp, exchange_tpsl)
|
|
||||||
try:
|
|
||||||
cur_sl = float(plan_sl or 0)
|
|
||||||
cur_tp = float(plan_tp or 0)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
cur_sl, cur_tp = 0.0, 0.0
|
|
||||||
new_sl = ex_sl if ex_sl is not None else cur_sl
|
|
||||||
new_tp = ex_tp if ex_tp is not None else cur_tp
|
|
||||||
changed = (
|
|
||||||
(ex_sl is not None and abs(new_sl - cur_sl) > eps)
|
|
||||||
or (ex_tp is not None and abs(new_tp - cur_tp) > eps)
|
|
||||||
)
|
|
||||||
return new_sl, new_tp, changed
|
|
||||||
|
|
||||||
|
|
||||||
def apply_order_price_display_fields(
|
|
||||||
payload: dict[str, Any],
|
|
||||||
*,
|
|
||||||
direction: str,
|
|
||||||
entry_price: Any,
|
|
||||||
initial_stop_loss: Any,
|
|
||||||
stop_loss: Any,
|
|
||||||
take_profit: Any,
|
|
||||||
calc_rr_ratio_fn: Callable[..., Optional[float]],
|
|
||||||
exchange_tpsl: Any = None,
|
|
||||||
format_price_fn: Optional[Callable[[Any, Any], str]] = None,
|
|
||||||
symbol: Any = None,
|
|
||||||
margin_capital: Any = None,
|
|
||||||
leverage: Any = None,
|
|
||||||
exchange_notional: Any = None,
|
|
||||||
contracts: Any = None,
|
|
||||||
contract_size: Any = None,
|
|
||||||
mark_price: Any = None,
|
|
||||||
avg_entry_price: Any = None,
|
|
||||||
funds_decimals: int = 2,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
disp_sl, disp_tp, _, _ = resolve_live_tpsl_prices(stop_loss, take_profit, exchange_tpsl)
|
|
||||||
payload["stop_loss_raw"] = _positive_float(stop_loss)
|
|
||||||
payload["take_profit_raw"] = _positive_float(take_profit)
|
|
||||||
payload["rr_ratio"] = snapshot_rr(
|
|
||||||
calc_rr_ratio_fn,
|
|
||||||
direction,
|
|
||||||
entry_price,
|
|
||||||
initial_stop_loss,
|
|
||||||
stop_loss,
|
|
||||||
take_profit,
|
|
||||||
)
|
|
||||||
risk_entry = resolve_breakeven_entry_price(entry_price, avg_entry_price)
|
|
||||||
payload["avg_entry_price"] = risk_entry
|
|
||||||
payload["sl_breakeven_secured"] = sl_breakeven_from_exchange_tpsl(
|
|
||||||
direction, risk_entry, exchange_tpsl
|
|
||||||
)
|
|
||||||
payload["stop_loss"] = disp_sl
|
|
||||||
payload["take_profit"] = disp_tp
|
|
||||||
if disp_sl is not None and disp_tp is not None:
|
|
||||||
payload["display_rr_ratio"] = calc_rr_ratio_fn(
|
|
||||||
direction or "long", entry_price, disp_sl, disp_tp
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
payload["display_rr_ratio"] = None
|
|
||||||
if contracts is not None:
|
|
||||||
try:
|
|
||||||
from lib.market.position_metrics_lib import normalize_contracts_qty
|
|
||||||
|
|
||||||
c = normalize_contracts_qty(contracts)
|
|
||||||
if c > 0:
|
|
||||||
payload["contracts"] = c
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
pass
|
|
||||||
payload["latest_risk_amount"] = calc_latest_risk_amount(
|
|
||||||
direction,
|
|
||||||
risk_entry,
|
|
||||||
disp_sl if disp_sl is not None else stop_loss,
|
|
||||||
margin_capital=margin_capital,
|
|
||||||
leverage=leverage,
|
|
||||||
exchange_notional=exchange_notional,
|
|
||||||
contracts=payload.get("contracts") if payload.get("contracts") is not None else contracts,
|
|
||||||
contract_size=contract_size,
|
|
||||||
mark_price=mark_price,
|
|
||||||
funds_decimals=funds_decimals,
|
|
||||||
)
|
|
||||||
tp_for_reward = disp_tp if disp_tp is not None else _positive_float(take_profit)
|
|
||||||
qty_for_reward = payload.get("contracts")
|
|
||||||
if qty_for_reward is None and contracts is not None:
|
|
||||||
try:
|
|
||||||
qty_for_reward = abs(float(contracts))
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
qty_for_reward = None
|
|
||||||
if risk_entry is not None and tp_for_reward is not None and qty_for_reward:
|
|
||||||
try:
|
|
||||||
def reward_at_tp_usdt(direction, entry, tp, contracts, contract_size, fee_rate=0.0):
|
|
||||||
try:
|
|
||||||
d = (direction or "long").lower()
|
|
||||||
e, t, c, cs = float(entry), float(tp), float(contracts), float(contract_size or 1)
|
|
||||||
if e <= 0 or t <= 0 or c <= 0:
|
|
||||||
return None
|
|
||||||
raw = (t - e) * c * cs if d == "long" else (e - t) * c * cs
|
|
||||||
return round(raw, 2)
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
reward = reward_at_tp_usdt(
|
|
||||||
direction,
|
|
||||||
risk_entry,
|
|
||||||
tp_for_reward,
|
|
||||||
float(qty_for_reward),
|
|
||||||
contract_size=float(contract_size or 1.0),
|
|
||||||
)
|
|
||||||
payload["reward_at_tp_usdt"] = (
|
|
||||||
round(reward, funds_decimals) if reward is not None else None
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
payload["reward_at_tp_usdt"] = None
|
|
||||||
else:
|
|
||||||
payload["reward_at_tp_usdt"] = None
|
|
||||||
if format_price_fn is not None and symbol is not None:
|
|
||||||
payload["stop_loss_display"] = (
|
|
||||||
format_price_fn(symbol, disp_sl) if disp_sl is not None else "—"
|
|
||||||
)
|
|
||||||
payload["take_profit_display"] = (
|
|
||||||
format_price_fn(symbol, disp_tp) if disp_tp is not None else "—"
|
|
||||||
)
|
|
||||||
mark_raw = mark_price if mark_price is not None else None
|
|
||||||
if mark_raw is not None and format_price_fn is not None and symbol is not None:
|
|
||||||
try:
|
|
||||||
payload["exchange_mark_price_display"] = format_price_fn(symbol, float(mark_raw))
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
payload["exchange_mark_price_display"] = None
|
|
||||||
return payload
|
|
||||||
|
|
||||||
|
|
||||||
def enrich_active_monitor_tpsl_json(
|
|
||||||
row: Any,
|
|
||||||
stop_loss: Any,
|
|
||||||
take_profit: Any,
|
|
||||||
exchange_tpsl: Any,
|
|
||||||
*,
|
|
||||||
position_row: Any = None,
|
|
||||||
exchange_notional: Any = None,
|
|
||||||
contracts: Any = None,
|
|
||||||
contract_size: float = 1.0,
|
|
||||||
mark_price: Any = None,
|
|
||||||
calc_rr_ratio_fn: Callable[..., Optional[float]],
|
|
||||||
format_price_fn: Optional[Callable[[Any, Any], str]] = None,
|
|
||||||
symbol: Any = None,
|
|
||||||
funds_decimals: int = 2,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""place_tpsl 响应:展示用 TP/SL,最新风险,当前盈亏比."""
|
|
||||||
def _row_val(key: str, default=None):
|
|
||||||
try:
|
|
||||||
if hasattr(row, "keys") and key in row.keys():
|
|
||||||
return row[key]
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
if isinstance(row, dict):
|
|
||||||
return row.get(key, default)
|
|
||||||
return default
|
|
||||||
|
|
||||||
direction = _row_val("direction") or "long"
|
|
||||||
entry = _row_val("trigger_price")
|
|
||||||
init_sl = _row_val("initial_stop_loss")
|
|
||||||
margin = _row_val("margin_capital")
|
|
||||||
leverage = _row_val("leverage")
|
|
||||||
if position_row is not None:
|
|
||||||
from lib.market.position_metrics_lib import parse_position_entry_price, position_contracts
|
|
||||||
|
|
||||||
live_c = position_contracts(position_row)
|
|
||||||
if abs(live_c) >= 1e-12:
|
|
||||||
contracts = abs(live_c)
|
|
||||||
avg_entry = parse_position_entry_price(position_row)
|
|
||||||
else:
|
|
||||||
avg_entry = None
|
|
||||||
payload: dict[str, Any] = {
|
|
||||||
"stop_loss": stop_loss,
|
|
||||||
"take_profit": take_profit,
|
|
||||||
}
|
|
||||||
apply_order_price_display_fields(
|
|
||||||
payload,
|
|
||||||
direction=direction,
|
|
||||||
entry_price=entry,
|
|
||||||
initial_stop_loss=init_sl,
|
|
||||||
stop_loss=stop_loss,
|
|
||||||
take_profit=take_profit,
|
|
||||||
calc_rr_ratio_fn=calc_rr_ratio_fn,
|
|
||||||
exchange_tpsl=exchange_tpsl,
|
|
||||||
format_price_fn=format_price_fn,
|
|
||||||
symbol=symbol or _row_val("symbol"),
|
|
||||||
margin_capital=margin,
|
|
||||||
leverage=leverage,
|
|
||||||
exchange_notional=exchange_notional,
|
|
||||||
contracts=contracts,
|
|
||||||
contract_size=contract_size,
|
|
||||||
mark_price=mark_price,
|
|
||||||
avg_entry_price=avg_entry,
|
|
||||||
funds_decimals=funds_decimals,
|
|
||||||
)
|
|
||||||
return payload
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
"""
|
|
||||||
三所共用:计仓模式 risk(以损定仓)| full_margin(全仓杠杆).
|
|
||||||
仅 env POSITION_SIZING_MODE 切换;须无持仓(由部署流程保证).
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import os
|
|
||||||
from typing import Any, Optional, Tuple
|
|
||||||
|
|
||||||
MODE_RISK = "risk"
|
|
||||||
MODE_FULL_MARGIN = "full_margin"
|
|
||||||
VALID_MODES = frozenset({MODE_RISK, MODE_FULL_MARGIN})
|
|
||||||
|
|
||||||
OPEN_SOURCE_MANUAL = "manual"
|
|
||||||
# 历史兼容常量(策略/关键位自动单已移除,assert 仍可引用)
|
|
||||||
OPEN_SOURCE_KEY_AUTO = "key_auto"
|
|
||||||
OPEN_SOURCE_KEY_FIB = "key_fib"
|
|
||||||
OPEN_SOURCE_KEY_TRIGGER = "key_trigger"
|
|
||||||
OPEN_SOURCE_TREND = "trend"
|
|
||||||
OPEN_SOURCE_ROLL = "roll"
|
|
||||||
|
|
||||||
FULL_MARGIN_BLOCKED_SOURCES = frozenset()
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_position_sizing_mode(raw: Optional[str]) -> str:
|
|
||||||
v = (raw or MODE_RISK).strip().lower()
|
|
||||||
if v in ("full", "full_margin", "fullmargin", "全仓", "全仓杠杆"):
|
|
||||||
return MODE_FULL_MARGIN
|
|
||||||
return MODE_RISK if v in ("risk", "r", "以损定仓", "") else MODE_RISK
|
|
||||||
|
|
||||||
|
|
||||||
def load_position_sizing_mode(env: Optional[dict] = None) -> str:
|
|
||||||
e = env if env is not None else os.environ
|
|
||||||
return normalize_position_sizing_mode(e.get("POSITION_SIZING_MODE"))
|
|
||||||
|
|
||||||
|
|
||||||
def is_full_margin_mode(mode: str) -> bool:
|
|
||||||
return normalize_position_sizing_mode(mode) == MODE_FULL_MARGIN
|
|
||||||
|
|
||||||
|
|
||||||
def mode_label_zh(mode: str) -> str:
|
|
||||||
return "全仓杠杆" if is_full_margin_mode(mode) else "以损定仓"
|
|
||||||
|
|
||||||
|
|
||||||
def leverage_for_full_margin(symbol: str, btc_leverage: int, alt_leverage: int) -> int:
|
|
||||||
sym = (symbol or "").strip().upper()
|
|
||||||
if sym.startswith("BTC") or sym.startswith("ETH"):
|
|
||||||
return max(1, int(btc_leverage or 10))
|
|
||||||
return max(1, int(alt_leverage or 5))
|
|
||||||
|
|
||||||
|
|
||||||
def round_funds(value: float, decimals: int = 2) -> float:
|
|
||||||
return round(float(value), int(decimals))
|
|
||||||
|
|
||||||
|
|
||||||
def risk_percent_for_storage(mode: str, risk_percent: float) -> Optional[float]:
|
|
||||||
"""全仓杠杆:库内不写风险百分比(仅 risk_amount U)."""
|
|
||||||
if is_full_margin_mode(mode):
|
|
||||||
return None
|
|
||||||
return risk_percent
|
|
||||||
|
|
||||||
|
|
||||||
def format_risk_display_text(
|
|
||||||
mode: str,
|
|
||||||
risk_percent: Optional[float],
|
|
||||||
risk_amount: Optional[float],
|
|
||||||
*,
|
|
||||||
decimals: int = 2,
|
|
||||||
) -> str:
|
|
||||||
"""持仓/通知「风险」文案:全仓仅 U;以损定仓为 %≈U."""
|
|
||||||
amt: Optional[float] = None
|
|
||||||
if risk_amount is not None and risk_amount != "":
|
|
||||||
try:
|
|
||||||
amt = float(risk_amount)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
amt = None
|
|
||||||
if is_full_margin_mode(mode):
|
|
||||||
if amt is None:
|
|
||||||
return "—"
|
|
||||||
return f"{round_funds(amt, decimals)}U"
|
|
||||||
pct: Optional[float] = None
|
|
||||||
if risk_percent is not None and risk_percent != "":
|
|
||||||
try:
|
|
||||||
pct = float(risk_percent)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
pct = None
|
|
||||||
pct_txt = f"{pct:g}" if pct is not None else "—"
|
|
||||||
amt_txt = round_funds(amt, decimals) if amt is not None else "—"
|
|
||||||
return f"{pct_txt}%≈{amt_txt}U"
|
|
||||||
|
|
||||||
|
|
||||||
def assert_open_source_allowed(mode: str, source: str) -> Tuple[bool, str]:
|
|
||||||
if not is_full_margin_mode(mode):
|
|
||||||
return True, ""
|
|
||||||
src = (source or "").strip().lower()
|
|
||||||
if src in FULL_MARGIN_BLOCKED_SOURCES:
|
|
||||||
return False, (
|
|
||||||
"当前为全仓杠杆模式(POSITION_SIZING_MODE=full_margin),"
|
|
||||||
"不允许关键位突破/斐波自动开仓,趋势回调与顺势加仓;"
|
|
||||||
"仅支持实盘人工下单与阻力/支撑提醒."
|
|
||||||
)
|
|
||||||
return True, ""
|
|
||||||
|
|
||||||
|
|
||||||
def full_margin_requires_flat_position(active_count: int) -> Tuple[bool, str]:
|
|
||||||
if active_count > 0:
|
|
||||||
return False, "全仓杠杆模式仅允许单仓且无其它持仓,请先平仓后再开仓"
|
|
||||||
return True, ""
|
|
||||||
|
|
||||||
|
|
||||||
def compute_full_margin_sizing(
|
|
||||||
*,
|
|
||||||
symbol: str,
|
|
||||||
available_usdt: float,
|
|
||||||
capital_base: float,
|
|
||||||
buffer_ratio: float,
|
|
||||||
btc_leverage: int,
|
|
||||||
alt_leverage: int,
|
|
||||||
funds_decimals: int = 2,
|
|
||||||
) -> Tuple[Optional[dict[str, Any]], Optional[str]]:
|
|
||||||
if available_usdt is None or float(available_usdt) <= 0:
|
|
||||||
return None, "全仓杠杆:无法读取合约账户可用保证金"
|
|
||||||
lev = leverage_for_full_margin(symbol, btc_leverage, alt_leverage)
|
|
||||||
margin = round_funds(float(available_usdt) * float(buffer_ratio), funds_decimals)
|
|
||||||
if margin <= 0:
|
|
||||||
return None, "全仓杠杆:可用保证金不足"
|
|
||||||
notional = round_funds(margin * lev, funds_decimals)
|
|
||||||
ratio = round(margin / float(capital_base) * 100, 2) if capital_base else 0.0
|
|
||||||
return {
|
|
||||||
"margin_capital": margin,
|
|
||||||
"leverage": lev,
|
|
||||||
"notional_value": notional,
|
|
||||||
"position_ratio": ratio,
|
|
||||||
"mode": MODE_FULL_MARGIN,
|
|
||||||
}, None
|
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
"""平仓交易:交易所口径双边成交额与手续费(三所共用聚合逻辑)."""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any, Callable, Optional
|
|
||||||
|
|
||||||
|
|
||||||
def _coerce_ts_ms(raw: Any) -> int | None:
|
|
||||||
if raw in (None, ""):
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
v = int(raw)
|
|
||||||
return v if v > 1_000_000_000_000 else v * 1000
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def quote_turnover_usdt_from_fill(trade: dict, *, contract_size: float = 1.0) -> float:
|
|
||||||
"""单笔成交的报价币成交额(USDT 口径)."""
|
|
||||||
info = trade.get("info") or {}
|
|
||||||
if not isinstance(info, dict):
|
|
||||||
info = {}
|
|
||||||
for key in ("quoteQty", "quote_qty", "fillNotionalUsd", "notional"):
|
|
||||||
try:
|
|
||||||
v = float(info.get(key) or 0)
|
|
||||||
if v > 0:
|
|
||||||
return abs(v)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
cost = float(trade.get("cost") or 0)
|
|
||||||
if cost > 0:
|
|
||||||
return abs(cost)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
price = float(trade.get("price") or 0)
|
|
||||||
amount = float(trade.get("amount") or 0) * float(contract_size or 1.0)
|
|
||||||
if price > 0 and amount > 0:
|
|
||||||
return abs(price * amount)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
pass
|
|
||||||
return 0.0
|
|
||||||
|
|
||||||
|
|
||||||
def commission_usdt_from_fill(trade: dict) -> float:
|
|
||||||
"""单笔成交手续费(正数表示成本)."""
|
|
||||||
fee = trade.get("fee")
|
|
||||||
if isinstance(fee, dict):
|
|
||||||
try:
|
|
||||||
cost = float(fee.get("cost") or 0)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
cost = 0.0
|
|
||||||
if cost != 0:
|
|
||||||
cur = str(fee.get("currency") or "USDT").upper()
|
|
||||||
if cur in ("USDT", "USD", "BUSD", "USDC"):
|
|
||||||
return abs(cost)
|
|
||||||
return abs(cost)
|
|
||||||
info = trade.get("info") or {}
|
|
||||||
if isinstance(info, dict):
|
|
||||||
for key in ("fee", "commission", "fillFee"):
|
|
||||||
try:
|
|
||||||
v = float(info.get(key) or 0)
|
|
||||||
if v != 0:
|
|
||||||
return abs(v)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
continue
|
|
||||||
return 0.0
|
|
||||||
|
|
||||||
|
|
||||||
def aggregate_bilateral_stats(
|
|
||||||
fills: list[dict],
|
|
||||||
*,
|
|
||||||
contract_size: float = 1.0,
|
|
||||||
) -> dict[str, float] | None:
|
|
||||||
"""双边成交额 = 开+平所有相关 fill 的报价币成交额之和;手续费 = fill fee 之和."""
|
|
||||||
if not fills:
|
|
||||||
return None
|
|
||||||
turnover = 0.0
|
|
||||||
commission = 0.0
|
|
||||||
for t in fills:
|
|
||||||
turnover += quote_turnover_usdt_from_fill(t, contract_size=contract_size)
|
|
||||||
commission += commission_usdt_from_fill(t)
|
|
||||||
if turnover <= 0 and commission <= 0:
|
|
||||||
return None
|
|
||||||
return {
|
|
||||||
"exchange_turnover_usdt": round(turnover, 4),
|
|
||||||
"exchange_commission_usdt": round(commission, 4),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def filter_position_lifecycle_fills(
|
|
||||||
trades: list[dict],
|
|
||||||
direction: str,
|
|
||||||
open_ms: int | None,
|
|
||||||
close_ms: int | None,
|
|
||||||
*,
|
|
||||||
hedge_mode: bool = False,
|
|
||||||
close_buffer_ms: int = 15 * 60 * 1000,
|
|
||||||
) -> list[dict]:
|
|
||||||
"""
|
|
||||||
持仓生命周期内 fill:多=开买+平卖;空=开卖+平买.
|
|
||||||
hedge_mode 时按 posSide 与 direction 过滤.
|
|
||||||
"""
|
|
||||||
direction = (direction or "long").strip().lower()
|
|
||||||
open_side = "buy" if direction == "long" else "sell"
|
|
||||||
close_side = "sell" if direction == "long" else "buy"
|
|
||||||
allowed_sides = {open_side, close_side}
|
|
||||||
upper = int(close_ms) + int(close_buffer_ms) if close_ms else None
|
|
||||||
out: list[dict] = []
|
|
||||||
for t in trades or []:
|
|
||||||
side = (t.get("side") or "").lower()
|
|
||||||
if side not in allowed_sides:
|
|
||||||
continue
|
|
||||||
ts = _coerce_ts_ms(t.get("timestamp"))
|
|
||||||
if ts is None:
|
|
||||||
continue
|
|
||||||
if open_ms and ts < int(open_ms) - 60_000:
|
|
||||||
continue
|
|
||||||
if upper and ts > upper:
|
|
||||||
continue
|
|
||||||
if hedge_mode:
|
|
||||||
info = t.get("info") or {}
|
|
||||||
if not isinstance(info, dict):
|
|
||||||
info = {}
|
|
||||||
pos_side = (info.get("posSide") or t.get("posSide") or "").lower()
|
|
||||||
if pos_side in ("long", "short") and pos_side != direction:
|
|
||||||
continue
|
|
||||||
out.append(t)
|
|
||||||
out.sort(key=lambda x: x.get("timestamp") or 0)
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def sum_binance_commission_income(entries: list[dict], trade_ids: set[str] | None) -> float | None:
|
|
||||||
"""Binance income 流水中 COMMISSION 合计(负值取绝对值为成本)."""
|
|
||||||
if not entries:
|
|
||||||
return None
|
|
||||||
total = 0.0
|
|
||||||
found = False
|
|
||||||
for e in entries:
|
|
||||||
it = (e.get("incomeType") or e.get("income_type") or "").strip()
|
|
||||||
if it != "COMMISSION":
|
|
||||||
continue
|
|
||||||
if trade_ids:
|
|
||||||
tid = str(e.get("tradeId") or e.get("trade_id") or "").strip()
|
|
||||||
if tid and tid not in trade_ids:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
total += float(e.get("income") or 0)
|
|
||||||
found = True
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
continue
|
|
||||||
if not found:
|
|
||||||
return None
|
|
||||||
return round(abs(total), 4)
|
|
||||||
|
|
||||||
|
|
||||||
def trade_ids_from_fills(fills: list[dict]) -> set[str]:
|
|
||||||
out: set[str] = set()
|
|
||||||
for t in fills or []:
|
|
||||||
info = t.get("info") or {}
|
|
||||||
if not isinstance(info, dict):
|
|
||||||
info = {}
|
|
||||||
for key in ("id", "tradeId", "trade_id"):
|
|
||||||
raw = t.get(key) if key in t else info.get(key)
|
|
||||||
if raw is not None and str(raw).strip():
|
|
||||||
out.add(str(raw).strip())
|
|
||||||
break
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def merge_commission_prefer_income(
|
|
||||||
fill_commission: float,
|
|
||||||
income_commission: float | None,
|
|
||||||
) -> float:
|
|
||||||
if income_commission is not None and income_commission > 0:
|
|
||||||
return round(income_commission, 4)
|
|
||||||
return round(max(fill_commission, 0.0), 4)
|
|
||||||
|
|
||||||
|
|
||||||
def update_trade_record_stats_columns(
|
|
||||||
conn: Any,
|
|
||||||
trade_id: int,
|
|
||||||
turnover_usdt: float | None,
|
|
||||||
commission_usdt: float | None,
|
|
||||||
) -> None:
|
|
||||||
if turnover_usdt is None and commission_usdt is None:
|
|
||||||
return
|
|
||||||
conn.execute(
|
|
||||||
"""
|
|
||||||
UPDATE trade_records
|
|
||||||
SET exchange_turnover_usdt = COALESCE(?, exchange_turnover_usdt),
|
|
||||||
exchange_commission_usdt = COALESCE(?, exchange_commission_usdt)
|
|
||||||
WHERE id = ?
|
|
||||||
""",
|
|
||||||
(turnover_usdt, commission_usdt, int(trade_id)),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def attach_exchange_stats_to_trade(
|
|
||||||
conn: Any,
|
|
||||||
trade_id: int,
|
|
||||||
*,
|
|
||||||
fetch_fills: Callable[[], list[dict]],
|
|
||||||
contract_size: float = 1.0,
|
|
||||||
income_commission: float | None = None,
|
|
||||||
) -> dict[str, float] | None:
|
|
||||||
"""拉 fill 并写库;仅在新单平仓路径调用."""
|
|
||||||
try:
|
|
||||||
fills = fetch_fills() or []
|
|
||||||
except Exception:
|
|
||||||
fills = []
|
|
||||||
stats = aggregate_bilateral_stats(fills, contract_size=contract_size)
|
|
||||||
if not stats and income_commission is None:
|
|
||||||
return None
|
|
||||||
turnover = stats.get("exchange_turnover_usdt") if stats else None
|
|
||||||
fill_comm = float(stats.get("exchange_commission_usdt") or 0) if stats else 0.0
|
|
||||||
commission = merge_commission_prefer_income(fill_comm, income_commission)
|
|
||||||
update_trade_record_stats_columns(
|
|
||||||
conn,
|
|
||||||
trade_id,
|
|
||||||
turnover,
|
|
||||||
commission if commission > 0 else None,
|
|
||||||
)
|
|
||||||
out = {}
|
|
||||||
if turnover is not None:
|
|
||||||
out["exchange_turnover_usdt"] = turnover
|
|
||||||
if commission > 0:
|
|
||||||
out["exchange_commission_usdt"] = commission
|
|
||||||
return out or None
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
"""永续估算盈亏:固定 taker 手续费(默认单边 0.05%,开+平双边).
|
|
||||||
|
|
||||||
浮盈亏仍读交易所;本模块只服务「盈利金额 / 止盈盈利 / 推送 / 记账 pnl_amount」等估算口径.
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import math
|
|
||||||
import os
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
|
|
||||||
def _finite(v) -> Optional[float]:
|
|
||||||
try:
|
|
||||||
f = float(v)
|
|
||||||
return f if math.isfinite(f) else None
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def taker_fee_rate() -> float:
|
|
||||||
"""单边 taker 费率,默认 0.0005(=0.05%)."""
|
|
||||||
raw = os.getenv("PERP_TAKER_FEE_RATE", "0.0005")
|
|
||||||
rate = _finite(raw)
|
|
||||||
if rate is None or rate < 0:
|
|
||||||
return 0.0005
|
|
||||||
return rate
|
|
||||||
|
|
||||||
|
|
||||||
def notional_usdt(price, qty, contract_size: float = 1.0) -> Optional[float]:
|
|
||||||
"""名义价值 U = 价格 × 张数 × 合约面值."""
|
|
||||||
p = _finite(price)
|
|
||||||
q = _finite(qty)
|
|
||||||
cs = _finite(contract_size)
|
|
||||||
if p is None or q is None or p <= 0 or q <= 0:
|
|
||||||
return None
|
|
||||||
if cs is None or cs <= 0:
|
|
||||||
cs = 1.0
|
|
||||||
return abs(q) * p * cs
|
|
||||||
|
|
||||||
|
|
||||||
def estimate_roundtrip_fee_usdt(
|
|
||||||
entry_price,
|
|
||||||
exit_price,
|
|
||||||
qty=None,
|
|
||||||
contract_size: float = 1.0,
|
|
||||||
*,
|
|
||||||
open_notional: float | None = None,
|
|
||||||
rate: float | None = None,
|
|
||||||
) -> float:
|
|
||||||
"""开+平双边手续费(各单边 rate).
|
|
||||||
|
|
||||||
优先用 价×张×面值;若无张数则用 open_notional 估开仓名义,
|
|
||||||
平仓名义按 exit/entry 缩放.
|
|
||||||
"""
|
|
||||||
fee_rate = taker_fee_rate() if rate is None else float(rate)
|
|
||||||
if fee_rate <= 0:
|
|
||||||
return 0.0
|
|
||||||
entry = _finite(entry_price)
|
|
||||||
exit_p = _finite(exit_price)
|
|
||||||
open_n = notional_usdt(entry, qty, contract_size) if qty is not None else None
|
|
||||||
if open_n is None:
|
|
||||||
open_n = _finite(open_notional)
|
|
||||||
if open_n is None or open_n <= 0:
|
|
||||||
return 0.0
|
|
||||||
if entry is not None and entry > 0 and exit_p is not None and exit_p > 0:
|
|
||||||
close_n = open_n * (exit_p / entry)
|
|
||||||
else:
|
|
||||||
close_n = open_n
|
|
||||||
return round(open_n * fee_rate + close_n * fee_rate, 8)
|
|
||||||
|
|
||||||
|
|
||||||
def net_pnl_after_fee(
|
|
||||||
gross_pnl,
|
|
||||||
entry_price,
|
|
||||||
exit_price,
|
|
||||||
qty=None,
|
|
||||||
contract_size: float = 1.0,
|
|
||||||
*,
|
|
||||||
open_notional: float | None = None,
|
|
||||||
rate: float | None = None,
|
|
||||||
) -> Optional[float]:
|
|
||||||
"""毛利扣双边手续费后的净盈亏;gross 无效则返回 None."""
|
|
||||||
gross = _finite(gross_pnl)
|
|
||||||
if gross is None:
|
|
||||||
return None
|
|
||||||
fee = estimate_roundtrip_fee_usdt(
|
|
||||||
entry_price,
|
|
||||||
exit_price,
|
|
||||||
qty,
|
|
||||||
contract_size,
|
|
||||||
open_notional=open_notional,
|
|
||||||
rate=rate,
|
|
||||||
)
|
|
||||||
return round(gross - fee, 4)
|
|
||||||
@@ -158,12 +158,5 @@ def order_monitor_excluded_from_position_limit(conn, row) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def count_position_limit_active_monitors(conn) -> int:
|
def count_position_limit_active_monitors(conn) -> int:
|
||||||
try:
|
del conn
|
||||||
rows = conn.execute("SELECT * FROM order_monitors WHERE status='active'").fetchall()
|
|
||||||
except Exception:
|
|
||||||
return 0
|
return 0
|
||||||
n = 0
|
|
||||||
for row in rows:
|
|
||||||
if not order_monitor_excluded_from_position_limit(conn, row):
|
|
||||||
n += 1
|
|
||||||
return n
|
|
||||||
|
|||||||
@@ -2,11 +2,23 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
_WIN_EPS = 1e-9
|
_WIN_EPS = 1e-9
|
||||||
|
|
||||||
|
|
||||||
|
def snapshot_stop_loss(initial_stop_loss: Any, stop_loss: Any) -> Optional[float]:
|
||||||
|
"""展示盈亏比时优先用开仓止损快照."""
|
||||||
|
for v in (initial_stop_loss, stop_loss):
|
||||||
|
try:
|
||||||
|
f = float(v)
|
||||||
|
if f > 0:
|
||||||
|
return f
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def classify_exit_by_levels(
|
def classify_exit_by_levels(
|
||||||
direction,
|
direction,
|
||||||
trigger_price,
|
trigger_price,
|
||||||
|
|||||||
@@ -1,115 +0,0 @@
|
|||||||
"""按交易日聚合实例 trade_records 盈亏,供统计分析页日历 API 使用."""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import json
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from typing import Any, Callable
|
|
||||||
|
|
||||||
|
|
||||||
def build_trade_stats_calendar(
|
|
||||||
pnls: list[tuple],
|
|
||||||
year: int,
|
|
||||||
month: int,
|
|
||||||
segment_key: str,
|
|
||||||
row_matches_fn: Callable[[Any, str], bool],
|
|
||||||
*,
|
|
||||||
reset_hour: int = 8,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""pnls: _load_completed_trade_pnls 返回值 (pnl, close_dt, trading_day, row)."""
|
|
||||||
y = int(year)
|
|
||||||
m = int(month)
|
|
||||||
if m < 1 or m > 12:
|
|
||||||
raise ValueError("month 无效")
|
|
||||||
first = f"{y:04d}-{m:02d}-01"
|
|
||||||
if m == 12:
|
|
||||||
next_first = datetime(y + 1, 1, 1)
|
|
||||||
else:
|
|
||||||
next_first = datetime(y, m + 1, 1)
|
|
||||||
last = (next_first - timedelta(days=1)).strftime("%Y-%m-%d")
|
|
||||||
seg = (segment_key or "all").strip() or "all"
|
|
||||||
days: dict[str, dict[str, Any]] = {}
|
|
||||||
for pnl, _close_dt, td, row in pnls:
|
|
||||||
if not td or td < first or td > last:
|
|
||||||
continue
|
|
||||||
if not row_matches_fn(row, seg):
|
|
||||||
continue
|
|
||||||
bucket = days.setdefault(
|
|
||||||
td,
|
|
||||||
{
|
|
||||||
"trading_day": td,
|
|
||||||
"open_count": 0,
|
|
||||||
"pnl_total": 0.0,
|
|
||||||
"turnover_total": 0.0,
|
|
||||||
"commission_total": 0.0,
|
|
||||||
"has_sick": False,
|
|
||||||
"sick_count": 0,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
bucket["open_count"] += 1
|
|
||||||
bucket["pnl_total"] += float(pnl or 0)
|
|
||||||
try:
|
|
||||||
bucket["turnover_total"] += float(row["exchange_turnover_usdt"] or 0)
|
|
||||||
except (TypeError, ValueError, KeyError):
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
bucket["commission_total"] += float(row["exchange_commission_usdt"] or 0)
|
|
||||||
except (TypeError, ValueError, KeyError):
|
|
||||||
pass
|
|
||||||
for d in days.values():
|
|
||||||
d["pnl_total"] = round(float(d["pnl_total"]), 4)
|
|
||||||
d["turnover_total"] = round(float(d["turnover_total"]), 4)
|
|
||||||
d["commission_total"] = round(float(d["commission_total"]), 4)
|
|
||||||
month_pnl = sum(float(d["pnl_total"]) for d in days.values())
|
|
||||||
month_count = sum(int(d["open_count"]) for d in days.values())
|
|
||||||
return {
|
|
||||||
"year": y,
|
|
||||||
"month": m,
|
|
||||||
"date_from": first,
|
|
||||||
"date_to": last,
|
|
||||||
"segment": seg,
|
|
||||||
"reset_hour": int(reset_hour),
|
|
||||||
"days": days,
|
|
||||||
"month_pnl_total": round(month_pnl, 4),
|
|
||||||
"month_open_count": month_count,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def build_initial_stats_calendar(
|
|
||||||
pnls: list[tuple],
|
|
||||||
now_dt: datetime,
|
|
||||||
row_matches_fn: Callable[[Any, str], bool],
|
|
||||||
*,
|
|
||||||
reset_hour: int = 8,
|
|
||||||
segment_key: str = "all",
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""统计页首屏内嵌日历(当前自然月,默认品类)."""
|
|
||||||
return build_trade_stats_calendar(
|
|
||||||
pnls,
|
|
||||||
now_dt.year,
|
|
||||||
now_dt.month,
|
|
||||||
segment_key,
|
|
||||||
row_matches_fn,
|
|
||||||
reset_hour=reset_hour,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def build_stats_calendar_bootstrap(
|
|
||||||
pnls: list[tuple],
|
|
||||||
now_dt: datetime,
|
|
||||||
row_matches_fn: Callable[[Any, str], bool],
|
|
||||||
*,
|
|
||||||
reset_hour: int = 8,
|
|
||||||
segment_key: str = "all",
|
|
||||||
) -> tuple[dict[str, Any] | None, str | None]:
|
|
||||||
"""返回 (payload, json_str);失败时 (None, None),供模板安全内嵌."""
|
|
||||||
try:
|
|
||||||
payload = build_initial_stats_calendar(
|
|
||||||
pnls,
|
|
||||||
now_dt,
|
|
||||||
row_matches_fn,
|
|
||||||
reset_hour=reset_hour,
|
|
||||||
segment_key=segment_key,
|
|
||||||
)
|
|
||||||
return payload, json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
|
|
||||||
except Exception:
|
|
||||||
return None, None
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>实盘下单放大 | 100根K线</title>
|
|
||||||
<style>
|
|
||||||
*{margin:0;padding:0;box-sizing:border-box}
|
|
||||||
body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;background:#0b0d14;color:#eaeaea;padding:14px}
|
|
||||||
.container{width:min(98vw,1900px);margin:0 auto}
|
|
||||||
.card{background:#121726;border-radius:10px;padding:12px;border:1px solid #2a3150;margin-bottom:12px}
|
|
||||||
.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
|
||||||
.btn{padding:7px 10px;border-radius:8px;text-decoration:none;border:1px solid #304164;background:#151a2a;color:#8fc8ff;cursor:pointer}
|
|
||||||
.btn:hover{background:#1f2740}
|
|
||||||
select,button{padding:8px 10px;border-radius:8px;border:1px solid #2e2e45;background:#1a1a29;color:#fff}
|
|
||||||
.meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;margin-top:10px}
|
|
||||||
.meta-item{background:#141b2f;border:1px solid #27324e;border-radius:8px;padding:8px}
|
|
||||||
.meta-item .k{font-size:.76rem;color:#9fb0d8}
|
|
||||||
.meta-item .v{font-size:1rem;margin-top:4px;word-break:break-all}
|
|
||||||
.status{font-size:.84rem;color:#95a2c2}
|
|
||||||
.status.err{color:#ff8080}
|
|
||||||
#chart-wrap{height:560px;background:#0f1320;border:1px solid #2a3150;border-radius:10px;padding:8px}
|
|
||||||
#chart{width:100%;height:100%}
|
|
||||||
.empty{padding:18px;color:#95a2c2}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<div class="card">
|
|
||||||
<div class="row" style="justify-content:space-between">
|
|
||||||
<div class="row">
|
|
||||||
<a class="btn" href="/">返回首页</a>
|
|
||||||
<strong style="color:#dbe4ff">实盘下单放大(100根K线)</strong>
|
|
||||||
</div>
|
|
||||||
<div class="status">最近刷新:<span id="updated-at">--</span></div>
|
|
||||||
</div>
|
|
||||||
{% if orders %}
|
|
||||||
<div class="row" style="margin-top:10px">
|
|
||||||
<label>订单</label>
|
|
||||||
<select id="order-id">
|
|
||||||
{% for o in orders %}
|
|
||||||
<option value="{{ o.id }}" {% if selected_order and o.id == selected_order.id %}selected{% endif %}>
|
|
||||||
#{{ o.id }} {{ o.symbol }} {{ '做多' if o.direction == 'long' else '做空' }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
<label>周期</label>
|
|
||||||
<select id="timeframe">
|
|
||||||
{% for tf in ['1m','3m','5m','15m','30m','1h','4h','1d'] %}
|
|
||||||
<option value="{{ tf }}" {% if tf == default_timeframe %}selected{% endif %}>{{ tf }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
<button id="manual-refresh" type="button">刷新</button>
|
|
||||||
<span id="load-status" class="status"></span>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="empty">当前没有激活订单,无法展示放大K线.</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if orders %}
|
|
||||||
<div class="card">
|
|
||||||
<div class="meta">
|
|
||||||
<div class="meta-item"><div class="k">交易对</div><div class="v" id="m-symbol">-</div></div>
|
|
||||||
<div class="meta-item"><div class="k">方向</div><div class="v" id="m-direction">-</div></div>
|
|
||||||
<div class="meta-item"><div class="k">成交价</div><div class="v" id="m-entry">-</div></div>
|
|
||||||
<div class="meta-item"><div class="k">止损</div><div class="v" id="m-sl">-</div></div>
|
|
||||||
<div class="meta-item"><div class="k">止盈</div><div class="v" id="m-tp">-</div></div>
|
|
||||||
<div class="meta-item"><div class="k">盈亏比</div><div class="v" id="m-rr">-</div></div>
|
|
||||||
<div class="meta-item"><div class="k">现价</div><div class="v" id="m-price">-</div></div>
|
|
||||||
<div class="meta-item"><div class="k">浮盈亏</div><div class="v" id="m-pnl">-</div></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div id="chart-wrap"><div id="chart"></div></div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if orders %}
|
|
||||||
<script src="https://unpkg.com/lightweight-charts/dist/lightweight-charts.standalone.production.js"></script>
|
|
||||||
<script>
|
|
||||||
const refreshMs = Math.max({{ price_refresh_seconds * 1000 }}, 5000);
|
|
||||||
const orderSelect = document.getElementById("order-id");
|
|
||||||
const tfSelect = document.getElementById("timeframe");
|
|
||||||
const statusEl = document.getElementById("load-status");
|
|
||||||
const updatedAtEl = document.getElementById("updated-at");
|
|
||||||
const chartHost = document.getElementById("chart");
|
|
||||||
const fmt = (v, d=6) => (v === null || typeof v === "undefined" || Number.isNaN(Number(v))) ? "-" : Number(v).toFixed(d);
|
|
||||||
|
|
||||||
let chart = null;
|
|
||||||
let candleSeries = null;
|
|
||||||
let priceLines = [];
|
|
||||||
|
|
||||||
function ensureChart(){
|
|
||||||
if(chart){ return true; }
|
|
||||||
if(!window.LightweightCharts){
|
|
||||||
statusEl.className = "status err";
|
|
||||||
statusEl.innerText = "图表库加载失败";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
chart = LightweightCharts.createChart(chartHost, {
|
|
||||||
layout: { background: { color: "#0f1320" }, textColor: "#d6deff" },
|
|
||||||
grid: { vertLines: { color: "#1e263d" }, horzLines: { color: "#1e263d" } },
|
|
||||||
rightPriceScale: { borderColor: "#2a3150" },
|
|
||||||
timeScale: { borderColor: "#2a3150", timeVisible: true, secondsVisible: false },
|
|
||||||
crosshair: { mode: 0 }
|
|
||||||
});
|
|
||||||
candleSeries = chart.addCandlestickSeries({
|
|
||||||
upColor: "#4cd97f",
|
|
||||||
downColor: "#ff6666",
|
|
||||||
borderVisible: false,
|
|
||||||
wickUpColor: "#4cd97f",
|
|
||||||
wickDownColor: "#ff6666"
|
|
||||||
});
|
|
||||||
window.addEventListener("resize", () => {
|
|
||||||
chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight });
|
|
||||||
});
|
|
||||||
chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight });
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetPriceLines(){
|
|
||||||
if(!candleSeries){ return; }
|
|
||||||
priceLines.forEach(line => {
|
|
||||||
try { candleSeries.removePriceLine(line); } catch (_) {}
|
|
||||||
});
|
|
||||||
priceLines = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function addLine(price, title, color){
|
|
||||||
if(!candleSeries || typeof price === "undefined" || price === null){ return; }
|
|
||||||
const p = Number(price);
|
|
||||||
if(Number.isNaN(p) || p <= 0){ return; }
|
|
||||||
priceLines.push(candleSeries.createPriceLine({
|
|
||||||
price: p, color, lineWidth: 1, lineStyle: 0, axisLabelVisible: true, title
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
function paintOrder(order){
|
|
||||||
document.getElementById("m-symbol").innerText = order.symbol || "-";
|
|
||||||
document.getElementById("m-direction").innerText = (order.direction === "short") ? "做空" : "做多";
|
|
||||||
document.getElementById("m-entry").innerText = fmt(order.trigger_price, 8);
|
|
||||||
document.getElementById("m-sl").innerText = fmt(order.stop_loss, 8);
|
|
||||||
document.getElementById("m-tp").innerText = fmt(order.take_profit, 8);
|
|
||||||
const rr = order.rr_ratio;
|
|
||||||
document.getElementById("m-rr").innerText = (rr === null || typeof rr === "undefined") ? "-:1" : `${Number(rr).toFixed(2)}:1`;
|
|
||||||
document.getElementById("m-price").innerText = fmt(order.current_price, 8);
|
|
||||||
const pnlEl = document.getElementById("m-pnl");
|
|
||||||
pnlEl.innerText = `${fmt(order.float_pnl, 4)}U (${fmt(order.float_pct, 2)}%)`;
|
|
||||||
pnlEl.style.color = Number(order.float_pnl || 0) > 0 ? "#4cd97f" : (Number(order.float_pnl || 0) < 0 ? "#ff6666" : "#d6deff");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadOrderKline(){
|
|
||||||
if(!ensureChart()){ return; }
|
|
||||||
const orderId = orderSelect.value;
|
|
||||||
const timeframe = tfSelect.value;
|
|
||||||
if(!orderId){ return; }
|
|
||||||
statusEl.className = "status";
|
|
||||||
statusEl.innerText = "加载中...";
|
|
||||||
try{
|
|
||||||
const resp = await fetch(`/api/order_kline?order_id=${encodeURIComponent(orderId)}&timeframe=${encodeURIComponent(timeframe)}`);
|
|
||||||
const data = await resp.json();
|
|
||||||
if(!resp.ok || !data.ok){ throw new Error(data.msg || "请求失败"); }
|
|
||||||
const candles = Array.isArray(data.candles) ? data.candles : [];
|
|
||||||
if(!candles.length){
|
|
||||||
statusEl.className = "status err";
|
|
||||||
statusEl.innerText = "暂无K线数据";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
candleSeries.setData(candles);
|
|
||||||
resetPriceLines();
|
|
||||||
addLine(data.order.trigger_price, "成交价", "#42a5f5");
|
|
||||||
addLine(data.order.stop_loss, "止损", "#ff6666");
|
|
||||||
addLine(data.order.take_profit, "止盈", "#4cd97f");
|
|
||||||
chart.timeScale().fitContent();
|
|
||||||
paintOrder(data.order || {});
|
|
||||||
updatedAtEl.innerText = data.updated_at || "--";
|
|
||||||
statusEl.className = "status";
|
|
||||||
statusEl.innerText = `已加载 ${candles.length} 根K线`;
|
|
||||||
}catch(err){
|
|
||||||
statusEl.className = "status err";
|
|
||||||
statusEl.innerText = err && err.message ? err.message : "加载失败";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById("manual-refresh").addEventListener("click", loadOrderKline);
|
|
||||||
orderSelect.addEventListener("change", loadOrderKline);
|
|
||||||
tfSelect.addEventListener("change", loadOrderKline);
|
|
||||||
loadOrderKline();
|
|
||||||
setInterval(loadOrderKline, refreshMs);
|
|
||||||
</script>
|
|
||||||
{% endif %}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Reference in New Issue
Block a user