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:
@@ -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
+12
-373
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
@@ -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())
|
||||
|
||||
@@ -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
@@ -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
@@ -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())
|
||||
|
||||
@@ -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
@@ -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",
|
||||
]
|
||||
@@ -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 模板用 Jinja;FastAPI 路径用占位符版本
|
||||
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)
|
||||
@@ -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")
|
||||
@@ -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:
|
||||
"""稳定设备指纹,长度 16–64,供 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")
|
||||
@@ -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>
|
||||
@@ -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
@@ -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())
|
||||
|
||||
@@ -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
@@ -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
@@ -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
+12
-1283
File diff suppressed because one or more lines are too long
@@ -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
@@ -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
@@ -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())
|
||||
|
||||
+12
-232
File diff suppressed because one or more lines are too long
+12
-164
File diff suppressed because one or more lines are too long
@@ -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())
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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())
|
||||
|
||||
+12
-845
File diff suppressed because one or more lines are too long
@@ -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
+12
-509
File diff suppressed because one or more lines are too long
+12
-320
File diff suppressed because one or more lines are too long
+12
-136
@@ -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
+12
-150
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+12
-94
@@ -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())
|
||||
|
||||
@@ -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())
|
||||
|
||||
+12
-205
File diff suppressed because one or more lines are too long
@@ -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())
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user