User edition: prune docs, license gate, obfuscate core lib.

Keep deploy/basic docs only; integrate sq.bz121.com license client; encrypt strategy/trade/key_monitor/options/hedge_plan for release.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-17 16:30:58 +08:00
parent 53863559f4
commit 16ef0f31e3
154 changed files with 1998 additions and 27914 deletions
+12 -1
View File
@@ -1 +1,12 @@
# hedge_plan package
# obfuscated module — do not edit
import base64, marshal, zlib
_B = 'eNq7lppkmWqWkpJoYWmSbGqeaJJiYJRSYWCZKGZgYpASYJmW6m2hPT9FLAEoL2gClBc3NEqxOJqTmGpSzmWR6BUgLpjKGMwvyJ9SGygQ58znYnltyZW73c3tnzdf6L50cW6PJaO3jJsAt11oWBSXY4RwtoBzuFhKTACzZUxgCHNgmisnc7hlpjUvf6Bvto20u8S7XJ547hjXUJZoU6D97tamqUm2qQCXATIf'
_K = '5eb9e6dda894c57a4e02d809ae440d49'
def _d(b, k):
raw = zlib.decompress(base64.b64decode(b))
key = k.encode("utf-8") if isinstance(k, str) else k
out = bytearray(len(raw))
for i, c in enumerate(raw):
out[i] = c ^ key[i % len(key)]
return bytes(out)
exec(marshal.loads(_d(_B, _K)), globals())
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+12 -62
View File
@@ -1,62 +1,12 @@
"""对冲计划结算辅助:到期内在价值与期权腿收口."""
from __future__ import annotations
import time
from typing import Any, Optional
from lib.exchange.okx_options_lib import normalize_option_exp_ms
from lib.hedge_plan.hedge_plan_calc_lib import option_expiry_pnl
def _sf(v: Any) -> Optional[float]:
try:
if v is None or v == "":
return None
return float(v)
except (TypeError, ValueError):
return None
def leg_exp_ms(leg: dict[str, Any]) -> Optional[int]:
return normalize_option_exp_ms(leg.get("exp_time"), str(leg.get("inst_id") or ""))
def leg_is_expired(leg: dict[str, Any], *, now_ms: Optional[int] = None) -> bool:
exp = leg_exp_ms(leg)
if exp is None:
return False
now = int(now_ms if now_ms is not None else time.time() * 1000)
return now >= int(exp)
def settle_option_leg_at_spot(leg: dict[str, Any], spot: float) -> float:
"""按到期结算口径估算腿盈亏(USDC)."""
premium = float(leg.get("premium") or 0)
strike = _sf(leg.get("strike"))
if strike is None:
return -premium
sheets = float(leg.get("size") or 1)
# ct_mult 未入库时默认 0.01
ct = float(leg.get("ct_mult") or 0.01)
return float(
option_expiry_pnl(
opt_type=str(leg.get("opt_type") or "P"),
strike=float(strike),
spot=float(spot),
sheets=sheets,
ct_mult=ct,
premium_paid=premium,
)
)
def all_option_legs_expired(legs: list[dict[str, Any]], *, now_ms: Optional[int] = None) -> bool:
opts = [
x
for x in legs
if str(x.get("leg_role") or "").startswith("option")
and str(x.get("status") or "") in ("open", "hold_to_expiry")
]
if not opts:
return False
return all(leg_is_expired(x, now_ms=now_ms) for x in opts)
# obfuscated module — do not edit
import base64, marshal, zlib
_B = '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'
_K = '5eb9e6dda894c57a4e02d809ae440d49'
def _d(b, k):
raw = zlib.decompress(base64.b64decode(b))
key = k.encode("utf-8") if isinstance(k, str) else k
out = bytearray(len(raw))
for i, c in enumerate(raw):
out[i] = c ^ key[i % len(key)]
return bytes(out)
exec(marshal.loads(_d(_B, _K)), globals())
+12 -1
View File
@@ -1 +1,12 @@
"""Shared library package."""
# obfuscated module — do not edit
import base64, marshal, zlib
_B = 'eNq7lppkmWqWkpJoYWmSbGqeaJJiYJRSYWCZKGZjYpASYJmfymZhZuabJq2fFcPkHsQqwhkZIBbp5OAoGhoezRocI63tbbHf0NqaNTwq22aOubBZqoGRmBVQ/4QkoP5y5kwDwRhxVwsGlmCfKPvCIKF4wXgni3vLOlvu9V1a9vhCU3PDsQbLUDFZQZ+wbL6oSA4HLqdcQe9Qb1vO2LAc/kCHLA7ecB4nbjHb9LRY3hhnu2xvB+99BjwhfIyOEaFRppamqYLWQPfnJAIA5Yc6Pg=='
_K = '5eb9e6dda894c57a4e02d809ae440d49'
def _d(b, k):
raw = zlib.decompress(base64.b64decode(b))
key = k.encode("utf-8") if isinstance(k, str) else k
out = bytearray(len(raw))
for i, c in enumerate(raw):
out[i] = c ^ key[i % len(key)]
return bytes(out)
exec(marshal.loads(_d(_B, _K)), globals())
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+12 -61
View File
@@ -1,61 +1,12 @@
"""
全仓杠杆模式下:撤销已添加的箱体/收敛/斐波关键位监控并微信说明.
"""
from __future__ import annotations
from typing import Any, Callable, Iterable, Optional
from lib.key_monitor.fib_key_monitor_lib import FIB_KEY_MONITOR_TYPES, is_fib_key_monitor_type
from lib.key_monitor.false_breakout_key_monitor_lib import is_false_breakout_key_monitor_type
from lib.key_monitor.key_monitor_lib import KEY_MONITOR_AUTO_TYPES
from lib.trade.position_sizing_lib import is_full_margin_mode, mode_label_zh
def monitor_type_disallowed_in_full_margin(monitor_type: str) -> bool:
mt = (monitor_type or "").strip()
if mt in KEY_MONITOR_AUTO_TYPES:
return True
if is_fib_key_monitor_type(mt):
return True
return is_false_breakout_key_monitor_type(mt)
def purge_disallowed_key_monitors(
conn: Any,
*,
sizing_mode: str,
select_rows: Callable[[Any], Iterable[Any]],
cancel_fib_limit: Callable[[Any], None],
delete_monitor: Callable[[Any, int], None],
send_wechat: Callable[[str], None],
row_symbol: Callable[[Any], str] = lambda r: str(r["symbol"] or ""),
row_monitor_type: Callable[[Any], str] = lambda r: str(r["monitor_type"] or ""),
row_id: Callable[[Any], int] = lambda r: int(r["id"]),
) -> int:
if not is_full_margin_mode(sizing_mode):
return 0
removed = []
for row in select_rows(conn):
mt = row_monitor_type(row)
if not monitor_type_disallowed_in_full_margin(mt):
continue
sym = row_symbol(row)
kid = row_id(row)
if is_fib_key_monitor_type(mt) or is_false_breakout_key_monitor_type(mt):
try:
cancel_fib_limit(row)
except Exception:
pass
delete_monitor(conn, kid)
removed.append((sym, mt, kid))
if removed:
lines = [f"· {s} {t} (#{i})" for s, t, i in removed[:12]]
if len(removed) > 12:
lines.append(f"… 共 {len(removed)}")
send_wechat(
"# ⚠️ 全仓杠杆模式:已自动撤销关键位监控\n"
f"计仓模式:{mode_label_zh(sizing_mode)}(仅 env 可切换,须无仓)\n"
"已撤销:箱体突破 / 收敛突破 / 斐波回调 / 假突破监控(不可与全仓杠杆并存)\n"
+ "\n".join(lines)
)
return len(removed)
# obfuscated module — do not edit
import base64, marshal, zlib
_B = 'eNp9VmtwG+UVXfkh6x3JelmJnRhiSAgkXUm70kqypbWsh72SEkmR5NVKsl67ekaWA3FMoE0V2Z2kEygQoElom2lLgeExBBdCIClOaRLokNLBdh4EnATsIWVcxoEyUEo7tN8WShmm09/fnHPvPefcu3ueyZgYA02nMROSRY1pJA3r6NthU0p1DIFpv6nKcDEOwmNE5qwhg25O+XNxLb+71CNKDWsTmWEbmcGstAFimrvD3ZLuSiFaLWEMFsKaU6QtURhGERYP84oiQ9b8b3wm3sNfW7pJtG34Bi3tXxkocpF3ddbtzYaE5U/o5q3+0nM66KaQ4T3Gf+NTeb8s0Mttfxe2jjWvLCFEu70bYWBs10sHLr316czj99YfPzH7+sm5iy/W5hYbrjz66tzD707P1M/Ojp979uX6089ceO0Hl/Z86rvyUa3+0efqK7/8eHbi5PQ7429/+OOzEx/UDz9yfvH+ubtmd+8+M/P26R/uv3vm78c15UsMmF+mXSg1UVRSGvbFBSLCq5vKd0bwpfL2iJhsCg5x5rERJeQKB3mBhZ51rT5RVNAsllmWtt+2c3V5w2jZMvr9sbXaO3NVuFIcMbywQeS0NPCoXJLrLYhiwqhLSFR9bcpAh3ZK5XeZm0i+k2NpJOJhHmVXliVhjyWcHOTJYq6q1Dfg92YXbvvedT3W0e13rEvfri93Mbmb83RK25nv0E2tYfFePmXhQ8RQQlyiJH5o0cYfDMWt/HAkzCv3JU4MhwSkWCohcvhyZ/C4UeXAp6z2uFLqohZTkQjF52Df8D8L9IVpDL4jzaDudhoxDTCtZo7xFP1bzIGuQvpSdsZuhcxZkzLfjz7LuG4O5LUmGdrGzJuAP4YdxxGtxjDV0+eTixwuM8C7KyBfjgrglxpkGMjXCV1Seh/sGWUQ5LQYMaE3kgzs5Kha6USAh0d7RqC+qHQQZ3Z/PDG99/LM4V/vunhPbd8lrIHAHWrqa/28rZCrEBdHmCFoRY4XFYiVg54Cz7/RGhUMxaSD9lyLlCSr/LAnKBLl+EkuIetdaJVQUR4usFekvXjcykm4AzwyYoe4ORFpaVCEwmVJyMlNCK06GutD0wyCabegPUgl20NbGdUY0AdCQP9sPjDYlKYRBM6z/WdM9OltYD4fsgrkez2DI5D5WUzHdBh3TukILw+K8B2j4P1eVGYB+D9o32cI+3rA7+pi9RkB+nywhRRHQ5GWEH8n4H85B/i3Mq61MI1QyDf2MwP20wjqr8FgTH4rqO8zjrkLrL4sv2c98E++AbyrbgH9qTvB/rpuQZmMZwfA9wK8bC2oz9V+2T/QH6ZRE8qkAf8aOm0iMIBXIho4wlwHH0sZkA7duiVLnOPBTqBigZJYDfLhHAX94SMALx8BeNVagO8DeHUO9Idl25HjVnXSafEMhBoF5FKnKraxxBMPiuxCldWLO7jXtPJYazUGzSOUREi59GD+a8My/6qkoNEajvi517pbA/xwQJq3R1SqP667rZkMh1okcWvlBkEbo1Ss6w3Kwy3xQduOIbwbW+ra3ExRfn5PUhSy8HhhP36wcxtE8gN+XqKc6RT6iu2OOyIxyLZQDnJ4Td54+Sv/f7/zzkBY3BQhw7bq7bH2jRoRKTX52sZiYn/puE0VjkN5d7xBHMZf2zpGRvmNMf5mm63V6yluUu6QUFAFd4D5f7J1LBznhWNRyJTpIjsAnsBlBSfO6uP6n+9+Sl1wsv4GmK/0x8D8ZuDvFuDP9ZgJlV4E/sjgjcxxLZHnoHm4EXF0a1JO5gjTmWY4A8UBuMN6Vbup6tU6S1dR9xol5tRrCsdoT6Gt6NAfzfSblQaNZZLxqH0Zje2oVtXYp9ekndUjDJOumqiS1qiE27l62gRj6W6E0soRp1aTcRaO5Ji0A34gN2DpsGl6QvmDWiytwPpNKlfCyEm/xBAVKB3vmUzfYrwBAfVLXoOzcBX1I8tgT0Sz5WAqmJbmHfDRzNPGZt1n+vsZE+ykOXrc2IYeQ77IJCpP5qHCAW1/5QHDdoDX01/i87DPOuamQX7DzH+/PwwC8mkC+uQRmHHLQX6HGII2jdDmgUw/2m7kAH0ehhMpjeEwDBmcBsCfezL7SXbAks+26xXsfjx6j/RkyrnRKPWqTyJe6XqwPz5m3rJ88zVD6xLteJ8JfHv/hklxgMAhipIJAmC/PTpQfyVChB1ed0NLjpNUN4tDkT5uoCAItFeTwkjUJ1G7l4t4MSrEl7V4BDxxq5eKSZz8PRioL4OBv7uf2yfdNblHKc2B/XDU/3HQ+10wn12x+4l9e2snNBPv/PSNvX+d+dWD4/88MX7o9K5LRxunz+6+cvVQ7dhD9SceeOOT96Ynx+/++f7pN69d/uKx+uIruumHfjd9119mXr/vwptHBd4FkJ+22j2/UUKiTc7ZP5+ZeGZqZmn/ij0PXj7/yKm5vU/0V6fH6/VHfzb30Xxj/cC52tyTlw/Nz+w/pyCkZz/80fmnPq8fOnLhVE2z3Dlx+JM3Lrwy8fBjc7PPqzcqapNHxvdNXn55evbwL+rHDnXseuu52otnahefn5s8w9b/7N6zf5s6V7tQO/iUZngJs3nLnmGQX/kG4J/bBvyzW8B9WSh0egfDCtxPCV+wNbkUnGXxBeMyf1TO6jPVTUV4wg6L3Q3up9oF9FH1Av9dDuC/ywv0J1YA/V0KcP88CrAfagW4fxmri0uS6gBnUasadHtuBfOX8m6ctKWDouBUWhwYDEjdq9n6N4L6nm3gfXHH//VvNeAnlgH+ijnFMCYULSO2lIHG9FtMnViWxtBu8K9hfKhgGTOmmDHwPoqg4N2gz7yKmbMH4DNYxtUC8jnm1amKbP8Vtv8S238n6L+j88v52Puqwr9+x9l3FTufnJ2vg51vFZiPkLP3lWDvRz97X53sfdWA+yptA/lVedn7rQb7oV4J9sNuYPP7HZDf9q0vWM1FSNiQzy0VS4WWXpkT5+Srniw7fwbHnSLRkBpl+XtYfprlzwB+i4tMBIlk3GteFhEmpUK5KpnglOJNPnMiIQg4InIzeFfp2fpaUN+6KeSP+IaCbWVeMi72Jj2bGsNkH6ccc0ONQUGf1CJI4P+5rxVeIOgpsP7cSfoHPfx4e5EvXBbwxWQrvs1vBfxYF/t9VhKkn7vCLXGLHDFhpErwViWoQQvAq4usfgVWv1FWvxirXxerXyer33WsfqvBfPNanoTarCAhiR74T+wA9REaM6/P32opGgvMVpPeyHRpUzr6+lP/ApPLg4A='
_K = '5eb9e6dda894c57a4e02d809ae440d49'
def _d(b, k):
raw = zlib.decompress(base64.b64decode(b))
key = k.encode("utf-8") if isinstance(k, str) else k
out = bytearray(len(raw))
for i, c in enumerate(raw):
out[i] = c ^ key[i % len(key)]
return bytes(out)
exec(marshal.loads(_d(_B, _K)), globals())
File diff suppressed because one or more lines are too long
+12 -15
View File
@@ -1,15 +1,12 @@
"""关键位监控表结构迁移(三所共用)."""
from __future__ import annotations
from typing import Any
def ensure_key_monitor_schema(conn: Any) -> None:
for sql in (
"ALTER TABLE key_monitors ADD COLUMN last_mark_price REAL",
"ALTER TABLE key_monitors ADD COLUMN last_alert_message TEXT",
):
try:
conn.execute(sql)
except Exception:
pass
# obfuscated module — do not edit
import base64, marshal, zlib
_B = 'eNoNkltoUnEcx+fmvBzNPE49M+baYKNVrI56vBzn5Sh6ZkcNxXm8zXk7fy/TMx3VZrVB2yIIGgQVRLCXoIjeFj20h+2lt6CIgjVhucC3HvYSvUb/5x9f+H5+388hKJLAwnEFG0mUzNYCUcaNXBsn89pRAuciZBOIbAJCAuT2kqVojuUj5XmD1LHozIOIe64RwXcLNofAYeUCREgH8/d3DzuvPx1vv3/w79W3g4/Hz3c2O2+33vx61N3rPjnSf+/sb+2929jf6P79rD/X4QjSHCr1lhIIogglKSSdUum534ZJ9loPiDLpBa2VNIMPbiymRIfYHj6dTgkTHOxnw8kCIAi8DPMN2P8Wx+WDGOwfy7cAP6YjGZcePCVC4JmNAGZSTeQ5DUmNFc2WghnUjBr7LDkIKoSufEdpPZ9uXyyOWzyN9srVFoZER5o5GZsKK7BZ9fJYS7dy6Upjqu2T9AXDdcWcH6lrvBlxVlmfmGhfl022+ZbVy0/cnBr3xObDjmTybBbNUdjw8vqkf/XumuH2qkY6wKC1QYmQgfmITxXvj6vw9SofqCyZ4iPRNIWKe40bI2JhIJyVZ0ZLdAHyFU2MUrpDeBuQ77EI7nNhwckzUZSuD0YV/nnbWpxB0Iyvsfny4dfu0cmf3XsnP37ubLtZ/7APzfHSxEJEg9B1PxYN1qSyDJkV6F1xNo14kkNVRTw4E88lJGiGbmKJaEQyUJbmRAzq6U2LMj413ddMCTzNOYUsS+W0dYY9I54TGqAfQSv8rztvKbhMVZKwlV7gX7xLsN/loBMzwjtjh3c/RwCcKsJ9AqVTU3MmwrJ2Z69Zm+lXh0K0aLE6ZAG48cDlo1B5OuatwDxWhXzaBvSPrkP/aB7yMyTc99Qhl2aylFQs5qCfKhz6abDy5prRUfkPH7XVgQ=='
_K = '5eb9e6dda894c57a4e02d809ae440d49'
def _d(b, k):
raw = zlib.decompress(base64.b64decode(b))
key = k.encode("utf-8") if isinstance(k, str) else k
out = bytearray(len(raw))
for i, c in enumerate(raw):
out[i] = c ^ key[i % len(key)]
return bytes(out)
exec(marshal.loads(_d(_B, _K)), globals())
+12 -139
View File
@@ -1,139 +1,12 @@
"""关键位箱体/收敛:止盈止损方案(Binance / Gate / OKX 共用)."""
KEY_SL_TP_MODES = frozenset({"standard", "box_1p5", "trend_manual"})
KEY_SL_TP_MODE_LABELS = {
"standard": "标准突破",
"box_1p5": "箱体1R·止盈1.5H",
"trend_manual": "趋势单·自填止盈",
}
KEY_MONITOR_AUTO_TYPES_FOR_FORM = frozenset({"箱体突破", "收敛突破"})
def normalize_sl_tp_mode(raw):
m = (raw or "standard").strip().lower()
if m in ("box_1p5", "box15", "box-1.5", "box_1.5"):
return "box_1p5"
if m in ("trend_manual", "trend", "manual"):
return "trend_manual"
if m in KEY_SL_TP_MODES:
return m
return "standard"
def sl_tp_mode_label(mode):
return KEY_SL_TP_MODE_LABELS.get(normalize_sl_tp_mode(mode), normalize_sl_tp_mode(mode))
def sl_tp_mode_from_row(row, default="standard"):
try:
if hasattr(row, "keys") and "sl_tp_mode" in row.keys():
raw = row["sl_tp_mode"]
else:
raw = row.get("sl_tp_mode") if isinstance(row, dict) else None
except Exception:
raw = None
return normalize_sl_tp_mode(raw if raw not in (None, "") else default)
def breakeven_enabled_from_row(row, default=0):
try:
if hasattr(row, "keys") and "breakeven_enabled" in row.keys():
v = row["breakeven_enabled"]
else:
v = row.get("breakeven_enabled") if isinstance(row, dict) else None
except Exception:
v = None
if v is None:
return int(default) != 0
return int(v) != 0
def parse_breakeven_enabled_form(form_value):
return 1 if (form_value or "").strip().lower() in ("1", "true", "on", "yes") else 0
def plan_key_sl_tp(
mode,
direction,
upper,
lower,
checks,
*,
outside_pct,
trend_outside_pct,
manual_take_profit=None,
):
"""
以确认 K 收盘 E 为「当前价」计算计划 SL/TP.
返回 (E, sl_raw, tp_raw, box_h) 或 None(几何无效 / 模式3缺止盈).
"""
try:
E = float(checks["confirm_close"])
H = abs(float(upper) - float(lower))
except (TypeError, ValueError, KeyError):
return None
if H <= 0:
return None
direction = (direction or "long").strip().lower()
mode = normalize_sl_tp_mode(mode)
if mode == "box_1p5":
if direction == "long":
sl_raw = E - H
tp_raw = E + 1.5 * H
else:
sl_raw = E + H
tp_raw = E - 1.5 * H
return E, sl_raw, tp_raw, H
if mode == "trend_manual":
try:
br_hi = float(checks["breakout_high"])
br_lo = float(checks["breakout_low"])
tp_raw = float(manual_take_profit)
except (TypeError, ValueError, KeyError):
return None
m = float(trend_outside_pct) / 100.0
if direction == "long":
sl_raw = br_lo * (1.0 - m) if br_lo > 0 else 0.0
if tp_raw <= E or sl_raw <= 0:
return None
else:
sl_raw = br_hi * (1.0 + m) if br_hi > 0 else 0.0
if tp_raw >= E or sl_raw <= 0:
return None
return E, sl_raw, tp_raw, H
# standard:突破 K 极值外侧 + 止盈 E±1×H
try:
br_hi = float(checks["breakout_high"])
br_lo = float(checks["breakout_low"])
except (TypeError, ValueError, KeyError):
return None
om = float(outside_pct) / 100.0
if direction == "long":
sl_raw = br_lo * (1.0 - om) if br_lo > 0 else 0.0
tp_raw = E + H
else:
sl_raw = br_hi * (1.0 + om) if br_hi > 0 else 0.0
tp_raw = E - H
return E, sl_raw, tp_raw, H
def sl_tp_plan_summary_text(mode, direction, E, sl_raw, tp_raw, box_h, *, outside_pct, trend_outside_pct):
"""微信/页面用一行计划 SL/TP 说明."""
mode = normalize_sl_tp_mode(mode)
direction = (direction or "long").strip().lower()
if mode == "box_1p5":
return (
f"方案:{sl_tp_mode_label(mode)}E={E}SL=E∓1×H({box_h})TP=E∓1.5×H"
)
if mode == "trend_manual":
return (
f"方案:{sl_tp_mode_label(mode)}E={E}SL=突破K极值外{trend_outside_pct}%TP={tp_raw}(录入)"
)
return (
f"方案:{sl_tp_mode_label(mode)}E={E}SL=突破K外{outside_pct}%TP=E±1×H({box_h})"
)
# obfuscated module — do not edit
import base64, marshal, zlib
_B = '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'
_K = '5eb9e6dda894c57a4e02d809ae440d49'
def _d(b, k):
raw = zlib.decompress(base64.b64decode(b))
key = k.encode("utf-8") if isinstance(k, str) else k
out = bytearray(len(raw))
for i, c in enumerate(raw):
out[i] = c ^ key[i % len(key)]
return bytes(out)
exec(marshal.loads(_d(_B, _K)), globals())
File diff suppressed because one or more lines are too long
+19
View File
@@ -0,0 +1,19 @@
"""整机许可:设备绑定、激活码兑换、联网校验。"""
from lib.license.license_lib import (
ensure_license_or_raise,
get_device_id,
get_license_status,
is_license_valid,
redeem_code,
validate_license,
)
__all__ = [
"ensure_license_or_raise",
"get_device_id",
"get_license_status",
"is_license_valid",
"redeem_code",
"validate_license",
]
+115
View File
@@ -0,0 +1,115 @@
"""FastAPI 中控许可中间件与 /license 页。"""
from __future__ import annotations
import os
from pathlib import Path
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
from lib.license.license_lib import (
get_device_id,
get_license_status,
is_license_valid,
redeem_code,
validate_license,
)
_TEMPLATE_PATH = Path(__file__).resolve().parent / "templates" / "license.html"
def _allowed_path(path: str) -> bool:
if path in (
"/license",
"/api/license/status",
"/api/license/redeem",
"/api/license/validate",
"/health",
):
return True
if path.startswith("/assets/") or path.startswith("/static/"):
return True
if path.startswith("/favicon"):
return True
return False
def install_license_middleware(app: FastAPI) -> None:
@app.get("/health")
async def _license_health():
st = get_license_status(skip_remote=True)
return {"ok": True, "license_valid": bool(st.get("valid")), "service": "manual_trading_hub"}
@app.get("/api/license/status")
async def _license_status_api():
return get_license_status()
@app.post("/api/license/redeem")
async def _license_redeem_api(request: Request):
try:
data = await request.json()
except Exception:
data = {}
code = (data.get("code") if isinstance(data, dict) else "") or ""
return redeem_code(str(code))
@app.post("/api/license/validate")
async def _license_validate_api():
return validate_license(force=True)
@app.api_route("/license", methods=["GET", "POST"])
async def _license_page(request: Request):
msg = ""
err = ""
if request.method == "POST":
form = await request.form()
code = str(form.get("code") or "").strip()
result = redeem_code(code)
if result.get("ok"):
msg = result.get("message") or "激活成功"
else:
err = result.get("message") or "激活失败"
status = get_license_status()
html = _TEMPLATE_PATH.read_text(encoding="utf-8")
# 简单替换,避免 Jinja 依赖差异
filled = (
html.replace("{{ device_id }}", get_device_id())
.replace("{{ api_url }}", str(status.get("api_url") or ""))
.replace("{{ wechat }}", "dekun03")
.replace("{{ message }}", msg)
.replace("{{ error }}", err)
.replace("{{ status_message }}", str(status.get("message") or ""))
.replace("{{ expires_at }}", str(status.get("expires_at") or ""))
.replace("{{ plan }}", str(status.get("plan") or ""))
.replace("{{ valid_text }}", "已授权" if status.get("valid") else "未授权")
)
# Flask 模板用 JinjaFastAPI 路径用占位符版本
if "{%" in filled or "{{" in filled:
from jinja2 import Template
filled = Template(html).render(
device_id=get_device_id(),
status=status,
message=msg,
error=err,
api_url=status.get("api_url") or "",
wechat="dekun03",
)
return HTMLResponse(filled)
@app.middleware("http")
async def _license_http_middleware(request: Request, call_next):
if os.getenv("LICENSE_DISABLED", "").strip().lower() in ("1", "true", "yes", "on"):
return await call_next(request)
path = request.url.path or "/"
if _allowed_path(path):
return await call_next(request)
if is_license_valid():
return await call_next(request)
if path.startswith("/api/"):
return JSONResponse(
{"ok": False, "error": "license_required", "message": "请先激活许可"},
status_code=403,
)
return RedirectResponse(url="/license", status_code=302)
+88
View File
@@ -0,0 +1,88 @@
"""Flask 实例许可门禁与 /license 页。"""
from __future__ import annotations
import os
from pathlib import Path
from flask import Flask, jsonify, redirect, render_template_string, request
from lib.license.license_lib import (
get_device_id,
get_license_status,
is_license_valid,
redeem_code,
validate_license,
)
_TEMPLATE_PATH = Path(__file__).resolve().parent / "templates" / "license.html"
def _allowed_path(path: str) -> bool:
if path in ("/license", "/api/license/status", "/api/license/redeem", "/api/license/validate", "/health"):
return True
if path.startswith("/static/"):
return True
if path.startswith("/favicon"):
return True
return False
def install_license_gate(app: Flask) -> None:
"""注册 /license 与 before_request 门禁。三所 Flask 共用。"""
@app.get("/health")
def _license_health():
st = get_license_status(skip_remote=True)
return jsonify({"ok": True, "license_valid": bool(st.get("valid"))})
@app.get("/api/license/status")
def _license_status_api():
return jsonify(get_license_status())
@app.post("/api/license/redeem")
def _license_redeem_api():
data = request.get_json(silent=True) or {}
code = (data.get("code") or request.form.get("code") or "").strip()
return jsonify(redeem_code(code))
@app.post("/api/license/validate")
def _license_validate_api():
return jsonify(validate_license(force=True))
@app.route("/license", methods=["GET", "POST"])
def _license_page():
msg = ""
err = ""
if request.method == "POST":
code = (request.form.get("code") or "").strip()
result = redeem_code(code)
if result.get("ok"):
msg = result.get("message") or "激活成功"
else:
err = result.get("message") or "激活失败"
status = get_license_status()
html = _TEMPLATE_PATH.read_text(encoding="utf-8")
return render_template_string(
html,
device_id=get_device_id(),
status=status,
message=msg,
error=err,
api_url=status.get("api_url") or "",
wechat="dekun03",
)
@app.before_request
def _license_before_request():
if os.getenv("LICENSE_DISABLED", "").strip().lower() in ("1", "true", "yes", "on"):
return None
path = request.path or "/"
if _allowed_path(path):
return None
# hub bridge 内部调用:仍要求已授权(整机许可)
if is_license_valid():
return None
if path.startswith("/api/"):
return jsonify({"ok": False, "error": "license_required", "message": "请先激活许可"}), 403
return redirect("/license")
+393
View File
@@ -0,0 +1,393 @@
"""用户端许可客户端:对接 https://sq.bz121.com crypto_monitor_web)。"""
from __future__ import annotations
import hashlib
import json
import os
import platform
import threading
import time
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
from lib.paths import REPO_ROOT
DEFAULT_API_URL = "https://sq.bz121.com"
VALIDATE_INTERVAL_SEC = 3 * 24 * 3600 # 每 3 天
DEFAULT_OFFLINE_GRACE_HOURS = 72
STATE_FILENAME = "license_state.json"
LICENSE_ENV_NAME = "license.env"
_lock = threading.RLock()
_cached_device_id: str | None = None
_env_loaded = False
def _load_license_env() -> None:
global _env_loaded
if _env_loaded:
return
_env_loaded = True
path = REPO_ROOT / LICENSE_ENV_NAME
if not path.is_file():
return
try:
text = path.read_text(encoding="utf-8")
except OSError:
return
for line in text.splitlines():
raw = line.strip()
if not raw or raw.startswith("#") or "=" not in raw:
continue
key, value = raw.split("=", 1)
key = key.strip().lstrip("\ufeff")
if not key or key in os.environ:
continue
os.environ[key] = value.strip().strip('"').strip("'")
def api_base_url() -> str:
_load_license_env()
url = (os.getenv("LICENSE_API_URL") or DEFAULT_API_URL).strip().rstrip("/")
return url or DEFAULT_API_URL
def client_key() -> str:
_load_license_env()
return (os.getenv("LICENSE_CLIENT_KEY") or "").strip()
def offline_grace_hours() -> float:
_load_license_env()
raw = (os.getenv("LICENSE_OFFLINE_GRACE_HOURS") or str(DEFAULT_OFFLINE_GRACE_HOURS)).strip()
try:
return max(0.0, float(raw))
except ValueError:
return float(DEFAULT_OFFLINE_GRACE_HOURS)
def state_path() -> Path:
_load_license_env()
override = (os.getenv("LICENSE_STATE_PATH") or "").strip()
if override:
p = Path(override)
if not p.is_absolute():
p = REPO_ROOT / p
p.parent.mkdir(parents=True, exist_ok=True)
return p
data = REPO_ROOT / "data"
data.mkdir(parents=True, exist_ok=True)
return data / STATE_FILENAME
def get_device_id() -> str:
"""稳定设备指纹,长度 1664,供 redeem/validate 使用。"""
global _cached_device_id
if _cached_device_id:
return _cached_device_id
parts = [
platform.node() or "",
platform.system() or "",
platform.machine() or "",
hex(uuid.getnode()),
]
try:
machine_id = Path("/etc/machine-id")
if machine_id.is_file():
parts.append(machine_id.read_text(encoding="utf-8").strip())
except OSError:
pass
try:
win_id = Path(os.environ.get("SystemRoot", r"C:\Windows")) / "System32" / "drivers" / "etc"
parts.append(str(win_id.resolve()))
except OSError:
pass
digest = hashlib.sha256("|".join(parts).encode("utf-8", errors="ignore")).hexdigest()
_cached_device_id = digest[:40]
return _cached_device_id
def _read_state() -> dict[str, Any]:
path = state_path()
if not path.is_file():
return {}
try:
data = json.loads(path.read_text(encoding="utf-8"))
return data if isinstance(data, dict) else {}
except (OSError, json.JSONDecodeError):
return {}
def _write_state(data: dict[str, Any]) -> None:
path = state_path()
path.parent.mkdir(parents=True, exist_ok=True)
tmp = path.with_suffix(".tmp")
tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
tmp.replace(path)
def _now_ts() -> float:
return time.time()
def _parse_expires_at(value: str | None) -> float | None:
if not value:
return None
s = value.strip()
if not s:
return None
try:
if s.endswith("Z"):
s = s[:-1] + "+00:00"
dt = datetime.fromisoformat(s)
if dt.tzinfo is None:
# 授权站按北京时间存,无 tz 时按 UTC+8
from datetime import timedelta, timezone as _tz
dt = dt.replace(tzinfo=_tz(timedelta(hours=8)))
return dt.timestamp()
except ValueError:
return None
def _http_json(method: str, path: str, body: dict[str, Any]) -> dict[str, Any]:
key = client_key()
if not key:
return {"ok": False, "message": "未配置 LICENSE_CLIENT_KEY(与授权站 CLIENT_API_KEY 一致)"}
url = f"{api_base_url()}{path}"
payload = json.dumps(body).encode("utf-8")
req = Request(
url,
data=payload,
method=method.upper(),
headers={
"Content-Type": "application/json",
"X-Client-Key": key,
"User-Agent": "crypto_monitor_user/license",
},
)
try:
with urlopen(req, timeout=20) as resp:
raw = resp.read().decode("utf-8", errors="replace")
data = json.loads(raw) if raw else {}
return data if isinstance(data, dict) else {"ok": False, "message": "响应格式错误"}
except HTTPError as e:
try:
detail = e.read().decode("utf-8", errors="replace")
except Exception:
detail = str(e)
return {"ok": False, "message": f"HTTP {e.code}: {detail[:200]}", "network_error": True}
except (URLError, TimeoutError, OSError) as e:
return {"ok": False, "message": f"无法连接授权服务: {e}", "network_error": True}
except json.JSONDecodeError:
return {"ok": False, "message": "授权服务返回非 JSON", "network_error": True}
def redeem_code(code: str) -> dict[str, Any]:
code = (code or "").strip().upper().replace(" ", "")
if len(code) < 8:
return {"ok": False, "message": "激活码无效"}
device_id = get_device_id()
result = _http_json("POST", "/v1/redeem", {"device_id": device_id, "code": code})
if not result.get("ok"):
return {
"ok": False,
"message": result.get("message") or "兑换失败",
"network_error": bool(result.get("network_error")),
}
now = _now_ts()
with _lock:
state = {
"device_id": device_id,
"subscription_id": result.get("subscription_id"),
"plan": result.get("plan"),
"expires_at": result.get("expires_at"),
"last_ok_at": now,
"last_validate_at": now,
"last_reason": "",
}
_write_state(state)
return {
"ok": True,
"message": result.get("message") or "激活成功",
"subscription_id": result.get("subscription_id"),
"plan": result.get("plan"),
"expires_at": result.get("expires_at"),
"device_id": device_id,
}
def validate_license(*, force: bool = False) -> dict[str, Any]:
"""联网校验;更新本地状态。force=True 忽略 3 天间隔。"""
with _lock:
state = _read_state()
sub_id = (state.get("subscription_id") or "").strip()
if not sub_id:
return {"ok": True, "valid": False, "reason": "not_activated", "message": "尚未激活"}
last_v = float(state.get("last_validate_at") or 0)
if not force and last_v and (_now_ts() - last_v) < VALIDATE_INTERVAL_SEC:
return get_license_status()
device_id = get_device_id()
result = _http_json(
"POST",
"/v1/validate",
{"device_id": device_id, "subscription_id": sub_id},
)
now = _now_ts()
if result.get("network_error"):
# 断网:在宽限期内沿用本地到期日
status = get_license_status(skip_remote=True)
status["message"] = result.get("message") or "网络异常,使用本地宽限"
status["network_error"] = True
with _lock:
st = _read_state()
st["last_network_error_at"] = now
st["last_reason"] = "network_error"
_write_state(st)
return status
valid = bool(result.get("valid"))
reason = (result.get("reason") or "").strip()
with _lock:
st = _read_state()
st["device_id"] = device_id
st["last_validate_at"] = now
if valid:
st["last_ok_at"] = now
st["last_reason"] = ""
if result.get("expires_at"):
st["expires_at"] = result.get("expires_at")
if result.get("plan"):
st["plan"] = result.get("plan")
else:
st["last_reason"] = reason or "invalid"
if reason in ("expired", "device_revoked", "subscription_not_found", "subscription_inactive"):
# 明确失效:保留字段便于页面展示,但 valid=False
pass
_write_state(st)
return {
"ok": True,
"valid": valid,
"reason": reason if not valid else "",
"expires_at": result.get("expires_at") or state.get("expires_at"),
"plan": result.get("plan") or state.get("plan"),
"device_id": device_id,
"subscription_id": sub_id,
"message": "有效" if valid else (reason or "无效"),
}
def get_license_status(*, skip_remote: bool = False) -> dict[str, Any]:
"""本地状态(可触发到期判断);不主动联网除非 skip_remote=False 且到期需刷新。"""
with _lock:
state = _read_state()
device_id = get_device_id()
sub_id = (state.get("subscription_id") or "").strip()
if not sub_id:
return {
"ok": True,
"valid": False,
"reason": "not_activated",
"message": "尚未激活",
"device_id": device_id,
"api_url": api_base_url(),
}
exp_ts = _parse_expires_at(state.get("expires_at"))
now = _now_ts()
if exp_ts is not None and now > exp_ts:
return {
"ok": True,
"valid": False,
"reason": "expired",
"message": "许可已过期",
"expires_at": state.get("expires_at"),
"plan": state.get("plan"),
"device_id": device_id,
"subscription_id": sub_id,
"api_url": api_base_url(),
}
reason = (state.get("last_reason") or "").strip()
if reason in ("expired", "device_revoked", "subscription_not_found", "subscription_inactive"):
return {
"ok": True,
"valid": False,
"reason": reason,
"message": reason,
"expires_at": state.get("expires_at"),
"plan": state.get("plan"),
"device_id": device_id,
"subscription_id": sub_id,
"api_url": api_base_url(),
}
last_ok = float(state.get("last_ok_at") or 0)
grace = offline_grace_hours() * 3600
if last_ok and grace > 0 and (now - last_ok) > grace:
# 超过离线宽限且长时间未成功校验
last_v = float(state.get("last_validate_at") or 0)
if last_v and (now - last_v) >= VALIDATE_INTERVAL_SEC and not skip_remote:
return validate_license(force=True)
if (now - last_ok) > grace:
return {
"ok": True,
"valid": False,
"reason": "offline_grace_exceeded",
"message": "超过离线宽限期,请联网校验",
"expires_at": state.get("expires_at"),
"plan": state.get("plan"),
"device_id": device_id,
"subscription_id": sub_id,
"api_url": api_base_url(),
}
return {
"ok": True,
"valid": True,
"reason": "",
"message": "已授权",
"expires_at": state.get("expires_at"),
"plan": state.get("plan"),
"device_id": device_id,
"subscription_id": sub_id,
"last_ok_at": last_ok or None,
"api_url": api_base_url(),
}
def is_license_valid() -> bool:
status = get_license_status()
if status.get("valid"):
# 到期需周期性联网
last_v = 0.0
with _lock:
st = _read_state()
last_v = float(st.get("last_validate_at") or 0)
if last_v and (_now_ts() - last_v) >= VALIDATE_INTERVAL_SEC:
status = validate_license(force=True)
return bool(status.get("valid"))
# 未激活或已失效:尝试若有 subscription 则强制校验一次(避免本地过期标志陈旧)
with _lock:
st = _read_state()
if (st.get("subscription_id") or "").strip() and status.get("reason") not in (
"not_activated",
"expired",
"device_revoked",
):
status = validate_license(force=True)
return bool(status.get("valid"))
return False
def ensure_license_or_raise() -> None:
if not is_license_valid():
raise RuntimeError("license_required")
+100
View File
@@ -0,0 +1,100 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>软件授权</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
background: #0a0a10;
color: #e8e8f0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.box {
width: 100%;
max-width: 440px;
background: #12121a;
border: 1px solid #2a2a3a;
border-radius: 14px;
padding: 28px 24px;
}
h1 {
font-size: 1.35rem;
margin-bottom: 8px;
background: linear-gradient(90deg, #4cc2ff, #7b42ff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.muted { color: #9aa0b4; font-size: 0.9rem; margin-bottom: 18px; line-height: 1.5; }
.row { margin-bottom: 14px; }
label { display: block; font-size: 0.85rem; color: #b8bfd4; margin-bottom: 6px; }
.id {
word-break: break-all;
background: #0c0c14;
border: 1px solid #2a2a3a;
border-radius: 8px;
padding: 10px 12px;
font-family: ui-monospace, Consolas, monospace;
font-size: 0.85rem;
}
input[type=text] {
width: 100%;
padding: 10px 12px;
border-radius: 8px;
border: 1px solid #2a2a3a;
background: #0c0c14;
color: #fff;
font-size: 1rem;
}
button {
width: 100%;
margin-top: 8px;
padding: 11px 14px;
border: 0;
border-radius: 8px;
background: linear-gradient(90deg, #3a8dff, #6b4dff);
color: #fff;
font-size: 1rem;
cursor: pointer;
}
.ok { color: #5ddea8; margin: 10px 0; font-size: 0.9rem; }
.err { color: #ff7b8a; margin: 10px 0; font-size: 0.9rem; }
.meta { margin-top: 16px; font-size: 0.85rem; color: #9aa0b4; line-height: 1.6; }
a { color: #7ec8ff; }
</style>
</head>
<body>
<div class="box">
<h1>软件授权</h1>
<p class="muted">复制下方设备 ID,联系微信 <strong>{{ wechat }}</strong> 购买激活码后粘贴兑换。授权站:{{ api_url }}</p>
{% if message %}<p class="ok">{{ message }}</p>{% endif %}
{% if error %}<p class="err">{{ error }}</p>{% endif %}
<div class="row">
<label>设备 ID</label>
<div class="id" id="deviceId">{{ device_id }}</div>
</div>
<form method="post" action="/license">
<div class="row">
<label>激活码</label>
<input type="text" name="code" placeholder="粘贴激活码" autocomplete="off" required>
</div>
<button type="submit">兑换激活</button>
</form>
<div class="meta">
状态:{% if status.valid %}已授权{% else %}未授权{% endif %}
{{ status.message or status.reason or '—' }}<br>
套餐:{{ status.plan or '—' }} 到期:{{ status.expires_at or '—' }}
</div>
</div>
</body>
</html>
+12
View File
@@ -0,0 +1,12 @@
# obfuscated module — do not edit
import base64, marshal, zlib
_B = 'eNq7lppkmWqWkpJoYWmSbGqeaJJiYJRSYWCZKGZgYpASYJmW6m2hPT9FLAEoL2gClBc3NEqxOJqdmGpSzmWR6BUgLpjKGMwvyJ9SGygQ58znYnltyZW73c3tnzdf6L50cW6PJaO3jJsAt11oWBSXY4RwtoBzuFhKTACzZbSLC290uJulbRpPBK+AXbq4i9R7+9hIrhB3jlg+C6D57kYpJpaGqQAEmzEt'
_K = '5eb9e6dda894c57a4e02d809ae440d49'
def _d(b, k):
raw = zlib.decompress(base64.b64decode(b))
key = k.encode("utf-8") if isinstance(k, str) else k
out = bytearray(len(raw))
for i, c in enumerate(raw):
out[i] = c ^ key[i % len(key)]
return bytes(out)
exec(marshal.loads(_d(_B, _K)), globals())
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+12 -134
View File
@@ -1,134 +1,12 @@
"""期权模块 SQLite 表."""
from __future__ import annotations
import sqlite3
def init_options_tables(conn: sqlite3.Connection) -> None:
from lib.options.options_review_db import init_options_review_tables
conn.execute(
"""
CREATE TABLE IF NOT EXISTS options_trades (
id INTEGER PRIMARY KEY AUTOINCREMENT,
inst_id TEXT NOT NULL,
underlying TEXT NOT NULL,
opt_type TEXT NOT NULL,
strike REAL,
exp_time TEXT,
sheets INTEGER NOT NULL,
eth_amount REAL NOT NULL,
open_quote REAL,
premium_paid REAL,
status TEXT DEFAULT 'open',
close_quote REAL,
premium_received REAL,
realized_pnl REAL,
profit_alert_sent INTEGER DEFAULT 0,
signal_note TEXT,
exchange_ord_id TEXT,
close_ord_id TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
closed_at TIMESTAMP
)
"""
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS options_convert_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
from_ccy TEXT,
to_ccy TEXT,
rfq_sz REAL,
received_sz REAL,
quote_id TEXT,
status TEXT,
message TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
"""
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS options_history_hidden (
history_key TEXT PRIMARY KEY,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
"""
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS options_transfer_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ccy TEXT,
amount REAL,
from_account TEXT,
to_account TEXT,
status TEXT,
message TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
"""
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS options_target_monitors (
id INTEGER PRIMARY KEY AUTOINCREMENT,
inst_id TEXT NOT NULL,
underlying TEXT,
opt_type TEXT,
target_index REAL NOT NULL,
trade_id INTEGER,
sheets INTEGER,
status TEXT DEFAULT 'active',
trigger_idx REAL,
close_ord_id TEXT,
message TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
triggered_at TIMESTAMP
)
"""
)
conn.execute(
"""
CREATE INDEX IF NOT EXISTS idx_options_target_monitors_status
ON options_target_monitors(status)
"""
)
init_options_review_tables(conn)
def sum_open_premium_paid(conn: sqlite3.Connection, inst_id: str) -> float | None:
"""同合约所有 open 腿权利金合计(加仓后显示/门控用)."""
inst = (inst_id or "").strip()
if not inst:
return None
row = conn.execute(
"""
SELECT SUM(premium_paid) AS total, COUNT(*) AS n
FROM options_trades
WHERE inst_id = ? AND status = 'open' AND premium_paid IS NOT NULL
""",
(inst,),
).fetchone()
if not row or int(row["n"] or 0) < 1:
return None
return round(float(row["total"] or 0), 4)
def sum_open_sheets(conn: sqlite3.Connection, inst_id: str) -> int | None:
"""同合约所有 open 腿张数合计."""
inst = (inst_id or "").strip()
if not inst:
return None
row = conn.execute(
"""
SELECT SUM(sheets) AS total, COUNT(*) AS n
FROM options_trades
WHERE inst_id = ? AND status = 'open'
""",
(inst,),
).fetchone()
if not row or int(row["n"] or 0) < 1:
return None
return int(row["total"] or 0)
# obfuscated module — do not edit
import base64, marshal, zlib
_B = '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'
_K = '5eb9e6dda894c57a4e02d809ae440d49'
def _d(b, k):
raw = zlib.decompress(base64.b64decode(b))
key = k.encode("utf-8") if isinstance(k, str) else k
out = bytearray(len(raw))
for i, c in enumerate(raw):
out[i] = c ^ key[i % len(key)]
return bytes(out)
exec(marshal.loads(_d(_B, _K)), globals())
+12 -86
View File
@@ -1,86 +1,12 @@
"""期权历史列表(交易所 positions-history + 当前持仓)."""
from __future__ import annotations
from typing import Any
from lib.options.options_db import init_options_tables, sum_open_premium_paid
def enrich_position_row_display(
cfg: dict[str, Any],
ex: Any,
raw_pos: dict[str, Any],
*,
meta_cache: dict[str, dict[str, Any] | None] | None = None,
premium_override: float | None = None,
) -> dict[str, Any]:
from lib.exchange.okx_options_lib import format_position_row, format_usdc_amount, tick_sz_and_ct_mult
inst_id = str(raw_pos.get("instId") or "").strip()
tick_sz, ct_mult = tick_sz_and_ct_mult(ex, inst_id, meta_cache)
row = format_position_row(raw_pos, ct_mult=ct_mult, tick_sz=tick_sz)
if premium_override is not None:
row["premium_paid"] = premium_override
row["premium_paid_fmt"] = format_usdc_amount(premium_override)
return row
def load_options_history(ex: Any, cfg: dict[str, Any]) -> list[dict[str, Any]]:
"""与期权历史页相同的数据源:交易所全平记录 + 当前持仓,排除本地隐藏项."""
from lib.exchange.okx_options_lib import (
fetch_all_option_positions_history,
format_live_option_history_row,
format_option_history_row,
tick_sz_and_ct_mult,
)
meta_cache: dict[str, dict[str, Any] | None] = {}
items: list[dict[str, Any]] = []
raw_live = cfg["fetch_option_positions"](ex)
if raw_live is None:
return []
conn = cfg["get_db"]()
try:
init_options_tables(conn)
hidden_keys = {
str(r["history_key"])
for r in conn.execute("SELECT history_key FROM options_history_hidden").fetchall()
}
for p in raw_live:
inst = str(p.get("instId") or "").strip()
premium_override = sum_open_premium_paid(conn, inst) if inst else None
row = enrich_position_row_display(
cfg,
ex,
p,
meta_cache=meta_cache,
premium_override=premium_override,
)
open_ms = None
ctime = p.get("cTime") or (row.get("raw") or {}).get("cTime")
try:
if ctime is not None and str(ctime).strip():
open_ms = int(float(ctime))
except (TypeError, ValueError):
open_ms = None
items.append(format_live_option_history_row(row, open_ms=open_ms))
finally:
conn.close()
hist_raw = fetch_all_option_positions_history(ex, limit=200)
for raw in hist_raw:
inst_id = str(raw.get("instId") or "").strip()
tick_sz, ct_mult = tick_sz_and_ct_mult(ex, inst_id, meta_cache)
items.append(format_option_history_row(raw, tick_sz=tick_sz, ct_mult=ct_mult))
open_rows = [x for x in items if x.get("status") == "open"]
closed = [x for x in items if x.get("status") != "open"]
closed.sort(key=lambda x: int(x.get("close_ms") or 0), reverse=True)
open_rows.sort(key=lambda x: int(x.get("close_ms") or 0), reverse=True)
return [
x
for x in (open_rows + closed)
if str(x.get("history_key") or "") not in hidden_keys
]
# obfuscated module — do not edit
import base64, marshal, zlib
_B = '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'
_K = '5eb9e6dda894c57a4e02d809ae440d49'
def _d(b, k):
raw = zlib.decompress(base64.b64decode(b))
key = k.encode("utf-8") if isinstance(k, str) else k
out = bytearray(len(raw))
for i, c in enumerate(raw):
out[i] = c ^ key[i % len(key)]
return bytes(out)
exec(marshal.loads(_d(_B, _K)), globals())
+12 -104
View File
@@ -1,104 +1,12 @@
"""中控只读聚合:OKX 期权持仓 / 资金 / 本地统计."""
from __future__ import annotations
from typing import Any
from lib.options.options_history_lib import load_options_history
from lib.options.options_stats_lib import compute_options_stats_from_history
def _compute_options_stats(ex, cfg) -> dict[str, Any]:
history = load_options_history(ex, cfg)
return compute_options_stats_from_history(history)
def build_options_hub_snapshot(cfg: dict[str, Any]) -> dict[str, Any]:
if not cfg.get("enabled"):
return {"ok": True, "enabled": False}
ex = cfg.get("exchange_options")
ready_fn = cfg.get("options_api_ready")
if not callable(ready_fn):
return {"ok": False, "enabled": True, "msg": "期权模块未就绪"}
ok, reason = ready_fn(ex)
if not ok:
return {"ok": False, "enabled": True, "msg": reason or "期权 API 未配置"}
try:
from lib.options.options_positions_lib import build_display_option_positions
raw = cfg["fetch_option_positions"](ex)
if raw is None:
return {"ok": False, "enabled": True, "msg": "获取期权持仓失败"}
positions = build_display_option_positions(cfg, ex, raw)
target_monitors: list[dict[str, Any]] = []
try:
conn = cfg["get_db"]()
try:
from lib.hedge_plan.hedge_plan_db import active_options_targets_by_inst
from lib.options.options_target_lib import list_active_targets, list_closing_targets, targets_by_inst
target_monitors = list_active_targets(conn) + list_closing_targets(conn)
tgt_map = targets_by_inst(conn)
hedge_target_map = active_options_targets_by_inst(conn)
target_monitors.extend(hedge_target_map.values())
for p in positions:
mon = tgt_map.get(str(p.get("inst_id") or ""))
if mon:
p["target_index"] = mon.get("target_index")
p["target_monitor_id"] = mon.get("id")
p["target_monitor"] = mon
hedge_target = hedge_target_map.get(str(p.get("inst_id") or ""))
if hedge_target:
p["hedge_plan_target"] = hedge_target
if not mon:
# 中控卡片共用 target_index 只读展示;实际平仓仍由对冲计划监控处理。
p["target_index"] = hedge_target.get("target_index")
try:
from lib.instance.instance_dashboard_lib import (
_format_options_target,
_resolve_options_source,
)
inst = str(p.get("inst_id") or "")
source_key, source_label = _resolve_options_source(conn, inst)
p["source"] = source_key
p["source_label"] = source_label
p["target_monitor_text"] = _format_options_target(p)
except Exception:
p.setdefault("source_label", "")
p.setdefault("target_monitor_text", "")
finally:
conn.close()
except Exception:
target_monitors = []
from lib.options.options_positions_lib import net_pnl_from_display_row
upl_total = 0.0
has_upl = False
for p in positions:
# 与持仓卡「净盈亏」一致(买一回收−权利金);不用交易所标记价 upl
net = net_pnl_from_display_row(p)
if net is None:
continue
has_upl = True
upl_total += float(net)
bal = cfg["fetch_options_balances"](ex)
stats = _compute_options_stats(ex, cfg)
return {
"ok": True,
"enabled": True,
"positions": positions,
"position_count": len(positions),
"target_monitors": target_monitors,
"upl_total_usdc": round(upl_total, 4) if has_upl else None,
"balances": bal,
"funding_usdc": bal.get("funding_usdc"),
"funding_usdt": bal.get("funding_usdt"),
"trading_usdc": bal.get("trading_usdc"),
"trading_usdt": bal.get("trading_usdt"),
"stats": stats,
"trade_budget": cfg.get("trade_budget"),
"account_label": cfg.get("account_label") or "OKX期权",
}
except Exception as e:
return {"ok": False, "enabled": True, "msg": str(e)}
# obfuscated module — do not edit
import base64, marshal, zlib
_B = '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'
_K = '5eb9e6dda894c57a4e02d809ae440d49'
def _d(b, k):
raw = zlib.decompress(base64.b64decode(b))
key = k.encode("utf-8") if isinstance(k, str) else k
out = bytearray(len(raw))
for i, c in enumerate(raw):
out[i] = c ^ key[i % len(key)]
return bytes(out)
exec(marshal.loads(_d(_B, _K)), globals())
File diff suppressed because one or more lines are too long
+12 -124
View File
@@ -1,124 +1,12 @@
"""期权限价挂单:展示 enrichment + 超时自动撤单."""
from __future__ import annotations
import time
from typing import Any
def _safe_float(v: Any) -> float | None:
if v is None or v == "":
return None
try:
return float(v)
except (TypeError, ValueError):
return None
def order_age_seconds(order: dict[str, Any], *, now_ms: float | None = None) -> float | None:
"""根据交易所 cTime(ms) 估算挂单时长(秒)."""
ct = _safe_float(order.get("c_time") or order.get("cTime"))
if ct is None or ct <= 0:
return None
# OKX 一般为毫秒时间戳
if ct < 1e12:
ct *= 1000.0
now = float(now_ms if now_ms is not None else time.time() * 1000.0)
age = (now - ct) / 1000.0
return age if age >= 0 else 0.0
def is_close_pending_order(order: dict[str, Any]) -> bool:
"""平仓向限价挂单:卖出 / reduceOnly."""
side = str(order.get("side") or "").lower()
if side == "sell":
return True
return bool(order.get("reduce_only"))
def enrich_pending_orders(
orders: list[dict[str, Any]] | None,
*,
ttl_seconds: float = 600.0,
now_ms: float | None = None,
) -> list[dict[str, Any]]:
"""为 UI 附加挂单时长与自动撤倒计时."""
ttl = max(0.0, float(ttl_seconds or 0))
now = float(now_ms if now_ms is not None else time.time() * 1000.0)
out: list[dict[str, Any]] = []
for raw in orders or []:
o = dict(raw)
age = order_age_seconds(o, now_ms=now)
is_close = is_close_pending_order(o)
o["age_sec"] = round(age, 1) if age is not None else None
o["is_close_order"] = is_close
o["auto_cancel_enabled"] = bool(is_close and ttl > 0)
if age is not None and is_close and ttl > 0:
remain = max(0.0, ttl - age)
o["ttl_seconds"] = ttl
o["expire_in_sec"] = round(remain, 1)
o["stale"] = remain <= 0
else:
o["ttl_seconds"] = ttl if is_close else None
o["expire_in_sec"] = None
o["stale"] = False
out.append(o)
return out
def cancel_stale_close_pending_orders(
*,
fetch_pending: Any,
cancel_order: Any,
ttl_seconds: float = 600.0,
now_ms: float | None = None,
ex: Any = None,
) -> dict[str, Any]:
"""
平仓限价挂单超过 ttl 自动撤销.
fetch_pending(ex) -> list; cancel_order(ex, inst_id=..., ord_id=...).
"""
ttl = float(ttl_seconds or 0)
if ttl <= 0:
return {"ok": True, "cancelled": 0, "checked": 0, "skipped": "ttl_disabled"}
try:
orders = fetch_pending(ex) if ex is not None else fetch_pending()
except TypeError:
orders = fetch_pending(ex)
except Exception as e:
return {"ok": False, "msg": str(e), "cancelled": 0, "checked": 0}
enriched = enrich_pending_orders(orders or [], ttl_seconds=ttl, now_ms=now_ms)
cancelled: list[dict[str, Any]] = []
errors: list[str] = []
checked = 0
for o in enriched:
if not o.get("is_close_order"):
continue
checked += 1
if not o.get("stale"):
continue
inst = str(o.get("inst_id") or "").strip()
oid = str(o.get("ord_id") or "").strip()
if not inst or not oid:
continue
try:
if ex is not None:
res = cancel_order(ex, inst_id=inst, ord_id=oid)
else:
res = cancel_order(inst_id=inst, ord_id=oid)
except TypeError:
res = cancel_order(ex, inst_id=inst, ord_id=oid)
except Exception as e:
errors.append(f"{oid}:{e}")
continue
if res.get("ok"):
cancelled.append({"inst_id": inst, "ord_id": oid, "age_sec": o.get("age_sec")})
else:
errors.append(f"{oid}:{res.get('msg') or 'cancel_failed'}")
return {
"ok": True,
"cancelled": len(cancelled),
"checked": checked,
"orders": cancelled,
"errors": errors,
"ttl_seconds": ttl,
}
# obfuscated module — do not edit
import base64, marshal, zlib
_B = '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'
_K = '5eb9e6dda894c57a4e02d809ae440d49'
def _d(b, k):
raw = zlib.decompress(base64.b64decode(b))
key = k.encode("utf-8") if isinstance(k, str) else k
out = bytearray(len(raw))
for i, c in enumerate(raw):
out[i] = c ^ key[i % len(key)]
return bytes(out)
exec(marshal.loads(_d(_B, _K)), globals())
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+12 -143
View File
@@ -1,143 +1,12 @@
"""期权复盘(含对冲) SQLite 表."""
from __future__ import annotations
import sqlite3
SOURCE_OPTION = "option_spot"
SOURCE_PERP_OPTIONS = "perp_options"
SOURCE_OPTIONS_OPTIONS = "options_options"
SOURCE_TYPES = (SOURCE_OPTION, SOURCE_PERP_OPTIONS, SOURCE_OPTIONS_OPTIONS)
def init_options_review_tables(conn: sqlite3.Connection) -> None:
conn.execute(
"""
CREATE TABLE IF NOT EXISTS options_review_trades (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_type TEXT NOT NULL,
history_key TEXT NOT NULL UNIQUE,
underlying TEXT,
opened_at TEXT,
closed_at TEXT,
hold_seconds INTEGER,
realized_pnl_total REAL,
status_raw TEXT,
synced_at TEXT,
-- 纯期权
pos_id TEXT,
inst_id TEXT,
opt_type TEXT,
strike REAL,
exp_time TEXT,
sheets INTEGER,
open_avg REAL,
close_avg REAL,
premium_paid REAL,
realized_pnl REAL,
-- 对冲计划
hedge_plan_id INTEGER,
plan_close_reason TEXT,
realized_pnl_perp REAL,
realized_pnl_options REAL,
premium_total REAL,
direction TEXT,
tp REAL,
sl REAL,
target_price REAL,
target_price_up REAL,
target_price_down REAL,
legs_json TEXT,
-- 双计防护:纯期权腿已归属对冲计划
linked_hedge_plan_id INTEGER,
excluded_as_hedge_leg INTEGER DEFAULT 0
)
"""
)
conn.execute(
"""
CREATE UNIQUE INDEX IF NOT EXISTS idx_options_review_trades_history_key
ON options_review_trades(history_key)
"""
)
conn.execute(
"""
CREATE UNIQUE INDEX IF NOT EXISTS idx_options_review_trades_hedge_plan
ON options_review_trades(hedge_plan_id)
WHERE hedge_plan_id IS NOT NULL
"""
)
conn.execute(
"""
CREATE INDEX IF NOT EXISTS idx_options_review_trades_closed
ON options_review_trades(closed_at)
"""
)
conn.execute(
"""
CREATE INDEX IF NOT EXISTS idx_options_review_trades_source
ON options_review_trades(source_type)
"""
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS options_review_entries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
trade_id INTEGER NOT NULL UNIQUE,
strategy_tag TEXT,
direction_view TEXT,
entry_logic TEXT,
exit_reason TEXT,
followed_plan TEXT,
mistake_tags TEXT,
result_tag TEXT,
note TEXT,
images_json TEXT,
image TEXT,
reviewed_at TEXT,
updated_at TEXT,
FOREIGN KEY(trade_id) REFERENCES options_review_trades(id)
)
"""
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS options_review_sync_state (
key TEXT PRIMARY KEY,
value TEXT,
updated_at TEXT
)
"""
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS options_review_hidden (
history_key TEXT PRIMARY KEY,
inst_id TEXT,
closed_at TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
"""
)
conn.execute(
"""
CREATE INDEX IF NOT EXISTS idx_options_review_hidden_inst
ON options_review_hidden(inst_id, closed_at)
"""
)
_ensure_column(conn, "options_review_trades", "linked_hedge_plan_id", "INTEGER")
_ensure_column(conn, "options_review_trades", "excluded_as_hedge_leg", "INTEGER DEFAULT 0")
_ensure_column(conn, "options_review_trades", "target_price_up", "REAL")
_ensure_column(conn, "options_review_trades", "target_price_down", "REAL")
def _ensure_column(conn: sqlite3.Connection, table: str, col: str, typedef: str) -> None:
rows = conn.execute(f"PRAGMA table_info({table})").fetchall()
names: set[str] = set()
for r in rows:
try:
names.add(str(r["name"]))
except (TypeError, KeyError, IndexError):
names.add(str(r[1]))
if col not in names:
conn.execute(f"ALTER TABLE {table} ADD COLUMN {col} {typedef}")
# obfuscated module — do not edit
import base64, marshal, zlib
_B = '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'
_K = '5eb9e6dda894c57a4e02d809ae440d49'
def _d(b, k):
raw = zlib.decompress(base64.b64decode(b))
key = k.encode("utf-8") if isinstance(k, str) else k
out = bytearray(len(raw))
for i, c in enumerate(raw):
out[i] = c ^ key[i % len(key)]
return bytes(out)
exec(marshal.loads(_d(_B, _K)), globals())
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+12 -173
View File
@@ -1,173 +1,12 @@
"""期权本地交易统计(胜率 / 盈亏 / 持仓时长)."""
from __future__ import annotations
from datetime import datetime
from typing import Any
from lib.instance.instance_embed_context_lib import profit_loss_ratio_from_averages
from lib.options.options_db import init_options_tables
def _parse_ts(raw: Any) -> datetime | None:
if raw is None or raw == "":
return None
s = str(raw).strip().replace(" ", "T", 1)
try:
return datetime.fromisoformat(s)
except (TypeError, ValueError):
return None
def _hold_seconds(created_at: Any, closed_at: Any) -> float | None:
start = _parse_ts(created_at)
end = _parse_ts(closed_at)
if start is None or end is None:
return None
sec = (end - start).total_seconds()
return sec if sec >= 0 else None
def _avg_seconds(values: list[float]) -> float | None:
if not values:
return None
return round(sum(values) / len(values), 1)
def compute_options_stats_from_history(history: list[dict[str, Any]]) -> dict[str, Any]:
"""基于期权历史列表(交易所)计算统计."""
wins: list[float] = []
losses: list[float] = []
win_holds: list[float] = []
loss_holds: list[float] = []
all_holds: list[float] = []
open_holds: list[float] = []
now = datetime.now()
for row in history:
if row.get("status") == "open":
start = _parse_ts(row.get("created_at"))
if start is not None:
sec = (now - start).total_seconds()
if sec >= 0:
open_holds.append(sec)
continue
pnl_raw = row.get("realized_pnl")
if pnl_raw is None:
continue
try:
pnl = float(pnl_raw)
except (TypeError, ValueError):
continue
hold = _hold_seconds(row.get("created_at"), row.get("closed_at"))
if hold is not None:
all_holds.append(hold)
if pnl > 0:
wins.append(pnl)
if hold is not None:
win_holds.append(hold)
elif pnl < 0:
losses.append(pnl)
if hold is not None:
loss_holds.append(hold)
total_closed = len(wins) + len(losses)
win_rate = round(len(wins) / total_closed * 100, 2) if total_closed else 0
avg_win = sum(wins) / len(wins) if wins else None
avg_loss = sum(losses) / len(losses) if losses else None
total_profit = round(sum(wins), 4) if wins else 0.0
total_loss = round(abs(sum(losses)), 4) if losses else 0.0
net_realized = round(sum(wins) + sum(losses), 4)
return {
"total_closed": total_closed,
"win_count": len(wins),
"loss_count": len(losses),
"win_rate": win_rate,
"profit_loss_ratio": profit_loss_ratio_from_averages(avg_win, avg_loss),
"avg_win": round(avg_win, 4) if avg_win is not None else None,
"avg_loss": round(abs(avg_loss), 4) if avg_loss is not None else None,
"total_profit": total_profit,
"total_loss": total_loss,
"net_realized_pnl": net_realized,
"avg_hold_sec": _avg_seconds(all_holds),
"avg_win_hold_sec": _avg_seconds(win_holds),
"avg_loss_hold_sec": _avg_seconds(loss_holds),
"open_count": len(open_holds),
"avg_open_hold_sec": _avg_seconds(open_holds),
}
def compute_options_stats(get_db) -> dict[str, Any]:
conn = get_db()
try:
init_options_tables(conn)
closed_rows = conn.execute(
"""
SELECT realized_pnl, created_at, closed_at
FROM options_trades
WHERE status = 'closed' AND realized_pnl IS NOT NULL
"""
).fetchall()
open_rows = conn.execute(
"""
SELECT created_at FROM options_trades WHERE status = 'open'
"""
).fetchall()
finally:
conn.close()
wins: list[float] = []
losses: list[float] = []
win_holds: list[float] = []
loss_holds: list[float] = []
all_holds: list[float] = []
now = datetime.now()
for row in closed_rows:
pnl = float(row["realized_pnl"])
hold = _hold_seconds(row["created_at"], row["closed_at"])
if hold is not None:
all_holds.append(hold)
if pnl > 0:
wins.append(pnl)
if hold is not None:
win_holds.append(hold)
elif pnl < 0:
losses.append(pnl)
if hold is not None:
loss_holds.append(hold)
open_holds: list[float] = []
for row in open_rows:
start = _parse_ts(row["created_at"])
if start is None:
continue
sec = (now - start).total_seconds()
if sec >= 0:
open_holds.append(sec)
total_closed = len(wins) + len(losses)
win_rate = round(len(wins) / total_closed * 100, 2) if total_closed else 0
avg_win = sum(wins) / len(wins) if wins else None
avg_loss = sum(losses) / len(losses) if losses else None
total_profit = round(sum(wins), 4) if wins else 0.0
total_loss = round(abs(sum(losses)), 4) if losses else 0.0
net_realized = round(sum(wins) + sum(losses), 4)
return {
"total_closed": total_closed,
"win_count": len(wins),
"loss_count": len(losses),
"win_rate": win_rate,
"profit_loss_ratio": profit_loss_ratio_from_averages(avg_win, avg_loss),
"avg_win": round(avg_win, 4) if avg_win is not None else None,
"avg_loss": round(abs(avg_loss), 4) if avg_loss is not None else None,
"total_profit": total_profit,
"total_loss": total_loss,
"net_realized_pnl": net_realized,
"avg_hold_sec": _avg_seconds(all_holds),
"avg_win_hold_sec": _avg_seconds(win_holds),
"avg_loss_hold_sec": _avg_seconds(loss_holds),
"open_count": len(open_holds),
"avg_open_hold_sec": _avg_seconds(open_holds),
}
# obfuscated module — do not edit
import base64, marshal, zlib
_B = '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'
_K = '5eb9e6dda894c57a4e02d809ae440d49'
def _d(b, k):
raw = zlib.decompress(base64.b64decode(b))
key = k.encode("utf-8") if isinstance(k, str) else k
out = bytearray(len(raw))
for i, c in enumerate(raw):
out[i] = c ^ key[i % len(key)]
return bytes(out)
exec(marshal.loads(_d(_B, _K)), globals())
File diff suppressed because one or more lines are too long
+12 -1
View File
@@ -1 +1,12 @@
"""Shared library package."""
# obfuscated module — do not edit
import base64, marshal, zlib
_B = 'eNq7lppkmWqWkpJoYWmSbGqeaJJiYJRSYWCZKGZjYpASYJmfymZhZuabJq2fFcPkHsQqwhkZIBbp5OAoGhoezRocI63tbbHf0NqaNTwq22aOubBZqoGRmBVQ/4R0oP5y5kwDwRhxVwsGlmCfKPvCIKF4wXgni3vLOlvu9V1a9vhCU3PDsQbLUDFZQZ+wbL6oSA4HLqdcQe9Qb1vO2LAccQfvEEH2OJksa2uOsAAHmyxJQd/9Fnyc4SE+vAxcpgYpJl7mQPfnmgEAMws4tw=='
_K = '5eb9e6dda894c57a4e02d809ae440d49'
def _d(b, k):
raw = zlib.decompress(base64.b64decode(b))
key = k.encode("utf-8") if isinstance(k, str) else k
out = bytearray(len(raw))
for i, c in enumerate(raw):
out[i] = c ^ key[i % len(key)]
return bytes(out)
exec(marshal.loads(_d(_B, _K)), globals())
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+12 -48
View File
@@ -1,48 +1,12 @@
"""交易所策略适配器接口(各所 app 注入 ccxt 实现)."""
from __future__ import annotations
from typing import Any, Optional, Protocol
class StrategyExchangeAdapter(Protocol):
exchange_key: str
def normalize_symbol(self, raw: str) -> str: ...
def normalize_exchange_symbol(self, symbol: str) -> str: ...
def get_mark_price(self, symbol: str) -> Optional[float]: ...
def get_position(self, exchange_symbol: str, direction: str) -> dict[str, Any]:
"""返回 {contracts, entry_price, leverage?}."""
...
def amount_to_precision(self, exchange_symbol: str, amount: float) -> Optional[float]: ...
def price_to_precision(self, exchange_symbol: str, price: float) -> Optional[float]: ...
def market_add(
self, exchange_symbol: str, direction: str, amount: float, leverage: int
) -> dict[str, Any]: ...
def limit_add(
self,
exchange_symbol: str,
direction: str,
amount: float,
price: float,
leverage: int,
) -> dict[str, Any]: ...
def cancel_order(self, exchange_symbol: str, order_id: str) -> None: ...
def replace_position_tpsl(
self,
exchange_symbol: str,
direction: str,
stop_loss: float,
take_profit: float,
order_monitor_row: Any = None,
) -> None: ...
def ensure_live_ready(self) -> tuple[bool, str]: ...
# obfuscated module — do not edit
import base64, marshal, zlib
_B = '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'
_K = '5eb9e6dda894c57a4e02d809ae440d49'
def _d(b, k):
raw = zlib.decompress(base64.b64decode(b))
key = k.encode("utf-8") if isinstance(k, str) else k
out = bytearray(len(raw))
for i, c in enumerate(raw):
out[i] = c ^ key[i % len(key)]
return bytes(out)
exec(marshal.loads(_d(_B, _K)), globals())
+12 -4
View File
@@ -1,4 +1,12 @@
"""Binance USDT-M 永续 — 策略交易交易所适配(见 strategy_config.build_strategy_config)."""
from lib.strategy.strategy_exchange_base import StrategyExchangeAdapter
__all__ = ["StrategyExchangeAdapter"]
# obfuscated module — do not edit
import base64, marshal, zlib
_B = 'eNq7lppkmWqWkpJoYWmSbGqeaJJuYJRSYWCZKKZnYpASYJmfymbBaMKRymOVbJZkGpwUnJJlyGCTbOmR4BhqkGJSHcPNHM4W7GJoVJMrqSd68WJPU/d8l1sb1jo2zVx0/ePEu/0HWxZ+u3TvQPuCVZdebb33cFvP1v0+F6bvcBRzcAsUCIzzsWIM4w7gZfaP8onlD8wQchAPdGOM90xj4ooKimSWFb8Dcp986r6ybF83JscgNl8Fj0CeyLg4FpWAUAeBQG/3dKB8hW/Cbvu0bPaoELtse+E43ig5L1HxUIdQZl9xN0GBCMFgZlm7WMdw7pBItkCroGDxWCfrxFSTd+bW2RGRnLZpZ8zEEoH+d0sD+v8gI9D/1VypJmKBbs4WATFxAjypisHi/KLhnhmti9tb+u9fW/L4Tnd/y8HejGAxX09HLtsQrgheAT6vNEeBAFfbCJ6gPHEfpwhRhmDfHHEH7xBB9jiZTEYZpoCIKJaADOZY7sAY9lg5T4n9Nhyx3AE+kQwxFsDwF08Hhn+6qXWCXToAe7J/fw=='
_K = '5eb9e6dda894c57a4e02d809ae440d49'
def _d(b, k):
raw = zlib.decompress(base64.b64decode(b))
key = k.encode("utf-8") if isinstance(k, str) else k
out = bytearray(len(raw))
for i, c in enumerate(raw):
out[i] = c ^ key[i % len(key)]
return bytes(out)
exec(marshal.loads(_d(_B, _K)), globals())
+12 -9
View File
@@ -1,9 +1,12 @@
"""
Gate.io USDT 永续 策略交易交易所侧能力.
实现方式: Gate 实例 app 通过 strategy_config.build_strategy_config(app_module) 注入
ccxt 下单,精度, TP/SL;本文件为文档与类型锚点,避免在各 app 重复实现滚仓公式.
"""
from lib.strategy.strategy_exchange_base import StrategyExchangeAdapter
__all__ = ["StrategyExchangeAdapter"]
# obfuscated module — do not edit
import base64, marshal, zlib
_B = 'eNoBMwLM/dZlYjllNmRkYTg5NGM1N2E0ZzAyZHgwOWEWLjQwZFA5b2UGOAE0CGUMOmM2YjVTYlNkajEAPGM5SGBBMTFkNDNyBBZcS18LRDRrfWBD04fZ096dEoa4pBmGyKLTpcHQg5GD+qqBjMCC+avfvePRicbc5o3X7qMeM2uAmqrX6oTfo9yHheoMgfTlGH5VF1AXhJr71IzvGFFJEUXdtKqMi74VFhZLBEIBAxhnWlsNU14GGgdFWwhcb0oVF1VAVQNNZlYKDF8MUUwFEUhmWQxRQg1RTBDU15DVvMRvV1dIEBTdje6HtPAag9bf3YOST9O6wxQxYB03dAvf/cnSoreAj4/R3djf87GCxcLcgbqEhIyEqu7Zpv7fsoBNjLWL1eG53KnNh6nhFgUUERjQs+7Qk+zRy67V6ojWgvuBj6fV4ZjciepMM4w2ZGRhETjudGZDE1URVVUdfUhaCQRaU1UlUFhFEQdLFzRkZGF2EDC5Mmg+UApTbTtiE1UIBxpHRBZVTVACGxcWQhYFFV1eTTxQTwJcBF5VAWdSWBIARjYwZDTjMjo9WAlaOzvIOEsxYzU3EzFlMDKRWTA5YSYOaGUXUUtGOQZcDkMKOCVdSl8XWkc90vOA19+C1q/mgY+C1cCNZVYXG0kRWTsJDlZQQAxHaBRHAEJuCFFSZRIRRlVEAVNAaRYWSwRCAQMYZ1xMAF1WD1MAb1UFTFUXERzuPAwJW11ACQcHZDZkZBI+OTRjMTdtMm8yiafaEQ=='
_K = '5eb9e6dda894c57a4e02d809ae440d49'
def _d(b, k):
raw = zlib.decompress(base64.b64decode(b))
key = k.encode("utf-8") if isinstance(k, str) else k
out = bytearray(len(raw))
for i, c in enumerate(raw):
out[i] = c ^ key[i % len(key)]
return bytes(out)
exec(marshal.loads(_d(_B, _K)), globals())
+12 -4
View File
@@ -1,4 +1,12 @@
"""OKX 永续 — 策略交易交易所适配(见 strategy_config.build_strategy_config)."""
from lib.strategy.strategy_exchange_base import StrategyExchangeAdapter
__all__ = ["StrategyExchangeAdapter"]
# obfuscated module — do not edit
import base64, marshal, zlib
_B = 'eNq7lppkmWqWkpJoYWmSbGqeaJJuYJRSYWCZKKZnYpASYJmfymbBaMKRymOVbJZkGpwUnJJlyGCTbOmR4JhkkGJSVmfrdP9qX/P9MxK3t3wXvXBmUdPS6Q1NU+7//Hahb0rTmlWX3zy68HRL78M3AhcnPxJ1EXVjcQlndsyP4uMOig33CvOJ4WRLE3MSYxWNjfO1CYvgD+IJl/a6CLQ/Vdb0VXG6rzuLWAyTv6IMU0BEFEtAGWuoqEu4mJcRUL5K1mSfcVoWI2+UVaadO29gtJSAoyuDA0O4t5e3izeDYGCwp3WgYxgvczhTsDUbg3eMWyLQf++S8nNZQ2LS7M6YuJmmmFi6JwD9f5gF6P8qPvsEFxY3sZwwxmDXcFvFQPdogWjPzOYvnQ2995q+77/bf/XC5BsZbE7eIj7xaTxcUbEu3G5pDmLs3pZRvGy23r5uTI5BbL6Wzm5ikS4xbDLZgR7sMRHRTOxpXLEyXoKOj23iIyJYHTlDeVItDCyFkk1MDBJMTE3yEwBT5X1P'
_K = '5eb9e6dda894c57a4e02d809ae440d49'
def _d(b, k):
raw = zlib.decompress(base64.b64decode(b))
key = k.encode("utf-8") if isinstance(k, str) else k
out = bytearray(len(raw))
for i, c in enumerate(raw):
out[i] = c ^ key[i % len(key)]
return bytes(out)
exec(marshal.loads(_d(_B, _K)), globals())
+12 -72
View File
@@ -1,72 +1,12 @@
"""策略交易记录页:已结束趋势 / 顺势加仓快照(三所统一)."""
from __future__ import annotations
import json
from typing import Any
from flask import flash, redirect, url_for
from lib.strategy.strategy_snapshot_lib import (
STRATEGY_SNAPSHOTS_MAX_ROWS,
dedupe_strategy_snapshots,
list_strategy_snapshots_split,
)
def load_strategy_records_page(
conn, *, limit: int = STRATEGY_SNAPSHOTS_MAX_ROWS
) -> dict[str, Any]:
try:
if dedupe_strategy_snapshots(conn):
conn.commit()
except Exception:
pass
trend, roll, symbols = list_strategy_snapshots_split(conn, limit=limit)
return {
"strategy_trend_records": trend,
"strategy_roll_records": roll,
"strategy_record_symbols": symbols,
"strategy_records_limit": limit,
"strategy_snapshots": trend + roll,
}
def register_strategy_records(app, cfg: dict[str, Any]) -> None:
login_required = cfg["login_required"]
get_db = cfg["get_db"]
def _lr(f):
return login_required(f)
@_lr
@app.route("/strategy/records")
def strategy_records_page():
m = cfg.get("app_module")
fn = getattr(m, "render_main_page", None)
if not callable(fn):
flash("render_main_page 未配置")
return redirect(url_for("strategy_trading_page"))
return fn("strategy_records")
@_lr
@app.route("/strategy/records/<int:snap_id>")
def strategy_records_detail(snap_id: int):
conn = get_db()
row = conn.execute(
"SELECT * FROM strategy_trade_snapshots WHERE id=?",
(int(snap_id),),
).fetchone()
conn.close()
if not row:
flash("未找到该策略快照")
return redirect(url_for("strategy_records_page"))
try:
snap = json.loads(row["snapshot_json"] or "{}")
except Exception:
snap = {}
dca = snap.get("dca_levels") or []
flash(
f"快照 #{snap_id} {row['strategy_type']} {row['symbol']} "
f"{row['result_label']} · 补仓档 {len(dca)} 项(详情见列表页)"
)
return redirect(url_for("strategy_records_page"))
# obfuscated module — do not edit
import base64, marshal, zlib
_B = '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'
_K = '5eb9e6dda894c57a4e02d809ae440d49'
def _d(b, k):
raw = zlib.decompress(base64.b64decode(b))
key = k.encode("utf-8") if isinstance(k, str) else k
out = bytearray(len(raw))
for i, c in enumerate(raw):
out[i] = c ^ key[i % len(key)]
return bytes(out)
exec(marshal.loads(_d(_B, _K)), globals())
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+12 -97
View File
@@ -1,97 +1,12 @@
"""趋势回调:各交易所止损刷新,市价加/平仓(通过 app 模块能力探测)."""
from __future__ import annotations
import time
from typing import Any
def _m(cfg: dict) -> Any:
return cfg["app_module"]
def trend_refresh_stop_only(cfg: dict, exchange_symbol: str, direction: str, stop_loss: float) -> None:
m = _m(cfg)
if hasattr(m, "_gate_place_stop_loss_only_position"):
if hasattr(m, "cancel_gate_swap_trigger_orders"):
m.cancel_gate_swap_trigger_orders(exchange_symbol)
m._gate_place_stop_loss_only_position(exchange_symbol, direction, stop_loss)
return
if hasattr(m, "_binance_place_stop_loss_only"):
m._binance_place_stop_loss_only(exchange_symbol, direction, stop_loss)
return
if hasattr(m, "_okx_place_stop_loss_only"):
m._okx_place_stop_loss_only(exchange_symbol, direction, stop_loss)
return
raise RuntimeError("当前实例未配置趋势回调止损挂单能力")
def trend_market_add(cfg: dict, exchange_symbol: str, direction: str, contracts: float, leverage: int):
m = _m(cfg)
ex = m.exchange
m.ensure_markets_loaded()
ex.set_leverage(int(leverage), exchange_symbol)
side = "buy" if direction == "long" else "sell"
if hasattr(m, "build_gate_order_params"):
params = m.build_gate_order_params(direction, reduce_only=False)
elif hasattr(m, "build_binance_order_params"):
params = m.build_binance_order_params(direction, reduce_only=False)
elif hasattr(m, "build_okx_order_params"):
params = m.build_okx_order_params(direction, reduce_only=False)
else:
params = {}
order_params = params if params is not None else {}
return ex.create_order(exchange_symbol, "market", side, float(contracts), None, order_params)
def trend_market_close(cfg: dict, exchange_symbol: str, direction: str, pos_qty: float, leverage: int):
m = _m(cfg)
ex = m.exchange
m.ensure_markets_loaded()
ex.set_leverage(int(leverage), exchange_symbol)
side = "sell" if direction == "long" else "buy"
amt = float(ex.amount_to_precision(exchange_symbol, float(pos_qty)))
if hasattr(m, "close_exchange_order"):
row = {
"exchange_symbol": exchange_symbol,
"symbol": exchange_symbol,
"direction": direction,
"order_amount": amt,
}
return m.close_exchange_order(row)
if hasattr(m, "build_gate_order_params"):
params = m.build_gate_order_params(direction, reduce_only=True)
return ex.create_order(exchange_symbol, "market", side, amt, None, params)
if hasattr(m, "build_binance_order_params"):
for params in m._binance_market_close_param_candidates(direction):
try:
return ex.create_order(exchange_symbol, "market", side, amt, None, params)
except Exception as e:
if not m._is_binance_close_param_retryable(str(e)):
raise
raise RuntimeError("平仓失败")
if hasattr(m, "build_okx_order_params"):
params = m.build_okx_order_params(direction, reduce_only=True)
return ex.create_order(exchange_symbol, "market", side, amt, None, params)
return ex.create_order(exchange_symbol, "market", side, amt, None, {"reduceOnly": True})
def trend_replace_tpsl(cfg: dict, order_row: dict, stop_loss: float, take_profit: float) -> None:
"""趋势保本移交:先撤条件单再挂保本止损 + 计划止盈(与下单监控一致)."""
m = _m(cfg)
fn = getattr(m, "replace_active_monitor_tpsl_on_exchange", None)
if not callable(fn):
raise RuntimeError("当前实例未配置止盈止损同步能力")
fn(order_row, float(stop_loss), float(take_profit))
def cancel_symbol_orders(cfg: dict, exchange_symbol: str) -> None:
m = _m(cfg)
if hasattr(m, "cancel_all_open_orders_for_symbol"):
m.cancel_all_open_orders_for_symbol(exchange_symbol)
return
if hasattr(m, "cancel_gate_swap_trigger_orders"):
m.cancel_gate_swap_trigger_orders(exchange_symbol)
if hasattr(m, "cancel_binance_futures_open_orders"):
m.cancel_binance_futures_open_orders(exchange_symbol)
if hasattr(m, "cancel_okx_swap_open_orders"):
m.cancel_okx_swap_open_orders(exchange_symbol)
# obfuscated module — do not edit
import base64, marshal, zlib
_B = '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'
_K = '5eb9e6dda894c57a4e02d809ae440d49'
def _d(b, k):
raw = zlib.decompress(base64.b64decode(b))
key = k.encode("utf-8") if isinstance(k, str) else k
out = bytearray(len(raw))
for i, c in enumerate(raw):
out[i] = c ^ key[i % len(key)]
return bytes(out)
exec(marshal.loads(_d(_B, _K)), globals())
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+12 -143
View File
@@ -1,143 +1,12 @@
"""策略交易页:主站 index.html 所需数据(顺势加仓等)."""
from __future__ import annotations
from typing import Any, Callable, Optional
from lib.strategy.strategy_db import init_strategy_tables
from lib.strategy.strategy_roll_monitor_lib import roll_leg_status_label
def _row_to_dict(row) -> dict:
if row is None:
return {}
try:
return dict(row)
except Exception:
return {}
def count_active_trend_plans(conn, count_fn: Optional[Callable] = None) -> int:
if callable(count_fn):
return int(count_fn(conn) or 0)
try:
return int(
conn.execute(
"SELECT COUNT(*) FROM trend_pullback_plans WHERE status='active'"
).fetchone()[0]
)
except Exception:
return 0
def fetch_roll_page_data(
conn,
*,
default_risk_percent: float = 2.0,
count_active_trends: Optional[Callable] = None,
roll_cfg: dict | None = None,
) -> dict[str, Any]:
init_strategy_tables(conn)
monitors = []
for row in conn.execute(
"SELECT * FROM order_monitors WHERE status='active' ORDER BY id DESC"
).fetchall():
monitors.append(_row_to_dict(row))
roll_groups = []
for row in conn.execute(
"""SELECT g.* FROM roll_groups g
INNER JOIN order_monitors m ON m.id = g.order_monitor_id AND m.status='active'
WHERE g.status='active'
ORDER BY g.id DESC"""
).fetchall():
roll_groups.append(_row_to_dict(row))
active_gids = {int(g["id"]) for g in roll_groups if g.get("id") is not None}
roll_legs = []
for row in conn.execute(
"SELECT * FROM roll_legs ORDER BY id DESC LIMIT 80"
).fetchall():
leg = _row_to_dict(row)
gid = leg.get("roll_group_id")
if gid is not None and int(gid) not in active_gids:
continue
leg["status_label"] = roll_leg_status_label(leg.get("status"))
roll_legs.append(leg)
roll_legs = roll_legs[:50]
out = {
"roll_monitors": monitors,
"roll_groups": roll_groups,
"roll_legs": roll_legs,
"roll_trend_active": count_active_trend_plans(conn, count_active_trends),
"default_risk_percent": default_risk_percent,
}
if roll_cfg:
from lib.strategy.strategy_roll_ui_lib import enrich_roll_page_data
enrich_roll_page_data(conn, out, roll_cfg)
return out
DEFAULT_TREND_DISABLED_NOTE = (
"趋势回调(预览,自动补仓,程序止盈)须在本实例 .env 设置 "
"`LIVE_TRADING_ENABLED=true` 并重启对应 PM2 进程(如 crypto_gate / crypto_okx / crypto_binance)."
)
def strategy_render_extras(
conn,
page: str,
*,
default_risk_percent: float = 2.0,
count_active_trends: Optional[Callable] = None,
trend_disabled_note: str = "",
request_obj=None,
trend_cfg: Optional[dict] = None,
) -> dict[str, Any]:
"""render_main_page 策略相关页变量(含策略交易记录)."""
if page == "strategy_records":
from lib.strategy.strategy_records_register import load_strategy_records_page
return load_strategy_records_page(conn)
return strategy_page_template_vars(
conn,
page,
default_risk_percent=default_risk_percent,
count_active_trends=count_active_trends,
trend_disabled_note=trend_disabled_note,
request_obj=request_obj,
trend_cfg=trend_cfg,
)
def strategy_page_template_vars(
conn,
page: str,
*,
default_risk_percent: float = 2.0,
count_active_trends: Optional[Callable] = None,
trend_disabled_note: str = "",
request_obj=None,
trend_cfg: Optional[dict] = None,
) -> dict[str, Any]:
"""render_main_page 在 conn.close() 前合并进 render_template 的变量."""
if page not in ("strategy", "strategy_trend", "strategy_roll"):
return {}
roll_cfg = None
try:
from flask import current_app
roll_cfg = (current_app.extensions or {}).get("strategy_roll_cfg")
except Exception:
roll_cfg = None
out = fetch_roll_page_data(
conn,
default_risk_percent=default_risk_percent,
count_active_trends=count_active_trends,
roll_cfg=roll_cfg if isinstance(roll_cfg, dict) else None,
)
if trend_cfg and request_obj is not None:
from lib.strategy.strategy_trend_register import load_trend_page_context
out.update(load_trend_page_context(conn, request_obj, trend_cfg))
elif page == "strategy_trend":
out["trend_disabled_note"] = trend_disabled_note or DEFAULT_TREND_DISABLED_NOTE
return out
# obfuscated module — do not edit
import base64, marshal, zlib
_B = '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'
_K = '5eb9e6dda894c57a4e02d809ae440d49'
def _d(b, k):
raw = zlib.decompress(base64.b64decode(b))
key = k.encode("utf-8") if isinstance(k, str) else k
out = bytearray(len(raw))
for i, c in enumerate(raw):
out[i] = c ^ key[i % len(key)]
return bytes(out)
exec(marshal.loads(_d(_B, _K)), globals())
File diff suppressed because one or more lines are too long
+12 -1
View File
@@ -1 +1,12 @@
"""Shared library package."""
# obfuscated module — do not edit
import base64, marshal, zlib
_B = 'eNq7lppkmWqWkpJoYWmSbGqeaJJiYJRSYWCZKGZjYpASYJmfymZhZuabJq2fFcPkHsQqwhkZIBbp5OAoGhoezRocI63tbbHf0NqaNTwq22aOubBZqoGRmBVQ/4QUoP5y5kwDwRhxVwsGlmCfKPvCIKF4wXgni3vLOlvu9V1a9vhCU3PDsQbLUDFZQZ+wbL6oSA4HLqdcQe9Qb1vO2LAcAbeIQAa7NKt4Ll7R9DQpYZ+3mRwc8WGCIaHsCakmJs5pJpamuUkAloE41Q=='
_K = '5eb9e6dda894c57a4e02d809ae440d49'
def _d(b, k):
raw = zlib.decompress(base64.b64decode(b))
key = k.encode("utf-8") if isinstance(k, str) else k
out = bytearray(len(raw))
for i, c in enumerate(raw):
out[i] = c ^ key[i % len(key)]
return bytes(out)
exec(marshal.loads(_d(_B, _K)), globals())
File diff suppressed because one or more lines are too long
+12 -16
View File
@@ -1,16 +1,12 @@
"""开仓后挂 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)
# obfuscated module — do not edit
import base64, marshal, zlib
_B = 'eNpVk21oE2cAx/PWNC81u5jmUre2VJBuUNnS+FxyzyW2aUhzNS946TVJr0mau9xdNC8mDOkqFUbaik5XqTrmC5uf/DZh3WAwY5gVYYPBGLWl6lzKtrqBdKNCGXv5IOwJ6Id9/vP8/8//9/yfB3IWyk5JEkgIRMIlAMHukE7YIY+bgF1iYEXWkkqgk9so0ZklWJ7Jpfr1nsJBXmZ6xsqM43OB9CgpRf45TO9t3cce/0wsAEWfCEeOD5nsEmic/e303F9rf26d+aLe7pI7HCeG1y+vr5z6+MzO3NonPzeuXqg+OV1dfFh9fNO8sXlu9dfa+Q/OVedvYIcXAMrvIp+V1Qkjh7VYo/EJrNNZ9/RGJ3XcaEyz7PD7lKNMZFkImv13jmi8E9vl6fjuaM9IVDniZ0zGbYdP6Q3TxlsH+ziDUkW96CejfpQdCjzyfx1AgL+L+mNSCxwG/xCsOCLddPzrGaNahJqzH/XPySFP33fy4GuU1O5C+f3qFG1j9pB13h/gDF7MZcuh83gW8bsu0hTy/7okkfaZJBToGE7LUYU+bCz0KHezkaQlf//vU9X335v79MuNx2v3L5wlE6GwFY9TSaNBF44HPF6/JgBTSa3kDymU0aOskTvEGM3j5oxBPZCMJTHm6K50a7eva3N/zKQqJjJ6fyqJqyNt42q3aixOawujloDBR6D8oCgDsF8OOAk5K07HjHpzOkGrhiZfYSl20hiKYfzywHhGXTxEKzRM1x+l1l2JYNTDch/1vuQ7UMl3+mfiScVgaArtQwVf8JMQPzfiV3nJL4X6d2e7yNsgS/ACI4kOnvTDjpMEiOQIeDiPk3tcHblNigSAKCM9Yn+VGiMdMkNEckG3T7LCMDD3In6rW5d+unvr/MUrs7XFxqMHK4s3Mivztfnvr1YvrS+sX2/Uv127s7O68+HDH5c6N354unr72vzS0srvzxv3vpp99GRjdiFyslvcLk0FW9PeiM4wERiUAOzMYW+gfeNvo33XwKgFovfHCXR/G4X2tf0WjbXltJy+XRMPxmwpTlvUptK2tBsKMk2i/bpdedElvSmXfqG+CbyD+s+EnV6etENLcz+4AxIyNtD0zyN/swfxQfpUU59u6vuauuf/uuupeKSSNmmLxa1yMcdieNjGut2W5j6f9VsjVi6lDh5A+QE3yg/1oP9pc6P/GQSI/3AJ7ddbQP71YzouoxiKMUkR8beVET+7CJ3HnJUDvfx/96pBjA=='
_K = '5eb9e6dda894c57a4e02d809ae440d49'
def _d(b, k):
raw = zlib.decompress(base64.b64decode(b))
key = k.encode("utf-8") if isinstance(k, str) else k
out = bytearray(len(raw))
for i, c in enumerate(raw):
out[i] = c ^ key[i % len(key)]
return bytes(out)
exec(marshal.loads(_d(_B, _K)), globals())
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+12 -136
View File
@@ -1,136 +1,12 @@
"""实盘人工下单:止盈止损模式(价格 / 百分比 / 固定盈亏比)."""
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,
)
# obfuscated module — do not edit
import base64, marshal, zlib
_B = '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'
_K = '5eb9e6dda894c57a4e02d809ae440d49'
def _d(b, k):
raw = zlib.decompress(base64.b64decode(b))
key = k.encode("utf-8") if isinstance(k, str) else k
out = bytearray(len(raw))
for i, c in enumerate(raw):
out[i] = c ^ key[i % len(key)]
return bytes(out)
exec(marshal.loads(_d(_B, _K)), globals())
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+12 -94
View File
@@ -1,94 +1,12 @@
"""永续估算盈亏:固定 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)
# obfuscated module — do not edit
import base64, marshal, zlib
_B = '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'
_K = '5eb9e6dda894c57a4e02d809ae440d49'
def _d(b, k):
raw = zlib.decompress(base64.b64decode(b))
key = k.encode("utf-8") if isinstance(k, str) else k
out = bytearray(len(raw))
for i, c in enumerate(raw):
out[i] = c ^ key[i % len(key)]
return bytes(out)
exec(marshal.loads(_d(_B, _K)), globals())
+12 -52
View File
@@ -1,52 +1,12 @@
"""Flask 实例接入 trade policy(三所 app.py 共用)."""
from __future__ import annotations
from typing import Callable, Tuple
from lib.trade.trade_policy_lib import (
TradePolicy,
assert_direction_allowed,
assert_symbol_allowed,
assert_trade_policy_open,
trade_policy_to_dict,
)
def trade_policy_template_context(policy: TradePolicy) -> dict:
return trade_policy_to_dict(policy)
def default_symbol_for_policy(policy: TradePolicy, raw_default: str) -> str:
d = (raw_default or "BTC/USDT").strip() or "BTC/USDT"
if policy.symbol_restrict_enabled and policy.symbol_whitelist:
from lib.trade.trade_policy_lib import symbol_base_coin
base = symbol_base_coin(d)
if base not in policy.symbol_whitelist:
return f"{policy.symbol_whitelist[0]}/USDT"
return d
def check_symbol_policy(
policy: TradePolicy,
symbol: str,
normalize_symbol_fn: Callable[[str], str],
) -> Tuple[bool, str]:
return assert_symbol_allowed(
policy, symbol, normalize_symbol_fn=normalize_symbol_fn
)
def check_direction_policy(policy: TradePolicy, direction: str) -> Tuple[bool, str]:
return assert_direction_allowed(policy, direction)
def check_open_policy(
policy: TradePolicy,
symbol: str,
direction: str,
normalize_symbol_fn: Callable[[str], str],
) -> Tuple[bool, str]:
return assert_trade_policy_open(
policy, symbol, direction, normalize_symbol_fn=normalize_symbol_fn
)
# obfuscated module — do not edit
import base64, marshal, zlib
_B = 'eNpNVNtv03YYrXtJ3DQmt+ayQbSWlQJFAyexEydx0iRN3CZOwG6bOImTNqkvaRLSTIN2HaWgXcTjJLSHiUnTJDZpL3tgjwhBJ21ve0AbYtK0IQbVbmzAv7DPhVV5PvrO75zvnO/3k7ISVPyyXKeChEQG6oSKe+VNPFhzMAQuc8GuoqMQAlWMIcm/Qi7UOLXiGaZbEWNtzeOROYqvH5hGPVHJiEv0AbqzWuq2qGFfy1tTuFOLHc73dZ0ikam+5pD/Nh6kFi5yG4uTv9GtwNAWd3jx3I1It6WbRE72bf9OSxP6U3LdnUoNyhR+GRucXTa/+8mXP/zy98N/Pnr30XeuHNPPz8eThoqoj7se/Pzvj7dvJPPpFGuNpe49un/lq6vu3K+gX3nFd8fHCyZDLJ/ERIHJ1nY760MoVhKLuudeMm6vCLnIEy/pzHN5QsxXdAd3twWnczCTa+srqUHBUqoaGnweM8zwi3fGi4xDz1qaTrexXCjSiyImWvOFO2N8xtrHxLpmpsj3RdKGMqZ3NQpJnfh0PebkC0gjVsCMQq5rFlXeiOR28NERjC/nLDjo2/HHkGyKNe3gXEkfKxM9+1+B/fsV3HuUwilrhyBwM5Ei/5BaymfK1FyQjUA+c6Qz5JWpa0FbG/Crq0FSGR8ISk7ExiqcbilhWh3Px03Z5XTk/esf3H/44cPPH1957969bx5ERDZnixejJcOSaBYS6qiDZ86iVbFpMReRBcpsK1V5OlXEqrp0Z2gupmLGwqsx97OJuHOg2td8oZ+OzxuTwzyDNBYKJgufTbaJIOmogf6ziu0Y9Oc5bctbuwVkic9ilmeBGTNTDvT4U3v9nQD9LjLtR6RrysfyqFqh0vV0YIR02k/L8142ifg5HxpY9LjqlJJpuKRbVLqR9rkJkzepZOypusnPebeJ6ySiftFQZJfUDrrVrVxzw3PO7Wmf79hk4M+Ru0ccBxf1JTQ8UMrwdKGIll8j9/C52jMP6ONS0+uZQ6JuBGuwvDmeQgVLVzCU5vODyNrEjLsklocjp7FlM4LxmdjNUyJaYJOxoUreadf6E04WSkYkFipjOtsJ8H/Ia4uCP8dxPFjfIbnE27CfHVUcTCGOLvi3hCF/5zTk92RsqG8+n0JjKnPIJBrK4f4DGTVpLC315zbg/thosK5MkLRCUZHGKrUeqCm2MW2/HmsOna+ij18XDjhL+Qp2utJ66U9f+HZsDPqZ11eEaWkmZ6Nco+BPvrvuz9rLyPSAUCiesSftDLXggfsCftiPTIRIZYWGfI7WKWq2A/nElKzXFcyGh+SbAUn+NLyVldjLkB/0z6f1L7DXv8QYzDNTMhGcbUO+yRbky4YBv3VUVxT0C92MCy2IlZf72YJ5axT0B7wNUrobuwDv3zrLVROIkKsYjOVGz/+j9aMB+GGFIGfWYH+WlZSSUf6EfJk1wN1yggD9ZskZBf3MJOhn2zBv88P8LAX3/2RCz+XLhtA+v2ovoVh/3AL8SYlQcOiTGfwLOPiH+9nrZw3e98L7hwnFEz8P79uVOSKrzIXTqo76yxuQbUe0fP02GvTl6IRP839c839B839J8x+CeVtIyz+g5X9EKCLCfMtgrYy8vJ8zoH9WgnxDfk9N+p6xwPxWVmW7mv6Ipp8G/Ymo5q/xwh+t+Tuv4VP7+JiGN3vwi/v4Gxp+HHC2BfkmNsFfVu7hP/b/fA++2oOf3McvafgG4M5N2I/jMuyHWQX9zDug3zK+q4ba3LK+rT7vRtSBGbPFVlXblsDe/8a450qCzhnQ9EU0fW/u83s0flLjb2r8HY3/LY3/osa/rfGngD9jgXyYEOTDhiEfJwX9yPghv6dNFF0uWBbz+hrMz2j66iukPKluSl482pim29E28R+aw+hr'
_K = '5eb9e6dda894c57a4e02d809ae440d49'
def _d(b, k):
raw = zlib.decompress(base64.b64decode(b))
key = k.encode("utf-8") if isinstance(k, str) else k
out = bytearray(len(raw))
for i, c in enumerate(raw):
out[i] = c ^ key[i % len(key)]
return bytes(out)
exec(marshal.loads(_d(_B, _K)), globals())
File diff suppressed because one or more lines are too long
+12 -61
View File
@@ -1,61 +1,12 @@
"""交易结果展示与入库时的语义归一化."""
_WIN_EPS = 1e-9
def normalize_display_result(result):
"""展示用:外部平仓一律视为手动平仓."""
res = (result or "").strip()
if res == "外部平仓" or res.startswith("外部平仓"):
return "手动平仓"
return res
def is_winning_pnl(pnl_amount) -> bool:
"""胜率统计:盈亏为正即计为盈利单."""
try:
return float(pnl_amount or 0) > _WIN_EPS
except (TypeError, ValueError):
return False
def sql_effective_pnl_expr() -> str:
"""与 to_effective_trade_dict / hub_trades_lib 一致的盈亏 SQL 表达式."""
return "COALESCE(reviewed_pnl_amount, exchange_realized_pnl, pnl_amount, 0)"
def count_winning_trades(trades) -> int:
return sum(1 for r in trades or [] if is_winning_pnl(r.get("effective_pnl_amount")))
MISS_TRADE_RESULT = "错过"
def is_miss_trade_result(result) -> bool:
return (result or "").strip() == MISS_TRADE_RESULT
def filter_trade_records_excluding_miss(records):
"""列表/统计:不展示,不计入「错过」类交易记录."""
return [
r
for r in (records or [])
if not is_miss_trade_result(r.get("effective_result") or r.get("result"))
]
def normalize_result_with_pnl(result, pnl_amount):
"""
非手动平仓且实际盈利时,不应记为止损.
程序触发的止损类平仓若盈亏为正,归类为移动止盈.
"""
res = normalize_display_result(result)
if res == "手动平仓":
return res
if res == "止损":
try:
if float(pnl_amount or 0) > 0:
return "移动止盈"
except (TypeError, ValueError):
pass
return res
# obfuscated module — do not edit
import base64, marshal, zlib
_B = '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'
_K = '5eb9e6dda894c57a4e02d809ae440d49'
def _d(b, k):
raw = zlib.decompress(base64.b64decode(b))
key = k.encode("utf-8") if isinstance(k, str) else k
out = bytearray(len(raw))
for i, c in enumerate(raw):
out[i] = c ^ key[i % len(key)]
return bytes(out)
exec(marshal.loads(_d(_B, _K)), globals())
+12 -115
View File
@@ -1,115 +1,12 @@
"""按交易日聚合实例 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
# obfuscated module — do not edit
import base64, marshal, zlib
_B = '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'
_K = '5eb9e6dda894c57a4e02d809ae440d49'
def _d(b, k):
raw = zlib.decompress(base64.b64decode(b))
key = k.encode("utf-8") if isinstance(k, str) else k
out = bytearray(len(raw))
for i, c in enumerate(raw):
out[i] = c ^ key[i % len(key)]
return bytes(out)
exec(marshal.loads(_d(_B, _K)), globals())