Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f53f2814ab | |||
| e5051fb309 | |||
| 1dc7914701 | |||
| 58e2bf3e8b | |||
| 19debee581 | |||
| 1755f67eca | |||
| 6886de0bad | |||
| f04a91efe6 | |||
| b43e33e24f | |||
| b5a061e758 | |||
| 4ef3b40353 | |||
| 4a79e010c4 | |||
| 791cc750da | |||
| c8231ea194 | |||
| aaccdcfc16 | |||
| f993a89a21 | |||
| 9dc363270e | |||
| 9a83dfe209 | |||
| 32c42b8447 | |||
| a2075ba73e | |||
| 846f3de525 | |||
| a7b75895e6 | |||
| d870178b83 | |||
| 7ebe1671b2 | |||
| cb4f6aaa4b | |||
| eb175820e9 | |||
| 890659f173 | |||
| ca499c6104 | |||
| 54f1857fa2 | |||
| 6f1ae14b3d | |||
| 29d59d6a53 |
@@ -3,5 +3,8 @@
|
||||
deploy/** text eol=lf
|
||||
# 文档统一 LF,避免 Windows 编辑后产生 CRLF 脏 diff
|
||||
docs/** text eol=lf
|
||||
# XMind 为 ZIP 二进制;须覆盖上面 docs/** 的 text/eol,否则入库会损坏打不开
|
||||
*.xmind -text -diff -merge -eol
|
||||
docs/**/*.xmind -text -diff -merge -eol
|
||||
# .env 模板统一 LF,避免 Linux PM2 source 报 $'\r': command not found
|
||||
**/.env.example text eol=lf
|
||||
|
||||
@@ -411,7 +411,7 @@ _APP_STARTED_AT = time.time()
|
||||
_RECONCILE_FLAT_STREAK = {}
|
||||
KLINE_TIMEFRAME = os.getenv("KLINE_TIMEFRAME", "5m")
|
||||
FULL_MARGIN_BUFFER_RATIO = float(os.getenv("FULL_MARGIN_BUFFER_RATIO", "0.98"))
|
||||
TRANSFER_CCY = os.getenv("TRANSFER_CCY", "USDT")
|
||||
TRANSFER_CCY = (os.getenv("TRANSFER_CCY", "USDT") or "USDT").strip().upper() or "USDT"
|
||||
UPLOAD_FOLDER = resolve_path(os.getenv("UPLOAD_DIR", "static/images"))
|
||||
ORDER_CHART_ENABLED = os.getenv("ORDER_CHART_ENABLED", "true").lower() == "true"
|
||||
ORDER_CHART_TFS = [x.strip() for x in (os.getenv("ORDER_CHART_TFS", "4h,1h,15m,5m") or "").split(",") if x.strip()]
|
||||
@@ -9870,7 +9870,7 @@ def manual_transfer():
|
||||
amount = float(request.form.get("amount", "0"))
|
||||
except Exception:
|
||||
flash("划转金额格式错误")
|
||||
return redirect("/settings")
|
||||
return redirect("/settings?settings_tab=transfer")
|
||||
from_account = (request.form.get("from_account") or AUTO_TRANSFER_FROM).strip()
|
||||
to_account = (request.form.get("to_account") or AUTO_TRANSFER_TO).strip()
|
||||
ok, msg, _ = execute_transfer_usdt(amount, from_account, to_account)
|
||||
@@ -9885,7 +9885,7 @@ def manual_transfer():
|
||||
flash(f"手动划转成功:{amount}U {from_account}->{to_account}")
|
||||
else:
|
||||
flash(f"手动划转失败:{msg}")
|
||||
return redirect("/settings")
|
||||
return redirect("/settings?settings_tab=transfer")
|
||||
|
||||
|
||||
def _journal_ai_chart_builder(row):
|
||||
|
||||
@@ -404,7 +404,7 @@ KLINE_TIMEFRAME = os.getenv("KLINE_TIMEFRAME", "5m")
|
||||
_APP_STARTED_AT = time.time()
|
||||
_RECONCILE_FLAT_STREAK = {}
|
||||
FULL_MARGIN_BUFFER_RATIO = float(os.getenv("FULL_MARGIN_BUFFER_RATIO", "0.98"))
|
||||
TRANSFER_CCY = os.getenv("TRANSFER_CCY", "USDT")
|
||||
TRANSFER_CCY = (os.getenv("TRANSFER_CCY", "USDT") or "USDT").strip().upper() or "USDT"
|
||||
UPLOAD_FOLDER = resolve_path(os.getenv("UPLOAD_DIR", "static/images"))
|
||||
ORDER_CHART_ENABLED = os.getenv("ORDER_CHART_ENABLED", "true").lower() == "true"
|
||||
ORDER_CHART_TFS = [x.strip() for x in (os.getenv("ORDER_CHART_TFS", "4h,1h,15m,5m") or "").split(",") if x.strip()]
|
||||
@@ -9727,7 +9727,7 @@ def manual_transfer():
|
||||
amount = float(request.form.get("amount", "0"))
|
||||
except Exception:
|
||||
flash("划转金额格式错误")
|
||||
return redirect("/settings")
|
||||
return redirect("/settings?settings_tab=transfer")
|
||||
from_account = (request.form.get("from_account") or AUTO_TRANSFER_FROM).strip()
|
||||
to_account = (request.form.get("to_account") or AUTO_TRANSFER_TO).strip()
|
||||
ok, msg, _ = execute_transfer_usdt(amount, from_account, to_account)
|
||||
@@ -9742,7 +9742,7 @@ def manual_transfer():
|
||||
flash(f"手动划转成功:{amount}U {from_account}->{to_account}")
|
||||
else:
|
||||
flash(f"手动划转失败:{msg}")
|
||||
return redirect("/settings")
|
||||
return redirect("/settings?settings_tab=transfer")
|
||||
|
||||
|
||||
def _journal_ai_chart_builder(row):
|
||||
|
||||
@@ -384,7 +384,7 @@ BREAKEVEN_EXCHANGE_MIN_INTERVAL_SEC = max(
|
||||
_BREAKEVEN_LAST_EX_SYNC: dict[int, float] = {}
|
||||
KLINE_TIMEFRAME = os.getenv("KLINE_TIMEFRAME", "5m")
|
||||
FULL_MARGIN_BUFFER_RATIO = float(os.getenv("FULL_MARGIN_BUFFER_RATIO", "0.98"))
|
||||
TRANSFER_CCY = os.getenv("TRANSFER_CCY", "USDT")
|
||||
TRANSFER_CCY = (os.getenv("TRANSFER_CCY", "USDT") or "USDT").strip().upper() or "USDT"
|
||||
OKX_POSITION_INST_TYPE = os.getenv("OKX_POSITION_INST_TYPE", "SWAP")
|
||||
EXCHANGE_POSITION_SYNC_FROM_BJ = (os.getenv("EXCHANGE_POSITION_SYNC_FROM_BJ") or "").strip()
|
||||
EXCHANGE_POSITION_HISTORY_LIMIT = max(50, min(1000, int(os.getenv("EXCHANGE_POSITION_HISTORY_LIMIT", "200"))))
|
||||
@@ -9455,7 +9455,7 @@ def manual_transfer():
|
||||
amount = float(request.form.get("amount", "0"))
|
||||
except Exception:
|
||||
flash("划转金额格式错误")
|
||||
return redirect("/settings")
|
||||
return redirect("/settings?settings_tab=transfer")
|
||||
from_account = (request.form.get("from_account") or AUTO_TRANSFER_FROM).strip()
|
||||
to_account = (request.form.get("to_account") or AUTO_TRANSFER_TO).strip()
|
||||
ok, msg, _ = execute_transfer_usdt(amount, from_account, to_account)
|
||||
@@ -9477,7 +9477,7 @@ def manual_transfer():
|
||||
flash(f"手动划转成功:{amount}U {from_account}->{to_account}")
|
||||
else:
|
||||
flash(f"手动划转失败:{msg}")
|
||||
return redirect("/settings")
|
||||
return redirect("/settings?settings_tab=transfer")
|
||||
|
||||
|
||||
def _journal_ai_chart_builder(row):
|
||||
|
||||
@@ -4,6 +4,15 @@
|
||||
|
||||
「内照明心」页(`/archive`)用于 **复盘语录 + 交易记录回顾 + 按需 K 线**.左侧维护每日复盘语录(最多 100 条);右侧按日期区间列出开仓记录,展示区间统计,并可展开 K 线图表对照单笔交易.
|
||||
|
||||
顶栏有 **永续 / 期权** 品种切换:
|
||||
|
||||
| 品种 | 数据 | 说明 |
|
||||
|------|------|------|
|
||||
| **永续** | 三所 `trade_records` → `archive_trade_cache` | 含犯病标签、K 线 |
|
||||
| **期权** | OKX `options_review_trades` → `archive_options_trade_cache` | 独立 Tab;同步进中控库后离线可看;默认排除对冲腿 |
|
||||
|
||||
同步:「同步」按钮与后台 4h 任务会同时拉永续与期权(仅 `capabilities` 含 `options` 的账户).
|
||||
|
||||
与行情区 `hub_kline.db`(15 天滚动缓存)**完全独立**:档案库只增不删,从建档起永久保留.
|
||||
|
||||
## 页面布局
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
|
||||
| 文档 | 实例 | 状态 |
|
||||
|------|------|------|
|
||||
| [交易执行手册-期权与Gate.md](../交易执行手册-期权与Gate.md) | 中控「策略说明」·执行手册 | 个人开单纪律 |
|
||||
| [交易执行手册-v2-期权与合约.md](../交易执行手册-v2-期权与合约.md) | 中控「策略说明」·执行手册v2 | **现行**:无对冲;1H→空间→结构→定损盈→期权/合约 |
|
||||
| [交易执行手册-期权与Gate.md](../交易执行手册-期权与Gate.md) | 中控「策略说明」·执行手册v1 | 含对冲;历史对照 |
|
||||
| [交易行为准则-开单三检.md](../交易行为准则-开单三检.md) | 中控「策略说明」·行为准则 | 开单前信号/流程/情绪三检 |
|
||||
| [binance-alt-trend-long.md](./binance-alt-trend-long.md) | 币安山寨·多头趋势 | v0.4 讨论稿 |
|
||||
| [okx-trend-both.md](./okx-trend-both.md) | OKX·多空趋势 | v0.4 讨论稿 |
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"items": [
|
||||
"最核心、最明确的一个点位/结构确认已写清",
|
||||
"该确认本身足够清晰(不是靠一长串宏大叙事)",
|
||||
"已过方向 → 空间 → 值不值得(不够格则空仓)"
|
||||
"已过主链条:1H方向 → 空间 → 结构 → 定损盈 → 选工具(期权/合约,无对冲);不够格则空仓"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"exchange": "playbook_v2",
|
||||
"title": "执行手册 v2 开仓清单(无对冲)",
|
||||
"version": "v0.1",
|
||||
"groups": [
|
||||
{
|
||||
"title": "主链条",
|
||||
"items": [
|
||||
"1H 方向清楚(含明显 N 字);跟的是 1H 波段",
|
||||
"空间足够(支撑/阻力;至少约 ≥2%)",
|
||||
"结构已出现且量级够(约 8h+ / 48 根 15m)",
|
||||
"止损按模型:结构突破=外沿;假突破=针尖;止盈与 RR 已接受",
|
||||
"工具只在「期权 / 合约」中选择;未开对冲"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "账户与仓位",
|
||||
"items": [
|
||||
"只动 OKX 期权或 Gate 合约;其它账户零操作",
|
||||
"期权:约 10U、一次一仓;合约:止损约 5U、本位置次数未超两次",
|
||||
"合计最坏风险可接受(约 ≤20U 量级)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "离场与心态",
|
||||
"items": [
|
||||
"期权离场只认规则止盈或到期;开仓后中间不手平",
|
||||
"不是「今天也要开点期权」;过检才开,不过则空仓",
|
||||
"已过开单三检(信号 / 流程 / 情绪)"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
# 交易执行手册 v2(期权 / 合约 · 无对冲)
|
||||
|
||||
> 个人开单纪律第二版(2026-07-24 起)。
|
||||
> **相对 v1:去掉期期对冲 / 偏置对冲;工具只留期权与合约。**
|
||||
> 目标:少而精、珍惜机会、样本干净;**不保证收益**。
|
||||
> 旧版(含对冲)见 [交易执行手册-期权与Gate.md](./交易执行手册-期权与Gate.md)。
|
||||
> **开单前先过** [交易行为准则-开单三检.md](./交易行为准则-开单三检.md);本手册管怎么做单。
|
||||
|
||||
---
|
||||
|
||||
## 1. 主链条(强制)
|
||||
|
||||
```
|
||||
1H 方向 → 空间 → 结构 → 定损盈 → 选工具(期权 / 合约)
|
||||
```
|
||||
|
||||
任一步不过 → **空仓等待**,不为开单找理由。
|
||||
|
||||
| 步骤 | 做什么 | 否决 |
|
||||
|------|--------|------|
|
||||
| **1H 方向** | 趋势周期以 **1H** 为准;1H 上要有明显 **N 字**。跟 1H 波段,不跟 4H 打架硬做。例:4H 多、1H 空 → 做 1H 空头波段 | 1H 方向不清、无 N 字 |
|
||||
| **空间** | 做空看下方支撑,做多看上方阻力;至少约 **≥2%** 才值得谈(常期望更大空间,如 ~5%) | 空间不够、贴着墙 |
|
||||
| **结构** | 方向与空间过关后,在 **15m / 5m** 等结构;结构量级至少约 **8h+**(约 **48 根 15m**)。形态:收敛 / 两段式回调 / 箱体 / 假突破等 | 结构未出现、磨不够就抢跑 |
|
||||
| **定损盈** | 结构出现后定义止损、止盈,算盈亏比。结构突破 → 止损在 **结构外沿**;假突破 → 止损在 **假突破针尖** | 损盈说不清、RR 不接受 |
|
||||
| **选工具** | 只在上四步都齐之后选:**期权** 或 **合约**。波段有足够时间考虑,不急着下手 | 用对冲、或「每天都要开点期权」 |
|
||||
|
||||
**丢掉对冲。** 对冲易带来「有保护就能多做」的幻觉;本版不做期期对冲、不做偏置对冲壳。
|
||||
|
||||
---
|
||||
|
||||
## 2. 总原则
|
||||
|
||||
1. **工具只有期权与合约**;同一时段尽量只让一边「说话」。
|
||||
2. **看不懂不做**;过滤比频率重要。日更不是目标,过检才是。
|
||||
3. 动手前先过 **开单三检**(信号 → 流程 → 情绪);不过 → 空仓。
|
||||
4. 玩法必须走完主链条;不够格 → 空仓。
|
||||
5. 期权离场只认:**系统/规则止盈** 与 **到期**;**开仓后中间不手动平仓**(紧急例外不进策略样本)。
|
||||
6. 过程可控、结果随缘:用规则管仓位与次数,不追求每天打满。
|
||||
|
||||
---
|
||||
|
||||
## 3. 账户与分工
|
||||
|
||||
| 账户 | 角色 | 说明 |
|
||||
|------|------|------|
|
||||
| OKX 期权 | **主业之一** | 方向单(虚值等);**不做对冲腿** |
|
||||
| Gate 合约 | **主业之一** | 结构清楚时的波段;与期权尽量错开 |
|
||||
| 其它 | 暂不做 | 减少分心与样本污染 |
|
||||
|
||||
**到期选择(期权)**
|
||||
|
||||
- 方向单默认 **一天期**。
|
||||
- 尽量在 **北京时间下午 4 点后** 开 **次日到期**,覆盖较完整的美盘 + 亚盘 + 欧盘窗口。
|
||||
- 更长故事优先考虑合约,不强行拉长期权。
|
||||
|
||||
---
|
||||
|
||||
## 4. 入场逻辑(两类工具)
|
||||
|
||||
开仓前先判断:当前是 **买方向的期权表达**,还是 **合约波段**。
|
||||
|
||||
### 4.1 方向明确 · 结构到位 → 期权
|
||||
|
||||
- **条件**:主链条全部过关;常用结构突破或假突破模型在 15m/5m 成立。
|
||||
- **工具**:**一天期期权方向单**(空间够时优先考虑 **虚值**:同止损口径下盈亏比往往更高)。
|
||||
- **离场**:规则止盈或到期;不手平。
|
||||
- **默认**:先只开期权,不上合约。
|
||||
|
||||
### 4.2 结构到位 · 更适合合约 → 合约
|
||||
|
||||
- **条件**:主链条过关;位置极明确;同一位置机会计数见 Gate 纪律。
|
||||
- **工具**:Gate 合约波段;止损挂在模型对应位置(外沿 / 针尖)。
|
||||
- **独立假突破**(没有先开突破期权时):优先 **只做合约** 或 **空仓**,勿与「突破期权后再加仓」混用同一套仓。
|
||||
|
||||
### 4.3 明确不做
|
||||
|
||||
- 横盘「买波动」的 **期期对冲**(Call+Put)。
|
||||
- 任何「对冲壳 + 偏置」伪装成单边。
|
||||
- 为了「今天也开点期权」而破主链条。
|
||||
|
||||
---
|
||||
|
||||
## 5. 仓位与风险预算
|
||||
|
||||
**总资金参考:约 800U。**
|
||||
|
||||
| 项目 | 规则 |
|
||||
|------|------|
|
||||
| 单笔期权 | 约 **10U** 权利金预算;**一次只持有一个期权仓位** |
|
||||
| Gate 合约 | 日内保证金约 **50U**、约 **10 倍**;有单才用,无单为 0 |
|
||||
| 合约止损 | 一般约 **5U**;单笔最大亏损不超过约 **10U** |
|
||||
| 日损失心理框 | 期权+合约若都错:合计大约 **≤20U**;都对时期望可到 **40U+**(理想情形,非每日目标) |
|
||||
|
||||
相对 800U:单笔约 **1.25%** 量级;全错一天约 **2.5%** 量级——防守优先。
|
||||
|
||||
**叠加红线**
|
||||
|
||||
- 期权一仓 + 合约同日存在时,按合计风险接受最坏约 20U,且尽量少「同向双开」。
|
||||
- 不为「好像有保护」放大仓位(本版已无对冲保护叙事)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 合约日纪律(Gate)
|
||||
|
||||
1. 只做 **很明确的位置**;不明确基本不做。
|
||||
2. 动手前想清:**如何进场**(假突破 / 结构突破)。
|
||||
3. **同一位置最多两次机会**:结构突破、假突破。
|
||||
4. **两次都错 → 当日不再做单**(即使后面更「看起来清楚」也留到明天)。
|
||||
5. 止损约 **5U**;波段规则开仓前想清。
|
||||
6. 离场以结构止盈/止损为准。
|
||||
|
||||
---
|
||||
|
||||
## 7. 期权日纪律(OKX)
|
||||
|
||||
1. **不手动平仓**;只等规则止盈或到期(紧急手平标记为非策略样本)。
|
||||
2. 一次一仓;约 10U 权利金。
|
||||
3. **不做对冲**;不做「每天默认开期权」。
|
||||
4. 结构突破 / 假突破用期权表达时,损位跟模型:外沿 / 针尖。
|
||||
5. 默认一天期;优先完整会话窗口再开。
|
||||
|
||||
---
|
||||
|
||||
## 8. 开仓前自检清单
|
||||
|
||||
- [ ] 今天是否只动「期权 / 合约」,其它账户零操作?是否 **未开对冲**?
|
||||
- [ ] **1H 方向**是否清楚(含 N 字)?
|
||||
- [ ] **空间**是否足够(支撑/阻力,至少约 ≥2%)?
|
||||
- [ ] **结构**是否出现且量级够(约 8h+ / 48×15m)?
|
||||
- [ ] **止损 / 止盈**是否按模型定好(外沿或针尖)?RR 是否接受?
|
||||
- [ ] **工具**选的是期权还是合约?理由是否写清?
|
||||
- [ ] 期权:止盈条件与「接受到期」是否写清?
|
||||
- [ ] 合约:本位置第几次机会?止损约 5U 设好了吗?今日两次是否已用完?
|
||||
|
||||
---
|
||||
|
||||
## 9. 一句话版本
|
||||
|
||||
> **1H 定方向 → 量空间 → 等够级别的结构 → 按模型定损盈 → 只在期权与合约里选工具;不对冲;期权不手平;一位置两次,错完收工;珍惜机会,日更不是目标。**
|
||||
|
||||
---
|
||||
|
||||
## 10. 修订记录
|
||||
|
||||
| 日期 | 说明 |
|
||||
|------|------|
|
||||
| 2026-07-24 | v2 初版:去掉对冲;主链条 1H→空间→结构→定损盈→期权/合约;吸收假突破针尖 / 结构外沿止损口径 |
|
||||
@@ -1,10 +1,10 @@
|
||||
# 交易执行手册(期权为主 · Gate 为辅)
|
||||
# 交易执行手册 v1(期权为主 · Gate 为辅 · 含对冲)
|
||||
|
||||
> 个人开单纪律与仓位规则(2026-07 起)。
|
||||
> 个人开单纪律与仓位规则(2026-07 起)。**本版保留对冲,仅作历史/对照。**
|
||||
> **现行主版本请用** [交易执行手册-v2-期权与合约.md](./交易执行手册-v2-期权与合约.md)(无对冲:1H→空间→结构→定损盈→期权/合约)。
|
||||
> 目标:少而精、可控回撤、样本干净;**不保证收益**。
|
||||
> 工具:OKX 期权(主)+ Gate 合约(辅);其它账户暂不做。
|
||||
> **开单前先过** [交易行为准则-开单三检.md](./交易行为准则-开单三检.md)(信号 / 流程 / 情绪);本手册管怎么做单。
|
||||
|
||||
---
|
||||
|
||||
## 1. 总原则
|
||||
@@ -146,3 +146,4 @@
|
||||
|------|------|
|
||||
| 2026-07-21 | 初版:根据实盘讨论整理(期权为主、Gate 为辅、仓位与日停手规则) |
|
||||
| 2026-07-23 | 挂钩开单三检行为准则 |
|
||||
| 2026-07-24 | 标注为 v1(含对冲);现行纪律迁至执行手册 v2 |
|
||||
|
||||
Binary file not shown.
+2
-1
@@ -49,7 +49,7 @@
|
||||
| 点位 / 结构本身已经够清楚 | 「好像有戏」但确认点模糊 |
|
||||
| 只描述事实与系统条件 | 堆细节证明自己分析很厉害 |
|
||||
|
||||
对照执行手册时:先过 **方向 → 空间 → 值不值得**;不够格 → 空仓(见手册 §1、§3)。
|
||||
对照执行手册时:先过 **1H 方向 → 空间 → 结构 → 定损盈 → 选工具(期权/合约)**;不够格 → 空仓(见手册 v2)。
|
||||
|
||||
### 4.2 流程确认(Process Confirmation)
|
||||
|
||||
@@ -112,3 +112,4 @@
|
||||
| 日期 | 说明 |
|
||||
|------|------|
|
||||
| 2026-07-23 | 初级版:三检 + 总循环 + 红线;对齐 AI 复盘与本人总结 |
|
||||
| 2026-07-24 | 信号检对齐执行手册 v2 主链条(1H→空间→结构→定损盈→期权/合约) |
|
||||
|
||||
+4
-1
@@ -6,6 +6,9 @@
|
||||
|
||||
| 标签 | 指向提交 | 说明 |
|
||||
|------|----------|------|
|
||||
| `snapshot/20260726-2` | `4a79e01` | 2026-07-26 午:执行手册脑图(业务主题)、`.xmind` 按二进制入库、去掉缩略图避免 Gitea raw 换行损坏 |
|
||||
| `snapshot/20260726` | `a2075ba` | 2026-07-26:Gate划转币种大写修复、系统设置划转页签停留、自动划转账户/币种下拉默认、期权「按可用余额打满」=min(余额,单笔预算)及说明 |
|
||||
| `snapshot/20260724` | `890659f` | 2026-07-24:执行手册v2(无对冲)、监控/策略页签显隐、内照明心期权档案同步、期权开平仓微信必发、实例导航显隐关键位/实盘下单等 |
|
||||
| `snapshot/20260723-2` | `9e0591c` | 2026-07-23:策略对比页(合约/单期权/期期7:3)、监控与看板隐藏浮盈偏好、对比页卡片内边距等 |
|
||||
| `snapshot/20260723-pre-amp-stats` | `40be3a5` | 2026-07-23:振幅统计开发前;含执行手册进教练、日亏损冻结、手机监控 UI、振幅统计开发方案等 |
|
||||
| `snapshot/20260721-2` | `a721642` | 2026-07-21 晚:日亏损次数冻结、交易执行手册入中控策略说明、期权/Gate 执行手册文档等 |
|
||||
@@ -28,7 +31,7 @@
|
||||
git tag -l 'snapshot/*'
|
||||
|
||||
# 检出快照(只读查看,勿在此分支直接开发)
|
||||
git checkout snapshot/20260723-2
|
||||
git checkout snapshot/20260726-2
|
||||
|
||||
# 回到主线
|
||||
git checkout main
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
# 服务说明与报价说明
|
||||
|
||||
> 本文说明本系统的定位、适用对象、托管方式与参考报价。
|
||||
> 配套文件:`著作权声明.md`、`软件使用授权合同-模板.md`(托管服务与软件使用合同)。
|
||||
> **本系统以著作权人自用为主**;对外托管属个案合作,并非标准化「卖工具」业务。
|
||||
|
||||
---
|
||||
|
||||
## 1. 这是什么
|
||||
|
||||
`crypto_monitor`(加密货币交易监控与中控系统)由著作权人 **马建军** 历时约三个月持续开发,用于自身实盘交易中的:
|
||||
|
||||
- 多交易所实例监控与下单辅助
|
||||
- 风控与纪律约束(如日亏冻结、执行规则落地到系统)
|
||||
- 复盘、关键位、期权/合约相关流程(以实际开通功能为准)
|
||||
- 中控统一查看与管理
|
||||
|
||||
开发目的首先是:**把交易习惯钉进系统,减少情绪单与随意操作**,而不是面向市场量产销售的通用软件商品。
|
||||
|
||||
---
|
||||
|
||||
## 2. 定位与适用对象
|
||||
|
||||
### 2.1 定位
|
||||
|
||||
| 是 | 不是 |
|
||||
|----|------|
|
||||
| 全职(或准全职)交易者的执行与纪律系统 | 兼职「玩玩」的下单插件 |
|
||||
| 规则、限制、复盘一起用的工作台 | 帮你加杠杆、追涨杀跌的「发财工具」 |
|
||||
| 著作权人自用为主;对外仅少量托管 | 开源产品或标准化 SaaS 大卖场 |
|
||||
|
||||
### 2.2 适合
|
||||
|
||||
- 以交易为主要工作、愿意按规则执行的人
|
||||
- 认同执行手册与系统内限制(含开仓限制、冻结等)
|
||||
- 接受「一户一机、不交付源码、按期付费」的托管方式
|
||||
- 账户规模与付费意愿匹配(服务费不应明显高于可承受的交易成本)
|
||||
|
||||
### 2.3 不适合(一般不承接)
|
||||
|
||||
- 兼职、偶尔开几单的小散
|
||||
- 只想要更快开仓、更高杠杆,不愿接受纪律约束
|
||||
- 要求交付源码、私有仓库权限或「买断随便改」
|
||||
- 希望多人共用一台服务器以压低费用
|
||||
|
||||
**说明:** 不适合不等于否定任何人,而是产品与服务形态不匹配;强行上线往往浪费双方时间。
|
||||
|
||||
---
|
||||
|
||||
## 3. 对外怎么提供(若合作)
|
||||
|
||||
默认且唯一推荐的方式:
|
||||
|
||||
1. **著作权人提供专属服务器**(一用户一服务器,不与其他客户共用)
|
||||
2. **部署中控与实例**,配置域名 / HTTPS
|
||||
3. 客户仅获得 **访问地址 + 登录账号**
|
||||
4. **不交付源代码**、不开放 Git、不移交服务器 root(由甲方代持运维)
|
||||
|
||||
合作前建议:先阅读相关执行/行为说明,确认认同纪律设计,再谈部署与费用。
|
||||
|
||||
正式合作须签署《托管服务与软件使用合同》(见合同模板)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 费用构成
|
||||
|
||||
费用分四项,建议在报价单中分列,避免被理解成「只卖服务器」:
|
||||
|
||||
| 费用 | 含义 | 通常周期 |
|
||||
|------|------|----------|
|
||||
| 服务器费用 | 该客户专属云主机、带宽、磁盘等 | 月 / 年 |
|
||||
| 域名费用 | 域名注册或续费(代持或客户自带域名) | 年 |
|
||||
| 部署费用 | 首次装机、证书、上线、基础培训 | 一次性 |
|
||||
| 程序使用费 | 软件托管使用权、基础更新与运维响应 | 月 / 年 |
|
||||
|
||||
续费年一般不再收部署费(大改版或迁移可另议)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 参考报价(非标价,可协商)
|
||||
|
||||
以下为**面向全职交易者、个案托管**的参考区间(人民币)。
|
||||
因以自用为主、名额有限,实际以当时口头/书面报价为准,可高于下列下限。
|
||||
|
||||
### 5.1 分项参考
|
||||
|
||||
| 项目 | 参考区间 | 备注 |
|
||||
|------|----------|------|
|
||||
| 服务器费用 | **200–400 元/月** | 按机型实报或固定档;专属机,不共用 |
|
||||
| 域名费用 | **60–120 元/年** | 实报实销;客户自带域名可减免 |
|
||||
| 部署费用 | **2,000–5,000 元** | 一次性;含上线与基础使用说明 |
|
||||
| 程序使用费 | **1,000–2,500 元/月** 或 **10,000–25,000 元/年** | 年付可相当于少收 1~2 个月 |
|
||||
|
||||
### 5.2 首年打包示意(便于沟通)
|
||||
|
||||
| 档位 | 首年大约量级 | 思路 |
|
||||
|------|--------------|------|
|
||||
| 协作档 | 约 **1.5–2.5 万** | 部署中档 + 服务器 + 使用费中低 |
|
||||
| 标准档 | 约 **2–4 万** | 部署与使用费取中高,含优先响应 |
|
||||
|
||||
**不提供:** 低价引流套餐、兼职小资金特惠、源码买断(若极少数个案谈源码,须另签合同且价格远高于年使用费,默认不做)。
|
||||
|
||||
### 5.3 付款与停服
|
||||
|
||||
- 部署费 + 首周期费用:签约后约定日内支付,到账后排期部署
|
||||
- 续费:到期前支付;逾期可暂停访问,严重逾期可停服并释放专属服务器
|
||||
- 细节以合同条款为准
|
||||
|
||||
---
|
||||
|
||||
## 6. 服务边界(简要)
|
||||
|
||||
**甲方(马建军)合理范围内可提供:**
|
||||
|
||||
- 专属机上的首次部署与基础运维
|
||||
- 程序常规更新、进程异常处理
|
||||
- 约定范围内的使用说明
|
||||
|
||||
**一般不包含(除非另议):**
|
||||
|
||||
- 代客交易、代管资金、投资建议
|
||||
- 保证盈利或胜率
|
||||
- 7×24 即时响应当成「专职客服」
|
||||
- 按客户要求无限改需求而不另计定制费
|
||||
|
||||
交易盈亏由客户自行承担;系统为辅助与纪律工具。
|
||||
|
||||
---
|
||||
|
||||
## 7. 知识产权
|
||||
|
||||
- 软件与文档著作权归 **马建军** 所有,见 `著作权声明.md`
|
||||
- 托管仅授权约定范围内的使用权,**不转移著作权、不交付源码**
|
||||
- 仓库为私有保存;私有不影响著作权主张
|
||||
|
||||
---
|
||||
|
||||
## 8. 联系
|
||||
|
||||
- 著作权人 / 服务提供方:马建军
|
||||
- 电话:18364911125
|
||||
|
||||
意向合作请说明:交易经验与是否全职、大致账户规模(可不精确)、希望开通的交易所、是否接受系统纪律限制。
|
||||
**谢绝:** 仅询源码价格、要求多人共用一台服务器、明确表示不接受任何交易限制的需求。
|
||||
|
||||
---
|
||||
|
||||
*文档版本:与仓库同步维护;报价为参考,最终以双方确认的报价单与合同为准。*
|
||||
+7
-2
@@ -74,10 +74,15 @@ OKX_OPTIONS_API_PASSPHRASE=...
|
||||
|
||||
## 5. 微信提醒
|
||||
|
||||
当某笔持仓 **未实现盈亏 ≥ 已付权利金的 100%**(翻倍)时,会发 **一条** 企业微信提醒(同一笔只提醒一次).
|
||||
|
||||
需已配置 `WECHAT_WEBHOOK`.
|
||||
|
||||
| 场景 | 标题 | 说明 |
|
||||
|------|------|------|
|
||||
| **开仓** | 【OKX期权·开仓】 | 下单成功并写入本地后必发(幂等) |
|
||||
| **平仓** | 【OKX期权·平仓】 | 手动全平 / 目标位全平 / 到期或交易所平仓同步后必发(幂等) |
|
||||
| 浮盈翻倍 | 【OKX期权·翻倍提醒】 | 未实现盈亏 ≥ 已付权利金约 100%,同一笔只提醒一次 |
|
||||
| 挂单超时撤销 | 【OKX期权·挂单超时撤销】 | 平仓挂单超时被系统撤销 |
|
||||
|
||||
## 6. 与永续 / 对冲计划的关系
|
||||
|
||||
| | 永续(子账户) | 期权(主账户) |
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# 著作权声明
|
||||
|
||||
## 作品信息
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 作品名称 | crypto_monitor(加密货币交易监控与中控系统) |
|
||||
| 作品形式 | 计算机软件及相关技术文档 |
|
||||
| 著作权人 | 马建军 |
|
||||
| 联系电话 | 18364911125 |
|
||||
| 权利主张起始 | 2026 年(以本仓库首次提交及后续持续开发为准) |
|
||||
|
||||
## 权利声明
|
||||
|
||||
本仓库所含下列内容之著作权归 **马建军** 所有:
|
||||
|
||||
1. 源代码、脚本、配置模板与部署相关文件;
|
||||
2. 界面文案、说明文档、执行手册、策略与设计类文档;
|
||||
3. 由著作权人创作并纳入本仓库的图表、脑图及其他配套材料。
|
||||
|
||||
**Copyright © 2026 马建军. 保留所有权利。**
|
||||
|
||||
未经著作权人书面许可,任何单位或个人不得擅自:
|
||||
|
||||
- 复制、传播、公开披露本仓库全部或部分内容;
|
||||
- 出售、出租、赠与或以任何方式向第三方提供本软件或其衍生版本;
|
||||
- 删除或篡改本声明及表明著作权归属的标识。
|
||||
|
||||
本仓库计划以私有方式保存;私有并不影响著作权人对本作品享有的权利。
|
||||
|
||||
## 证明与版本痕迹
|
||||
|
||||
本作品的创作过程以 Git 提交历史、远程私有仓库记录及快照标签(如 `snapshot/*`)为时间线依据。著作权主张以本声明与上述开发痕迹为准。
|
||||
|
||||
## 免责(与著作权并列说明)
|
||||
|
||||
本软件及相关文档仅供著作权人授权范围内的交易辅助与内部使用。市场有风险,交易决策与盈亏由使用者自行承担;本声明不构成任何投资建议。
|
||||
|
||||
## 对外提供方式
|
||||
|
||||
著作权人对外提供本软件的**默认方式**为:由著作权人为每位客户提供**专属服务器**(一用户一服务器,不与其他客户共用同一台机器)与部署,客户通过访问地址与账号使用,并缴纳服务器费、域名费、部署费及程序使用费;**不交付源代码**。
|
||||
|
||||
对外托管或授权使用时,请签署《托管服务与软件使用合同》(模板见同目录 `软件使用授权合同-模板.md`)。服务定位、适用对象与参考报价见 `服务说明与报价说明.md`。未签署有效合同的,除著作权人本人外,任何人均无权使用、复制或传播本软件。
|
||||
|
||||
## 联系
|
||||
|
||||
- 著作权人:马建军
|
||||
- 电话:18364911125
|
||||
|
||||
本声明随仓库版本一并维护;如有更新,以仓库中最新文本为准。
|
||||
@@ -0,0 +1,192 @@
|
||||
# 托管服务与软件使用合同(模板)
|
||||
|
||||
> 说明:本文为**合同模板**,适用于甲方(马建军)提供**专属服务器**与部署、乙方通过网页/账号使用软件、**不交付源码**的托管模式。
|
||||
> **一用户一服务器**:每位客户单独一台(套)服务器,不与其他客户共用同一台服务器。
|
||||
> 与仓库内《著作权声明》配套:声明主张权利;本合同约定服务范围、费用与使用边界。
|
||||
> 签署前请双方核对条款;金额较大或长期合作,建议再请律师审阅。
|
||||
|
||||
---
|
||||
|
||||
**合同编号:** ________________
|
||||
**签订日期:** ______ 年 ____ 月 ____ 日
|
||||
**签订地点:** ________________
|
||||
|
||||
## 甲方(服务提供方 / 著作权人)
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 姓名 | 马建军 |
|
||||
| 联系电话 | 18364911125 |
|
||||
| 身份证件号码 | ________________(签署时填写) |
|
||||
| 住址 | ________________(签署时填写,选填) |
|
||||
|
||||
## 乙方(客户 / 使用方)
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 姓名 / 名称 | ________________ |
|
||||
| 证件类型及号码 | ________________ |
|
||||
| 联系电话 | ________________ |
|
||||
| 住址 / 住所地 | ________________(选填) |
|
||||
|
||||
甲乙双方就甲方在其控制的服务器上部署、运维 `crypto_monitor`(加密货币交易监控与中控系统,以下称「本软件」),并向乙方提供**托管使用服务**,经协商一致,订立本合同。
|
||||
|
||||
---
|
||||
|
||||
## 第一条 服务内容与交付方式
|
||||
|
||||
1.1 **服务模式**:甲方为乙方提供**专属**云服务器(或等价专属托管环境)、域名解析(或子域名)、程序部署与运行维护;乙方通过甲方提供的 **访问地址与账号** 使用本软件,**不交付、不提供** 源代码、私有仓库权限、部署脚本全集或可用于独立重建系统的技术资料。
|
||||
|
||||
1.2 **一用户一服务器**:本合同项下服务器**仅供乙方使用**,不与其他客户共用同一台服务器、同一操作系统实例或同一套生产部署环境。甲方不得将其他客户的程序、数据或账号部署于本合同约定的专属服务器上。
|
||||
|
||||
1.3 **交付物**(勾选实际提供项):
|
||||
- [ ] 专属服务器标识 / 实例 ID(选填):________________
|
||||
- [ ] 中控访问地址:________________
|
||||
- [ ] 实例访问地址(交易所):________________ / ________________ / ________________
|
||||
- [ ] 登录账号:________________(或另行发放)
|
||||
- [ ] 使用说明 / 培训(____ 次,每次 ____ 分钟,选填)
|
||||
|
||||
1.4 **不包含**(除非另签书面补充协议并另付费):源码转让、源码只读权限、独立私有化部署包、二次开发源代码交付、数据库完整镜像导出用于迁移至第三方系统、服务器 root/控制台账号移交(服务器由甲方代持运维)。
|
||||
|
||||
1.5 本软件著作权及部署架构归甲方所有。专属服务器的云账号/机器所有权或租赁关系由甲方管理,乙方取得的是**该服务器上本软件的有限使用权**,不转让著作权、商标权、服务器所有权及其他知识产权。
|
||||
|
||||
---
|
||||
|
||||
## 第二条 授权范围与使用限制
|
||||
|
||||
2.1 **授权性质**:普通、非独占、不可再许可;仅限本合同约定的**专属服务器**及域名/访问地址范围内使用。
|
||||
|
||||
2.2 **使用主体**:仅限乙方本人及经甲方书面确认的 ______ 名操作人员;账号不得转借、共享给合同外第三方。
|
||||
|
||||
2.3 **使用目的**:仅限乙方自身交易辅助、内部监控与运营;不得将本软件或实质相同的功能作为产品/服务向不特定公众或第三方收费提供。
|
||||
|
||||
2.4 **服务期限**:
|
||||
- 自 ______ 年 ____ 月 ____ 日起,至 ______ 年 ____ 月 ____ 日止;
|
||||
- 期满前 ______ 日双方可协商续签;期满未续费且未书面延期的,甲方有权停服并回收该专属服务器资源。
|
||||
|
||||
2.5 乙方不得实施下列行为:
|
||||
1. 要求或试图获取源码、Git 仓库、服务器 root/云控制台权限(合同另有约定的除外);
|
||||
2. 复制、传播、截图外传足以重建系统的架构说明、配置全集或程序文件;
|
||||
3. 对系统进行反向工程、抓包重建、或委托他人仿制同类托管产品对外经营;
|
||||
4. 将访问账号、域名、API 密钥用于合同约定外的用途或转售;
|
||||
5. 攻击、扫描本合同专属服务器或甲方其他基础设施。
|
||||
|
||||
---
|
||||
|
||||
## 第三条 费用与支付
|
||||
|
||||
3.1 乙方按下列项目向甲方支付费用(勾选并填写金额;可打包为「标准套餐价」并在备注中列明分项):
|
||||
|
||||
| 费用项目 | 说明 | 金额(元) | 计费周期 |
|
||||
|----------|------|------------|----------|
|
||||
| 服务器费用 | **乙方专属**云主机、带宽、磁盘等(不与其他客户分摊同一台机器) | ¥ ______ | □月付 □年付 |
|
||||
| 域名费用 | 域名注册/续费(域名归属:□甲方代持 □乙方自有,解析由甲方配置) | ¥ ______ | □年付 |
|
||||
| 部署费用 | 在专属服务器上首次环境搭建、证书、实例与中控上线(一次性) | ¥ ______ | 一次性 |
|
||||
| 程序使用费 | 本软件托管使用权、日常更新与基础运维 | ¥ ______ | □月付 □年付 |
|
||||
|
||||
3.2 **合计**(首年 / 首月应付):人民币(大写)________________ 元整(¥ ________)。
|
||||
|
||||
3.3 **支付方式与时间**:________________(如:签约后 ____ 日内付部署费+首周期费用;之后每 ____ 提前 ____ 日支付续费)。
|
||||
|
||||
3.4 **续费**:服务期满前,乙方按 3.1 约定支付下一周期费用;逾期超过 ______ 日未付的,甲方有权暂停服务;逾期超过 ______ 日仍未付的,甲方有权解除合同并停服,已付未消费部分按实际服务天数抵扣后退还(部署费是否退还:□不退 □按约定 ________________)。
|
||||
|
||||
3.5 **价格调整**:续签时,因云厂商涨价、域名涨价或功能范围扩大,甲方可提前 ______ 日书面通知调整后续周期价格;乙方不同意调整的,可在当前周期结束后不再续签。
|
||||
|
||||
3.6 [ ] 本次为试用 / 友情托管:期限至 ______,费用减免 ________________,乙方仍须遵守第二条全部限制。
|
||||
|
||||
---
|
||||
|
||||
## 第四条 部署、运维与更新
|
||||
|
||||
4.1 **甲方责任**(合理范围内):
|
||||
- 按约定完成首次部署并使乙方可以登录使用;
|
||||
- 程序版本更新、安全补丁、PM2/进程异常重启等**基础运维**(具体 SLA:________________,如「工作日 24 小时内响应」);
|
||||
- 因交易所 API 变更导致的**常规适配**(重大重构另议)。
|
||||
|
||||
4.2 **乙方责任**:
|
||||
- 提供合法有效的交易所 API 等密钥信息,并保证账户使用合规;
|
||||
- 妥善保管登录密码;因乙方泄露导致的损失由乙方承担;
|
||||
- 按约定及时支付各项费用。
|
||||
|
||||
4.3 **数据**:乙方在系统中的交易记录、配置等业务数据归属乙方,并存放于本合同专属服务器;甲方为运维可接触相关数据,但不得用于合同约定外的目的,亦不得将乙方数据混存于其他客户服务器。合同终止后,乙方可申请导出**业务数据**(格式:________________,费用:________________);**不包含**源码与部署环境镜像。
|
||||
|
||||
4.4 **停服与备份**:甲方在停服前 ______ 日通知乙方(因乙方欠费紧急停服除外);停服后该专属服务器上的数据保留 ______ 日,逾期可删除并释放服务器资源。
|
||||
|
||||
---
|
||||
|
||||
## 第五条 保密
|
||||
|
||||
5.1 乙方对知悉的本软件存在、界面逻辑、非公开功能、报价及甲方技术方案负有保密义务。
|
||||
|
||||
5.2 甲方对乙方的 API 密钥、账户信息负有保密义务,除运维必需与法律要求外不得向第三方披露。
|
||||
|
||||
5.3 保密期限:合同存续期间及终止后 ______ 年(未填则视为 5 年)。
|
||||
|
||||
---
|
||||
|
||||
## 第六条 免责与风险提示
|
||||
|
||||
6.1 本软件为交易辅助工具,不构成投资建议。市场有风险,乙方交易决策与盈亏自行承担。
|
||||
|
||||
6.2 因行情、交易所接口变更、网络故障、云厂商故障、乙方误操作等导致的交易或间接损失,在法律允许范围内甲方不承担责任;因甲方故意或重大过失造成的服务长时间不可用除外(可约定:连续不可用超过 ____ 小时按比例退还当期程序使用费)。
|
||||
|
||||
6.3 甲方保证其有权提供本托管服务并享有本软件著作权;乙方保证身份信息及资金账户来源合法。
|
||||
|
||||
---
|
||||
|
||||
## 第七条 违约责任
|
||||
|
||||
7.1 乙方欠费、外传账号、试图获取源码或违反第二条的,甲方有权**暂停或立即终止服务**,并要求:
|
||||
1. 停止违约行为;
|
||||
2. 支付欠费及违约金人民币 ________ 元(或按实际损失);
|
||||
3. 赔偿甲方维权合理费用。
|
||||
|
||||
7.2 甲方无正当理由逾期未完成首次部署超过 ______ 日,或恶意长期停服且无合理解释的,乙方有权解除合同并要求退还已付未消费部分(部署费处理按 3.4 约定)。
|
||||
|
||||
---
|
||||
|
||||
## 第八条 合同解除与终止
|
||||
|
||||
8.1 协商一致可书面解除。
|
||||
|
||||
8.2 一方严重违约,守约方书面通知后 ______ 日内仍未改正的,守约方可解除。
|
||||
|
||||
8.3 终止后:乙方停止使用;甲方关闭访问权限;双方按第四条、第五条履行数据与保密义务。
|
||||
|
||||
---
|
||||
|
||||
## 第九条 争议解决
|
||||
|
||||
因本合同引起的争议,双方协商解决;协商不成的,提交甲方住所地有管辖权的人民法院诉讼解决(或:提交 ________ 仲裁委员会仲裁)。
|
||||
|
||||
---
|
||||
|
||||
## 第十条 其他
|
||||
|
||||
10.1 未尽事宜可签订补充协议。
|
||||
|
||||
10.2 本合同一式贰份,甲乙双方各执壹份,具有同等法律效力。
|
||||
|
||||
10.3 附件(如有):□《著作权声明》副本 □《服务说明与报价说明》 □ 服务清单 / 报价单 □ 域名与实例列表 □ 其他:________
|
||||
|
||||
---
|
||||
|
||||
## 签署栏
|
||||
|
||||
**甲方(服务提供方 / 著作权人):**
|
||||
|
||||
签名:________________ 日期:______ 年 ____ 月 ____ 日
|
||||
|
||||
**乙方(客户):**
|
||||
|
||||
签名 / 盖章:________________ 日期:______ 年 ____ 月 ____ 日
|
||||
|
||||
---
|
||||
|
||||
## 填写提示(签署前可删本段)
|
||||
|
||||
1. **标准商业路径**:专属服务器费 + 域名费 + 部署费(首单)+ 程序使用费(按月/年)— 四项建议在报价单里写清,合同 3.1 表格与报价一致。
|
||||
2. **一用户一服务器**:新客户开新机器;不要把多名客户塞进同一台 VPS。
|
||||
3. **源码**:默认一律不交付;若客户坚持私有化,应另签高价「源码许可/买断」合同,与本托管模板分开。
|
||||
4. **自用**:著作权人本人使用无需签本合同,见《著作权声明》。
|
||||
5. **不要**在仓库添加开源 `LICENSE`(MIT 等),与「保留所有权利 + 托管授权」冲突。
|
||||
@@ -61,6 +61,11 @@
|
||||
document.querySelectorAll(".embed-top-nav [data-embed-tab]").forEach((a) => {
|
||||
a.classList.toggle("active", a.getAttribute("data-embed-tab") === tab);
|
||||
});
|
||||
if (global.InstanceMobileNav && typeof global.InstanceMobileNav.onTabChange === "function") {
|
||||
global.InstanceMobileNav.onTabChange(tab);
|
||||
} else if (global.InstanceMobileNav && typeof global.InstanceMobileNav.syncTabActive === "function") {
|
||||
global.InstanceMobileNav.syncTabActive(tab);
|
||||
}
|
||||
}
|
||||
|
||||
function pageNavAllowed(tab) {
|
||||
@@ -235,9 +240,57 @@
|
||||
const parts = [];
|
||||
if (qs) parts.push(qs);
|
||||
parts.push("embed=1");
|
||||
if (tab === "settings") {
|
||||
try {
|
||||
const st = new URLSearchParams(location.search).get("settings_tab");
|
||||
if (st) parts.push("settings_tab=" + encodeURIComponent(st));
|
||||
} catch (_) {}
|
||||
}
|
||||
return url + "?" + parts.join("&");
|
||||
}
|
||||
|
||||
function setSettingsSubTabInUrl(key) {
|
||||
if (!key) return;
|
||||
try {
|
||||
const q = new URLSearchParams(location.search);
|
||||
q.set("tab", "settings");
|
||||
q.set("settings_tab", key);
|
||||
q.set("embed", "1");
|
||||
history.replaceState(null, "", "/embed?" + q.toString());
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function activateSettingsSubTab(key) {
|
||||
if (!key) return;
|
||||
setSettingsSubTabInUrl(key);
|
||||
const pane = tabPanes.get("settings") || document;
|
||||
const radio = pane.querySelector(
|
||||
'input.env-tab-radio[data-settings-tab="' + key + '"]'
|
||||
);
|
||||
if (radio) radio.checked = true;
|
||||
}
|
||||
|
||||
function formActionPath(form) {
|
||||
try {
|
||||
return new URL(form.action || "", location.href).pathname.replace(/\/$/, "") || "/";
|
||||
} catch (_) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function maybeKeepSettingsSubTabAfterForm(form) {
|
||||
const path = formActionPath(form);
|
||||
if (path === "/manual_transfer") {
|
||||
setSettingsSubTabInUrl("transfer");
|
||||
return "transfer";
|
||||
}
|
||||
if (path.indexOf("/api/options/transfer") >= 0 || path.indexOf("/api/options/cross-transfer") >= 0) {
|
||||
setSettingsSubTabInUrl("options_transfer");
|
||||
return "options_transfer";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
async function fetchTabHtml(tab) {
|
||||
const r = await fetch(embedPageUrl(tab), {
|
||||
credentials: "same-origin",
|
||||
@@ -400,14 +453,15 @@
|
||||
}
|
||||
}
|
||||
const fd = new FormData(form);
|
||||
const keepSub = maybeKeepSettingsSubTabAfterForm(form);
|
||||
return fetch(form.action, {
|
||||
method: form.method || "POST",
|
||||
body: fd,
|
||||
credentials: "same-origin",
|
||||
redirect: "manual",
|
||||
})
|
||||
.then(() => reloadCurrentTab())
|
||||
.catch(() => reloadCurrentTab());
|
||||
.then(() => reloadCurrentTab().then(() => activateSettingsSubTab(keepSub)))
|
||||
.catch(() => reloadCurrentTab().then(() => activateSettingsSubTab(keepSub)));
|
||||
}
|
||||
|
||||
function patchApplyListWindow() {
|
||||
@@ -466,14 +520,15 @@
|
||||
if (CUSTOM_SUBMIT_FORM_IDS.has(form.id)) return;
|
||||
ev.preventDefault();
|
||||
const fd = new FormData(form);
|
||||
const keepSub = maybeKeepSettingsSubTabAfterForm(form);
|
||||
fetch(form.action, {
|
||||
method: form.method || "POST",
|
||||
body: fd,
|
||||
credentials: "same-origin",
|
||||
redirect: "manual",
|
||||
})
|
||||
.then(() => reloadCurrentTab())
|
||||
.catch(() => reloadCurrentTab());
|
||||
.then(() => reloadCurrentTab().then(() => activateSettingsSubTab(keepSub)))
|
||||
.catch(() => reloadCurrentTab().then(() => activateSettingsSubTab(keepSub)));
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* 实例手机壳: ≤720px 底栏 +「更多」,与 embed soft-nav 同步.
|
||||
*/
|
||||
(function (global) {
|
||||
const PRIMARY = { trade: 1, key_monitor: 1, options: 1 };
|
||||
const MQ = "(max-width: 720px)";
|
||||
|
||||
function isEmbedShell() {
|
||||
return document.body && document.body.getAttribute("data-embed-shell") === "1";
|
||||
}
|
||||
|
||||
function isMobileLayout() {
|
||||
return window.matchMedia(MQ).matches;
|
||||
}
|
||||
|
||||
function syncPhoneClass() {
|
||||
if (!document.body) return;
|
||||
document.body.classList.toggle("inst-phone", isMobileLayout());
|
||||
}
|
||||
|
||||
function currentTab() {
|
||||
if (global.InstanceEmbed && typeof global.InstanceEmbed.getTab === "function") {
|
||||
return global.InstanceEmbed.getTab();
|
||||
}
|
||||
try {
|
||||
const t = new URLSearchParams(location.search).get("tab");
|
||||
if (t) return t;
|
||||
} catch (_) {}
|
||||
return (document.body && document.body.getAttribute("data-page")) || "trade";
|
||||
}
|
||||
|
||||
function closeMore() {
|
||||
document.body.classList.remove("inst-mobile-more-open");
|
||||
const more = document.getElementById("inst-mobile-more");
|
||||
const btn = document.getElementById("inst-m-tab-more");
|
||||
if (more) more.setAttribute("aria-hidden", "true");
|
||||
if (btn) btn.setAttribute("aria-expanded", "false");
|
||||
syncTabActive(currentTab());
|
||||
}
|
||||
|
||||
function openMore() {
|
||||
if (!isMobileLayout()) return;
|
||||
document.body.classList.add("inst-mobile-more-open");
|
||||
const more = document.getElementById("inst-mobile-more");
|
||||
const btn = document.getElementById("inst-m-tab-more");
|
||||
if (more) more.setAttribute("aria-hidden", "false");
|
||||
if (btn) btn.setAttribute("aria-expanded", "true");
|
||||
syncTabActive(currentTab());
|
||||
}
|
||||
|
||||
function toggleMore() {
|
||||
if (document.body.classList.contains("inst-mobile-more-open")) closeMore();
|
||||
else openMore();
|
||||
}
|
||||
|
||||
function syncTabActive(tab) {
|
||||
const page = tab || currentTab();
|
||||
const primary = !!PRIMARY[page];
|
||||
const moreOpen = document.body.classList.contains("inst-mobile-more-open");
|
||||
document.querySelectorAll("#inst-mobile-tabbar .inst-m-tab").forEach((el) => {
|
||||
const t = el.getAttribute("data-embed-tab") || "";
|
||||
let on = false;
|
||||
if (t === "more") on = moreOpen || !primary;
|
||||
else on = !moreOpen && t === page;
|
||||
el.classList.toggle("active", on);
|
||||
});
|
||||
document.querySelectorAll("#inst-mobile-more .inst-mobile-more-nav [data-embed-tab]").forEach((a) => {
|
||||
a.classList.toggle("active", a.getAttribute("data-embed-tab") === page);
|
||||
});
|
||||
}
|
||||
|
||||
/** embed 切页时关闭「更多」并同步高亮 */
|
||||
function onTabChange(tab) {
|
||||
document.body.classList.remove("inst-mobile-more-open");
|
||||
const more = document.getElementById("inst-mobile-more");
|
||||
const btn = document.getElementById("inst-m-tab-more");
|
||||
if (more) more.setAttribute("aria-hidden", "true");
|
||||
if (btn) btn.setAttribute("aria-expanded", "false");
|
||||
syncTabActive(tab);
|
||||
}
|
||||
|
||||
function goTab(tab) {
|
||||
if (!tab || tab === "more") return;
|
||||
closeMore();
|
||||
if (global.InstanceEmbed && typeof global.InstanceEmbed.loadTab === "function") {
|
||||
if (tab === currentTab()) {
|
||||
syncTabActive(tab);
|
||||
return;
|
||||
}
|
||||
void global.InstanceEmbed.loadTab(tab);
|
||||
return;
|
||||
}
|
||||
const pathMap = {
|
||||
dashboard: "/dashboard",
|
||||
key_monitor: "/key_monitor",
|
||||
trade: "/trade",
|
||||
strategy: "/strategy",
|
||||
strategy_records: "/strategy/records",
|
||||
options: "/options",
|
||||
options_review: "/options/review",
|
||||
hedge_plan: "/hedge-plan",
|
||||
records: "/records",
|
||||
stats: "/stats",
|
||||
risk_policy: "/risk_policy",
|
||||
system_guide: "/system_guide",
|
||||
env_config: "/env_config",
|
||||
settings: "/settings",
|
||||
};
|
||||
location.href = pathMap[tab] || "/trade";
|
||||
}
|
||||
|
||||
function bindChrome() {
|
||||
const moreBtn = document.getElementById("inst-m-tab-more");
|
||||
const backdrop = document.getElementById("inst-mobile-more-backdrop");
|
||||
const closeBtn = document.getElementById("inst-mobile-more-close");
|
||||
if (moreBtn) {
|
||||
moreBtn.addEventListener("click", (ev) => {
|
||||
ev.preventDefault();
|
||||
toggleMore();
|
||||
});
|
||||
}
|
||||
if (backdrop) backdrop.addEventListener("click", closeMore);
|
||||
if (closeBtn) closeBtn.addEventListener("click", closeMore);
|
||||
document.addEventListener("keydown", (ev) => {
|
||||
if (ev.key === "Escape" && document.body.classList.contains("inst-mobile-more-open")) {
|
||||
closeMore();
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll("#inst-mobile-tabbar .inst-m-tab[data-embed-tab]").forEach((el) => {
|
||||
if (el.getAttribute("data-embed-tab") === "more") return;
|
||||
el.addEventListener("click", (ev) => {
|
||||
if (ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey) return;
|
||||
ev.preventDefault();
|
||||
goTab(el.getAttribute("data-embed-tab"));
|
||||
});
|
||||
});
|
||||
document.querySelectorAll("#inst-mobile-more .inst-mobile-more-nav [data-embed-tab]").forEach((a) => {
|
||||
a.addEventListener("click", (ev) => {
|
||||
if (ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey) return;
|
||||
ev.preventDefault();
|
||||
goTab(a.getAttribute("data-embed-tab"));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function boot() {
|
||||
if (!isEmbedShell()) return;
|
||||
if (!document.getElementById("inst-mobile-tabbar")) return;
|
||||
syncPhoneClass();
|
||||
bindChrome();
|
||||
syncTabActive(currentTab());
|
||||
let resizeTimer = null;
|
||||
window.addEventListener("resize", () => {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(() => {
|
||||
const was = document.body.classList.contains("inst-phone");
|
||||
syncPhoneClass();
|
||||
if (!isMobileLayout()) closeMore();
|
||||
else if (!was) syncTabActive(currentTab());
|
||||
}, 120);
|
||||
});
|
||||
}
|
||||
|
||||
global.InstanceMobileNav = {
|
||||
syncTabActive,
|
||||
onTabChange,
|
||||
closeMore,
|
||||
isMobileLayout,
|
||||
};
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", boot);
|
||||
} else {
|
||||
boot();
|
||||
}
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
@@ -31,6 +31,8 @@
|
||||
function applyDisplayToNav(display) {
|
||||
const map = {
|
||||
dashboard: "show_nav_dashboard",
|
||||
key_monitor: "show_nav_key_monitor",
|
||||
trade: "show_nav_trade",
|
||||
strategy: "show_nav_strategy",
|
||||
strategy_records: "show_nav_strategy_records",
|
||||
records: "show_nav_records",
|
||||
@@ -44,14 +46,19 @@
|
||||
system_guide: "show_nav_system_guide",
|
||||
env_config: "show_nav_env_config",
|
||||
};
|
||||
document.querySelectorAll(".embed-top-nav [data-embed-tab], .top-nav a[href^='/']").forEach((a) => {
|
||||
const tab = a.getAttribute("data-embed-tab") || (a.getAttribute("href") || "").replace(/^\//, "").split("?")[0];
|
||||
const key = map[tab];
|
||||
if (!key) return;
|
||||
const show = navPrefShow(display, key);
|
||||
a.classList.toggle("nav-hidden", !show);
|
||||
a.style.display = show ? "" : "none";
|
||||
});
|
||||
document
|
||||
.querySelectorAll(
|
||||
".embed-top-nav [data-embed-tab], .top-nav a[href^='/'], #inst-mobile-tabbar [data-embed-tab], #inst-mobile-more [data-embed-tab]"
|
||||
)
|
||||
.forEach((a) => {
|
||||
const tab = a.getAttribute("data-embed-tab") || (a.getAttribute("href") || "").replace(/^\//, "").split("?")[0];
|
||||
if (tab === "more") return;
|
||||
const key = map[tab];
|
||||
if (!key) return;
|
||||
const show = navPrefShow(display, key);
|
||||
a.classList.toggle("nav-hidden", !show);
|
||||
a.style.display = show ? "" : "none";
|
||||
});
|
||||
global.__INSTANCE_DISPLAY__ = display;
|
||||
}
|
||||
|
||||
@@ -59,6 +66,8 @@
|
||||
const d = DISPLAY();
|
||||
const map = {
|
||||
dashboard: "show_nav_dashboard",
|
||||
key_monitor: "show_nav_key_monitor",
|
||||
trade: "show_nav_trade",
|
||||
strategy: "show_nav_strategy",
|
||||
strategy_records: "show_nav_strategy_records",
|
||||
records: "show_nav_records",
|
||||
|
||||
@@ -5526,3 +5526,441 @@ html[data-theme="light"] .options-review-wrap .or-reviewed-table tbody tr:hover
|
||||
color: #ff8b8b;
|
||||
}
|
||||
|
||||
/* —— 实例手机壳:底栏四件套(仅 ≤720px + body.inst-phone) —— */
|
||||
.inst-mobile-tabbar,
|
||||
.inst-mobile-more,
|
||||
.instance-phone-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
:root {
|
||||
--inst-m-tabbar-h: 56px;
|
||||
--inst-m-page-pad: calc(var(--inst-m-tabbar-h) + max(16px, env(safe-area-inset-bottom)) + 12px);
|
||||
}
|
||||
|
||||
/* 仅手机壳:不碰 >720 桌面/平板。overflow 放 container,避免裁切 fixed 下单弹窗 */
|
||||
body.inst-phone {
|
||||
padding-bottom: var(--inst-m-page-pad) !important;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body.inst-phone .embed-top-nav.top-nav {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body.inst-phone .header {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
body.inst-phone .header h1 {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body.inst-phone .container {
|
||||
padding-bottom: 8px !important;
|
||||
max-width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
body.inst-phone #embed-page-root,
|
||||
body.inst-phone .embed-tab-pane,
|
||||
body.inst-phone .embed-tab-pane.is-active-pane {
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body.inst-phone .card {
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body.inst-phone .instance-header-toolbar {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
body.inst-phone .instance-header-toolbar-end {
|
||||
width: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
body.inst-phone .instance-phone-only {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
body.inst-phone .instance-header-phone-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
html[data-theme="light"] body.inst-phone .instance-header-phone-strip {
|
||||
border-top-color: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
body.inst-phone .inst-phone-chip {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
min-height: 32px;
|
||||
min-width: 0;
|
||||
padding: 6px 8px;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
font-size: 12px;
|
||||
color: var(--inst-text);
|
||||
}
|
||||
|
||||
html[data-theme="light"] body.inst-phone .inst-phone-chip {
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
border-color: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
body.inst-phone .inst-phone-chip em {
|
||||
font-style: normal;
|
||||
color: var(--inst-muted);
|
||||
font-size: 11px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
body.inst-phone .inst-phone-chip b {
|
||||
font-weight: 600;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
body.inst-phone .inst-phone-chip--pnl b {
|
||||
color: var(--inst-nav-idle);
|
||||
}
|
||||
|
||||
/* 实盘/关键位等表单:窄屏拉满,避免挤成一行裁切 */
|
||||
body.inst-phone #add-order-form.form-row,
|
||||
body.inst-phone form.form-row {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
body.inst-phone #add-order-form.form-row > input:not([type="checkbox"]):not([type="radio"]),
|
||||
body.inst-phone #add-order-form.form-row > select,
|
||||
body.inst-phone #add-order-form #sltp-mode,
|
||||
body.inst-phone form.form-row > input:not([type="checkbox"]):not([type="radio"]),
|
||||
body.inst-phone form.form-row > select {
|
||||
flex: 1 1 100% !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body.inst-phone #add-order-form .order-entry-model-row,
|
||||
body.inst-phone #add-order-form .order-time-close-wrap,
|
||||
body.inst-phone #add-order-form > label,
|
||||
body.inst-phone #add-order-form > button {
|
||||
flex: 1 1 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
body.inst-phone #add-order-form .order-entry-model-row select {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
body.inst-phone .order-plan-preview {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/* 期权:防止宽表撑破页面;表内横向滑看「操作」 */
|
||||
body.inst-phone .options-page-wrap,
|
||||
body.inst-phone .options-dual-grid,
|
||||
body.inst-phone .options-order-card,
|
||||
body.inst-phone .options-pos-card-wrap {
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
body.inst-phone .options-dual-grid {
|
||||
grid-template-columns: minmax(0, 1fr) !important;
|
||||
}
|
||||
|
||||
body.inst-phone .options-chain-toolbar.form-row {
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
body.inst-phone .options-chain-toolbar .btn-secondary,
|
||||
body.inst-phone .options-chain-toolbar select,
|
||||
body.inst-phone .options-chain-toolbar .opt-chain-view-group,
|
||||
body.inst-phone .options-chain-toolbar .opt-type-btn-group {
|
||||
flex: 0 1 auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
body.inst-phone .options-strike-table-wrap,
|
||||
body.inst-phone .options-strike-table-wrap--t,
|
||||
body.inst-phone .options-history-table-wrap,
|
||||
body.inst-phone .table-wrap {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
overflow-x: auto !important;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior-x: contain;
|
||||
touch-action: pan-x pan-y;
|
||||
}
|
||||
|
||||
body.inst-phone .options-strike-table {
|
||||
width: max-content;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
body.inst-phone .options-strike-table th,
|
||||
body.inst-phone .options-strike-table td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 列表视图:隐藏合约 / 买一 / 到期平衡 / 距平衡(保留行权价·类型·卖一·操作) */
|
||||
body.inst-phone #opt-strike-head-list th:nth-child(3),
|
||||
body.inst-phone #opt-strike-head-list th:nth-child(5),
|
||||
body.inst-phone #opt-strike-head-list th:nth-child(6),
|
||||
body.inst-phone #opt-strike-head-list th:nth-child(7),
|
||||
body.inst-phone .opt-strike-row:not(.opt-strike-row-t) > td:nth-child(3),
|
||||
body.inst-phone .opt-strike-row:not(.opt-strike-row-t) > td:nth-child(5),
|
||||
body.inst-phone .opt-strike-row:not(.opt-strike-row-t) > td:nth-child(6),
|
||||
body.inst-phone .opt-strike-row:not(.opt-strike-row-t) > td:nth-child(7) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 选择后下单弹窗:盖过底栏,可滚动完整显示 */
|
||||
body.inst-phone .opt-order-backdrop {
|
||||
z-index: 2400;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
background: rgba(0, 0, 0, 0.72);
|
||||
}
|
||||
|
||||
body.inst-phone .opt-order-backdrop:not([hidden]) {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
body.inst-phone .opt-order-dialog {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
max-height: min(88vh, 720px);
|
||||
margin: 0;
|
||||
border-radius: 16px 16px 0 0;
|
||||
padding: 14px 14px calc(14px + env(safe-area-inset-bottom));
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
body.inst-phone .opt-order-dialog .options-order-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
body.inst-phone .options-estimate-row .opt-est-main {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
body.inst-phone .opt-size-mode-bar {
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body.inst-phone .inst-mobile-tabbar {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 80;
|
||||
height: calc(var(--inst-m-tabbar-h) + env(safe-area-inset-bottom));
|
||||
padding: 0 max(8px, env(safe-area-inset-right)) env(safe-area-inset-bottom)
|
||||
max(8px, env(safe-area-inset-left));
|
||||
align-items: stretch;
|
||||
justify-content: space-around;
|
||||
gap: 2px;
|
||||
background: color-mix(in srgb, #12161f 92%, transparent);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
backdrop-filter: blur(14px);
|
||||
-webkit-backdrop-filter: blur(14px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html[data-theme="light"] body.inst-phone .inst-mobile-tabbar {
|
||||
background: color-mix(in srgb, #f4f7fb 94%, transparent);
|
||||
border-top-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
body.inst-phone .inst-mobile-tabbar .inst-m-tab.nav-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body.inst-phone .inst-m-tab {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 6px 2px;
|
||||
padding: 0 4px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background: transparent;
|
||||
color: var(--inst-muted);
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
body.inst-phone .inst-m-tab:hover,
|
||||
body.inst-phone .inst-m-tab:focus-visible {
|
||||
color: var(--inst-text);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
body.inst-phone .inst-m-tab.active {
|
||||
color: var(--inst-nav-idle);
|
||||
background: rgba(143, 200, 255, 0.12);
|
||||
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--inst-nav-idle) 35%, transparent);
|
||||
}
|
||||
|
||||
body.inst-phone.inst-mobile-more-open .inst-mobile-more {
|
||||
display: block;
|
||||
}
|
||||
|
||||
body.inst-phone .inst-mobile-more {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 90;
|
||||
}
|
||||
|
||||
body.inst-phone .inst-mobile-more-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
body.inst-phone .inst-mobile-more-sheet {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
max-height: min(78vh, 560px);
|
||||
overflow: auto;
|
||||
padding: 10px 16px calc(16px + env(safe-area-inset-bottom));
|
||||
border-radius: 16px 16px 0 0;
|
||||
background: #12161f;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-bottom: none;
|
||||
box-shadow: 0 -12px 40px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
html[data-theme="light"] body.inst-phone .inst-mobile-more-sheet {
|
||||
background: #f4f7fb;
|
||||
border-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
body.inst-phone .inst-mobile-more-handle {
|
||||
width: 36px;
|
||||
height: 4px;
|
||||
margin: 2px auto 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
html[data-theme="light"] body.inst-phone .inst-mobile-more-handle {
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
body.inst-phone .inst-mobile-more-title {
|
||||
margin: 0 0 4px;
|
||||
font-size: 1rem;
|
||||
color: var(--inst-text);
|
||||
}
|
||||
|
||||
body.inst-phone .inst-mobile-more-hint {
|
||||
margin: 0 0 14px;
|
||||
font-size: 11px;
|
||||
color: var(--inst-muted);
|
||||
}
|
||||
|
||||
body.inst-phone .inst-mobile-more-nav {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
body.inst-phone .inst-mobile-more-nav a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 44px;
|
||||
padding: 10px 8px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
color: var(--inst-text);
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
html[data-theme="light"] body.inst-phone .inst-mobile-more-nav a {
|
||||
border-color: rgba(0, 0, 0, 0.1);
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
body.inst-phone .inst-mobile-more-nav a.nav-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body.inst-phone .inst-mobile-more-nav a.active {
|
||||
border-color: color-mix(in srgb, var(--inst-nav-idle) 45%, transparent);
|
||||
background: rgba(143, 200, 255, 0.12);
|
||||
color: var(--inst-nav-idle);
|
||||
}
|
||||
|
||||
body.inst-phone .inst-mobile-more-close {
|
||||
width: 100%;
|
||||
margin-top: 14px;
|
||||
min-height: 44px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: transparent;
|
||||
color: var(--inst-text);
|
||||
font: inherit;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
html[data-theme="light"] body.inst-phone .inst-mobile-more-close {
|
||||
border-color: rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -280,8 +280,15 @@
|
||||
const mode = currentSizeMode();
|
||||
const sheetsEl = document.getElementById("opt-sheets-amount");
|
||||
const ethEl = document.getElementById("opt-eth-amount");
|
||||
const hint = document.getElementById("opt-budget-full-hint");
|
||||
const capEl = document.getElementById("opt-budget-full-cap");
|
||||
if (sheetsEl) sheetsEl.style.display = mode === "sheets" ? "" : "none";
|
||||
if (ethEl) ethEl.style.display = mode === "eth_amount" ? "" : "none";
|
||||
if (hint) hint.style.display = mode === "budget_full" ? "" : "none";
|
||||
if (capEl && root && root.dataset.tradeBudget) {
|
||||
const n = Number(root.dataset.tradeBudget);
|
||||
if (Number.isFinite(n) && n > 0) capEl.textContent = n.toFixed(2);
|
||||
}
|
||||
document.querySelectorAll(".opt-size-mode-chip").forEach(function (chip) {
|
||||
const radio = chip.querySelector('input[name="opt-size-mode"]');
|
||||
chip.classList.toggle("is-selected", !!(radio && radio.checked));
|
||||
|
||||
Vendored
+18
@@ -70,7 +70,10 @@ HOT_RELOAD_EXACT = frozenset({
|
||||
"MONITOR_POLL_SECONDS",
|
||||
"AUTO_TRANSFER_ENABLED",
|
||||
"AUTO_TRANSFER_AMOUNT",
|
||||
"AUTO_TRANSFER_FROM",
|
||||
"AUTO_TRANSFER_TO",
|
||||
"AUTO_TRANSFER_BJ_HOUR",
|
||||
"TRANSFER_CCY",
|
||||
"FORCE_CLOSE_ENABLED",
|
||||
"FORCE_CLOSE_BJ_HOUR",
|
||||
"BTC_LEVERAGE",
|
||||
@@ -126,6 +129,17 @@ SELECT_OPTIONS: dict[str, tuple[tuple[str, str], ...]] = {
|
||||
("long_only", "仅做多"),
|
||||
("short_only", "仅做空"),
|
||||
),
|
||||
"AUTO_TRANSFER_FROM": (
|
||||
("funding", "funding 资金账户"),
|
||||
("swap", "swap 交易账户"),
|
||||
("spot", "spot 现货"),
|
||||
),
|
||||
"AUTO_TRANSFER_TO": (
|
||||
("swap", "swap 交易账户"),
|
||||
("funding", "funding 资金账户"),
|
||||
("spot", "spot 现货"),
|
||||
),
|
||||
"TRANSFER_CCY": (("USDT", "USDT"),),
|
||||
"HEDGE_PLAN_OO_BIAS_SPLIT_BY": (
|
||||
("budget", "预算金额"),
|
||||
("sheets", "张数"),
|
||||
@@ -136,6 +150,7 @@ _SELECT_ALIASES: dict[str, dict[str, str]] = {
|
||||
"OKX_TD_MODE": {"cross_margin": "cross", "isolated_margin": "isolated"},
|
||||
"BINANCE_MARGIN_MODE": {"cross_margin": "cross", "isolated_margin": "isolated"},
|
||||
"GATE_TD_MODE": {"cross_margin": "cross", "isolated_margin": "isolated"},
|
||||
"TRANSFER_CCY": {"usdt": "USDT"},
|
||||
}
|
||||
|
||||
|
||||
@@ -159,10 +174,13 @@ def normalize_select_value(key: str, value: Optional[str]) -> str:
|
||||
if low in aliases:
|
||||
return aliases[low]
|
||||
allowed = {v for v, _ in (SELECT_OPTIONS.get(key) or ())}
|
||||
allowed_by_lower = {v.lower(): v for v in allowed}
|
||||
if low in allowed:
|
||||
return low
|
||||
if raw in allowed:
|
||||
return raw
|
||||
if low in allowed_by_lower:
|
||||
return allowed_by_lower[low]
|
||||
return raw
|
||||
|
||||
|
||||
|
||||
Vendored
+9
-4
@@ -103,10 +103,10 @@ _SHARED_SECTIONS: list[dict[str, Any]] = [
|
||||
"fields": [
|
||||
("AUTO_TRANSFER_ENABLED", "启用自动划转", ""),
|
||||
("AUTO_TRANSFER_AMOUNT", "目标余额(U)", "交易账户目标 USDT"),
|
||||
("AUTO_TRANSFER_FROM", "划出账户", "funding 或 swap"),
|
||||
("AUTO_TRANSFER_TO", "划入账户", "swap 或 funding"),
|
||||
("AUTO_TRANSFER_FROM", "划出账户", "余额不足时从此账户划入交易账户"),
|
||||
("AUTO_TRANSFER_TO", "划入账户", "目标余额所在账户,一般为 swap"),
|
||||
("AUTO_TRANSFER_BJ_HOUR", "执行整点(北京时间)", ""),
|
||||
("TRANSFER_CCY", "划转币种", "默认 USDT"),
|
||||
("TRANSFER_CCY", "划转币种", ""),
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -200,6 +200,9 @@ _RUNTIME_ENV_DEFAULTS: dict[str, str] = {
|
||||
"RISK_MANUAL_CLOSE_DAILY_LIMIT": "2",
|
||||
"RISK_DAILY_LOSS_LIMIT": "2",
|
||||
"RISK_MOOD_ISSUES_DAILY_FREEZE": "true",
|
||||
"AUTO_TRANSFER_FROM": "funding",
|
||||
"AUTO_TRANSFER_TO": "swap",
|
||||
"TRANSFER_CCY": "USDT",
|
||||
"HEDGE_PLAN_SHOW_PERP_OPTIONS": "true",
|
||||
"HEDGE_PLAN_SHOW_OPTIONS_OPTIONS": "true",
|
||||
"OKX_OPTIONS_CHAIN_ASK_LIQ_FILTER_ENABLED": "true",
|
||||
@@ -214,7 +217,9 @@ _RUNTIME_ENV_DEFAULTS: dict[str, str] = {
|
||||
|
||||
def _effective_env_value(key: str, file_values: dict[str, str], schema_default: str = "") -> str:
|
||||
if key in file_values:
|
||||
return file_values[key]
|
||||
file_val = str(file_values.get(key) or "").strip()
|
||||
if file_val:
|
||||
return file_val
|
||||
runtime = os.getenv(key)
|
||||
if runtime is not None and str(runtime).strip() != "":
|
||||
return str(runtime).strip()
|
||||
|
||||
@@ -22,6 +22,7 @@ def execute_transfer_usdt(
|
||||
) -> tuple[bool, str, Any]:
|
||||
if amount <= 0:
|
||||
return False, "划转金额必须大于0", None
|
||||
ccy = (transfer_ccy or "USDT").strip().upper() or "USDT"
|
||||
ok_live, reason = ensure_live_ready()
|
||||
if not ok_live:
|
||||
return False, reason, None
|
||||
@@ -31,7 +32,7 @@ def execute_transfer_usdt(
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
resp = exchange.transfer(transfer_ccy, float(amount), from_account, to_account)
|
||||
resp = exchange.transfer(ccy, float(amount), from_account, to_account)
|
||||
return True, "划转成功", resp
|
||||
except Exception as e:
|
||||
msg = str(e)
|
||||
|
||||
@@ -71,6 +71,7 @@ def install_instance_theme_static(app) -> None:
|
||||
"strategy_roll.js": "application/javascript; charset=utf-8",
|
||||
"instance_page.css": "text/css; charset=utf-8",
|
||||
"instance_embed.js": "application/javascript; charset=utf-8",
|
||||
"instance_mobile_nav.js": "application/javascript; charset=utf-8",
|
||||
"instance_stats.js": "application/javascript; charset=utf-8",
|
||||
"instance_live.js": "application/javascript; charset=utf-8",
|
||||
"instance_settings_prefs.js": "application/javascript; charset=utf-8",
|
||||
@@ -672,6 +673,72 @@ def register_hub_routes(app):
|
||||
}
|
||||
)
|
||||
|
||||
@app.route("/api/hub/options/review/archive")
|
||||
@_hub_auth_required
|
||||
def api_hub_options_review_archive():
|
||||
"""中控期权档案:近 N 天已平仓复盘记录(默认排除对冲腿)."""
|
||||
from datetime import datetime, timedelta
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from lib.options.options_review_lib import (
|
||||
compute_review_stats,
|
||||
ensure_local_review_synced,
|
||||
list_review_trades,
|
||||
)
|
||||
|
||||
c = _ctx()
|
||||
get_db = c.get("get_db")
|
||||
if not get_db:
|
||||
return jsonify({"ok": False, "msg": "HUB_CTX 缺少 get_db"}), 500
|
||||
try:
|
||||
days = int(request.args.get("days") or "365")
|
||||
except ValueError:
|
||||
days = 365
|
||||
days = max(1, min(days, 3650))
|
||||
try:
|
||||
limit = int(request.args.get("limit") or "2000")
|
||||
except ValueError:
|
||||
limit = 2000
|
||||
limit = max(1, min(limit, 5000))
|
||||
include_hedge_legs = str(request.args.get("include_hedge_legs") or "").strip() in (
|
||||
"1",
|
||||
"true",
|
||||
"yes",
|
||||
)
|
||||
tz = ZoneInfo("Asia/Shanghai")
|
||||
closed_from = (datetime.now(tz) - timedelta(days=days)).strftime("%Y-%m-%d")
|
||||
cfg = (current_app.extensions or {}).get("options_cfg") or {}
|
||||
ex = cfg.get("exchange_options")
|
||||
conn = get_db()
|
||||
try:
|
||||
ensure_local_review_synced(conn, ex=ex, backfill_exchange_pnl=bool(ex))
|
||||
trades = list_review_trades(
|
||||
conn,
|
||||
include_hedge_legs=include_hedge_legs,
|
||||
closed_from=closed_from,
|
||||
limit=limit,
|
||||
offset=0,
|
||||
)
|
||||
stats = compute_review_stats(
|
||||
conn,
|
||||
include_hedge_legs=include_hedge_legs,
|
||||
closed_from=closed_from,
|
||||
)
|
||||
finally:
|
||||
conn.close()
|
||||
return jsonify(
|
||||
{
|
||||
"ok": True,
|
||||
"days": days,
|
||||
"limit": limit,
|
||||
"product": "options",
|
||||
"trades": trades,
|
||||
"stats": stats,
|
||||
}
|
||||
)
|
||||
|
||||
@app.route("/api/hub/trades/today")
|
||||
@_hub_auth_required
|
||||
def api_hub_trades_today():
|
||||
|
||||
@@ -0,0 +1,599 @@
|
||||
"""中控期权档案:同步 OKX options_review_trades 到 hub_symbol_archive.db."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from lib.hub.hub_symbol_archive_lib import (
|
||||
TRADING_DAY_RESET_HOUR,
|
||||
_connect,
|
||||
default_db_path,
|
||||
init_db as init_perp_archive_db,
|
||||
ms_to_trading_day,
|
||||
parse_wall_clock_ms,
|
||||
resolve_period_bounds,
|
||||
trading_day_bounds_ms,
|
||||
)
|
||||
|
||||
|
||||
def _now_ms() -> int:
|
||||
return int(time.time() * 1000)
|
||||
|
||||
|
||||
def init_options_archive_db(db_path: Path | None = None) -> None:
|
||||
"""确保期权缓存表存在(与永续共用同一 SQLite)."""
|
||||
init_perp_archive_db(db_path)
|
||||
conn = _connect(db_path)
|
||||
try:
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS archive_options_trade_cache (
|
||||
exchange_key TEXT NOT NULL,
|
||||
history_key TEXT NOT NULL,
|
||||
source_type TEXT,
|
||||
underlying TEXT,
|
||||
opened_at TEXT,
|
||||
closed_at TEXT,
|
||||
opened_at_ms INTEGER,
|
||||
closed_at_ms INTEGER,
|
||||
hold_seconds INTEGER,
|
||||
realized_pnl_total REAL,
|
||||
status_raw 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,
|
||||
strategy_tag TEXT,
|
||||
result_tag TEXT,
|
||||
reviewed INTEGER DEFAULT 0,
|
||||
source_label TEXT,
|
||||
payload_json TEXT NOT NULL,
|
||||
synced_at INTEGER NOT NULL,
|
||||
PRIMARY KEY (exchange_key, history_key)
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS idx_archive_options_closed
|
||||
ON archive_options_trade_cache (exchange_key, closed_at_ms)
|
||||
"""
|
||||
)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def purge_stale_options_trades_cache(
|
||||
exchange_key: str,
|
||||
active_history_keys: list[str],
|
||||
*,
|
||||
db_path: Path | None = None,
|
||||
) -> int:
|
||||
init_options_archive_db(db_path)
|
||||
ex_k = (exchange_key or "").strip().lower()
|
||||
if not ex_k:
|
||||
return 0
|
||||
active = {str(k).strip() for k in (active_history_keys or []) if str(k).strip()}
|
||||
conn = _connect(db_path)
|
||||
try:
|
||||
rows = conn.execute(
|
||||
"SELECT history_key FROM archive_options_trade_cache WHERE exchange_key=?",
|
||||
(ex_k,),
|
||||
).fetchall()
|
||||
stale = [r["history_key"] for r in rows if r["history_key"] not in active]
|
||||
removed = 0
|
||||
for hk in stale:
|
||||
cur = conn.execute(
|
||||
"DELETE FROM archive_options_trade_cache WHERE exchange_key=? AND history_key=?",
|
||||
(ex_k, hk),
|
||||
)
|
||||
removed += int(cur.rowcount or 0)
|
||||
return removed
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _optional_float(raw: Any) -> float | None:
|
||||
if raw in (None, ""):
|
||||
return None
|
||||
try:
|
||||
return float(raw)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _optional_int(raw: Any) -> int | None:
|
||||
if raw in (None, ""):
|
||||
return None
|
||||
try:
|
||||
return int(raw)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def upsert_options_trades_cache(
|
||||
exchange_key: str,
|
||||
trades: list[dict[str, Any]],
|
||||
*,
|
||||
db_path: Path | None = None,
|
||||
prune_missing: bool = True,
|
||||
) -> dict[str, int]:
|
||||
init_options_archive_db(db_path)
|
||||
ex_k = (exchange_key or "").strip().lower()
|
||||
if not ex_k:
|
||||
return {"upserted": 0, "removed": 0}
|
||||
now = _now_ms()
|
||||
n = 0
|
||||
active_keys: list[str] = []
|
||||
conn = _connect(db_path)
|
||||
try:
|
||||
for t in trades or []:
|
||||
if not isinstance(t, dict):
|
||||
continue
|
||||
hk = str(t.get("history_key") or "").strip()
|
||||
if not hk:
|
||||
continue
|
||||
if int(t.get("excluded_as_hedge_leg") or 0):
|
||||
continue
|
||||
active_keys.append(hk)
|
||||
opened_at = t.get("opened_at")
|
||||
closed_at = t.get("closed_at")
|
||||
opened_ms = t.get("opened_at_ms") or parse_wall_clock_ms(opened_at)
|
||||
closed_ms = t.get("closed_at_ms") or parse_wall_clock_ms(closed_at)
|
||||
entry = t.get("entry") if isinstance(t.get("entry"), dict) else {}
|
||||
strategy_tag = t.get("strategy_tag") or (entry or {}).get("strategy_tag")
|
||||
result_tag = t.get("result_tag") or (entry or {}).get("result_tag")
|
||||
reviewed = 1 if t.get("reviewed") or entry else 0
|
||||
row = dict(t)
|
||||
row["exchange_key"] = ex_k
|
||||
payload = json.dumps(row, ensure_ascii=False, default=str)
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO archive_options_trade_cache (
|
||||
exchange_key, history_key, source_type, underlying,
|
||||
opened_at, closed_at, opened_at_ms, closed_at_ms, hold_seconds,
|
||||
realized_pnl_total, status_raw,
|
||||
pos_id, inst_id, opt_type, strike, exp_time, sheets,
|
||||
open_avg, close_avg, premium_paid, realized_pnl,
|
||||
hedge_plan_id, plan_close_reason, realized_pnl_perp, realized_pnl_options,
|
||||
premium_total, direction, tp, sl, target_price, target_price_up, target_price_down,
|
||||
legs_json, linked_hedge_plan_id, excluded_as_hedge_leg,
|
||||
strategy_tag, result_tag, reviewed, source_label,
|
||||
payload_json, synced_at
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
ON CONFLICT(exchange_key, history_key) DO UPDATE SET
|
||||
source_type=excluded.source_type,
|
||||
underlying=excluded.underlying,
|
||||
opened_at=excluded.opened_at,
|
||||
closed_at=excluded.closed_at,
|
||||
opened_at_ms=excluded.opened_at_ms,
|
||||
closed_at_ms=excluded.closed_at_ms,
|
||||
hold_seconds=excluded.hold_seconds,
|
||||
realized_pnl_total=excluded.realized_pnl_total,
|
||||
status_raw=excluded.status_raw,
|
||||
pos_id=excluded.pos_id,
|
||||
inst_id=excluded.inst_id,
|
||||
opt_type=excluded.opt_type,
|
||||
strike=excluded.strike,
|
||||
exp_time=excluded.exp_time,
|
||||
sheets=excluded.sheets,
|
||||
open_avg=excluded.open_avg,
|
||||
close_avg=excluded.close_avg,
|
||||
premium_paid=excluded.premium_paid,
|
||||
realized_pnl=excluded.realized_pnl,
|
||||
hedge_plan_id=excluded.hedge_plan_id,
|
||||
plan_close_reason=excluded.plan_close_reason,
|
||||
realized_pnl_perp=excluded.realized_pnl_perp,
|
||||
realized_pnl_options=excluded.realized_pnl_options,
|
||||
premium_total=excluded.premium_total,
|
||||
direction=excluded.direction,
|
||||
tp=excluded.tp,
|
||||
sl=excluded.sl,
|
||||
target_price=excluded.target_price,
|
||||
target_price_up=excluded.target_price_up,
|
||||
target_price_down=excluded.target_price_down,
|
||||
legs_json=excluded.legs_json,
|
||||
linked_hedge_plan_id=excluded.linked_hedge_plan_id,
|
||||
excluded_as_hedge_leg=excluded.excluded_as_hedge_leg,
|
||||
strategy_tag=excluded.strategy_tag,
|
||||
result_tag=excluded.result_tag,
|
||||
reviewed=excluded.reviewed,
|
||||
source_label=excluded.source_label,
|
||||
payload_json=excluded.payload_json,
|
||||
synced_at=excluded.synced_at
|
||||
""",
|
||||
(
|
||||
ex_k,
|
||||
hk,
|
||||
t.get("source_type"),
|
||||
t.get("underlying"),
|
||||
opened_at,
|
||||
closed_at,
|
||||
int(opened_ms) if opened_ms else None,
|
||||
int(closed_ms) if closed_ms else None,
|
||||
_optional_int(t.get("hold_seconds")),
|
||||
float(t.get("realized_pnl_total") or t.get("realized_pnl") or 0),
|
||||
t.get("status_raw"),
|
||||
t.get("pos_id"),
|
||||
t.get("inst_id"),
|
||||
t.get("opt_type"),
|
||||
_optional_float(t.get("strike")),
|
||||
t.get("exp_time"),
|
||||
_optional_int(t.get("sheets")),
|
||||
_optional_float(t.get("open_avg")),
|
||||
_optional_float(t.get("close_avg")),
|
||||
_optional_float(t.get("premium_paid")),
|
||||
_optional_float(t.get("realized_pnl")),
|
||||
_optional_int(t.get("hedge_plan_id")),
|
||||
t.get("plan_close_reason"),
|
||||
_optional_float(t.get("realized_pnl_perp")),
|
||||
_optional_float(t.get("realized_pnl_options")),
|
||||
_optional_float(t.get("premium_total")),
|
||||
t.get("direction"),
|
||||
_optional_float(t.get("tp")),
|
||||
_optional_float(t.get("sl")),
|
||||
_optional_float(t.get("target_price")),
|
||||
_optional_float(t.get("target_price_up")),
|
||||
_optional_float(t.get("target_price_down")),
|
||||
t.get("legs_json")
|
||||
if isinstance(t.get("legs_json"), str)
|
||||
else (json.dumps(t.get("legs"), ensure_ascii=False) if t.get("legs") else None),
|
||||
_optional_int(t.get("linked_hedge_plan_id")),
|
||||
int(t.get("excluded_as_hedge_leg") or 0),
|
||||
strategy_tag,
|
||||
result_tag,
|
||||
reviewed,
|
||||
t.get("source_label"),
|
||||
payload,
|
||||
now,
|
||||
),
|
||||
)
|
||||
n += 1
|
||||
finally:
|
||||
conn.close()
|
||||
removed = 0
|
||||
if prune_missing:
|
||||
removed = purge_stale_options_trades_cache(ex_k, active_keys, db_path=db_path)
|
||||
return {"upserted": n, "removed": removed}
|
||||
|
||||
|
||||
def _options_row_to_dict(row: Any) -> dict[str, Any]:
|
||||
out: dict[str, Any] = dict(row)
|
||||
payload = {}
|
||||
raw = out.get("payload_json")
|
||||
if raw:
|
||||
try:
|
||||
payload = json.loads(raw) if isinstance(raw, str) else {}
|
||||
except (TypeError, ValueError, json.JSONDecodeError):
|
||||
payload = {}
|
||||
if isinstance(payload, dict):
|
||||
for k, v in payload.items():
|
||||
if k not in out or out.get(k) in (None, ""):
|
||||
out[k] = v
|
||||
pnl = float(out.get("realized_pnl_total") or out.get("realized_pnl") or 0)
|
||||
out["realized_pnl_total"] = pnl
|
||||
out["pnl_amount"] = pnl # 复用永续统计/日历字段名
|
||||
hold_sec = out.get("hold_seconds")
|
||||
if hold_sec is not None:
|
||||
try:
|
||||
out["hold_minutes"] = round(float(hold_sec) / 60.0, 2)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
if not out.get("opened_at_ms") and out.get("opened_at"):
|
||||
ms = parse_wall_clock_ms(out.get("opened_at"))
|
||||
if ms:
|
||||
out["opened_at_ms"] = int(ms)
|
||||
if not out.get("closed_at_ms") and out.get("closed_at"):
|
||||
ms = parse_wall_clock_ms(out.get("closed_at"))
|
||||
if ms:
|
||||
out["closed_at_ms"] = int(ms)
|
||||
out["trade_id"] = out.get("history_key")
|
||||
out["id"] = out.get("history_key")
|
||||
out["symbol"] = out.get("inst_id") or out.get("underlying") or ""
|
||||
return out
|
||||
|
||||
|
||||
def _empty_options_stats() -> dict[str, Any]:
|
||||
return {
|
||||
"open_count": 0,
|
||||
"sick_count": 0,
|
||||
"sick_pct": 0.0,
|
||||
"pnl_total": 0.0,
|
||||
"pnl_ex_sick": 0.0,
|
||||
"win_count": 0,
|
||||
"loss_count": 0,
|
||||
"avg_win": 0.0,
|
||||
"avg_loss": 0.0,
|
||||
"max_win": 0.0,
|
||||
"max_loss": 0.0,
|
||||
"win_rate": 0.0,
|
||||
"profit_loss_ratio": 0.0,
|
||||
"turnover_total": 0.0,
|
||||
"commission_total": 0.0,
|
||||
"premium_total": 0.0,
|
||||
"by_exchange": {},
|
||||
"by_source_type": {},
|
||||
}
|
||||
|
||||
|
||||
def _compute_options_period_stats(trade_rows: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
st = _empty_options_stats()
|
||||
wins: list[float] = []
|
||||
losses: list[float] = []
|
||||
by_ex: dict[str, dict[str, Any]] = {}
|
||||
by_src: dict[str, dict[str, Any]] = {}
|
||||
|
||||
def bucket() -> dict[str, Any]:
|
||||
return {
|
||||
"open_count": 0,
|
||||
"pnl_total": 0.0,
|
||||
"win_count": 0,
|
||||
"loss_count": 0,
|
||||
"premium_total": 0.0,
|
||||
}
|
||||
|
||||
for td in trade_rows:
|
||||
pnl = float(td.get("pnl_amount") or td.get("realized_pnl_total") or 0)
|
||||
ex = str(td.get("exchange_key") or "okx")
|
||||
src = str(td.get("source_type") or td.get("source_label") or "?")
|
||||
prem = float(td.get("premium_total") or td.get("premium_paid") or 0)
|
||||
st["open_count"] += 1
|
||||
st["pnl_total"] += pnl
|
||||
st["premium_total"] += prem
|
||||
if pnl > 0.0001:
|
||||
st["win_count"] += 1
|
||||
wins.append(pnl)
|
||||
elif pnl < -0.0001:
|
||||
st["loss_count"] += 1
|
||||
losses.append(pnl)
|
||||
if ex not in by_ex:
|
||||
by_ex[ex] = bucket()
|
||||
by_ex[ex]["open_count"] += 1
|
||||
by_ex[ex]["pnl_total"] += pnl
|
||||
by_ex[ex]["premium_total"] += prem
|
||||
if pnl > 0.0001:
|
||||
by_ex[ex]["win_count"] += 1
|
||||
elif pnl < -0.0001:
|
||||
by_ex[ex]["loss_count"] += 1
|
||||
if src not in by_src:
|
||||
by_src[src] = bucket()
|
||||
by_src[src]["open_count"] += 1
|
||||
by_src[src]["pnl_total"] += pnl
|
||||
|
||||
total = int(st["open_count"] or 0)
|
||||
st["pnl_ex_sick"] = round(float(st["pnl_total"]), 4)
|
||||
st["pnl_total"] = round(float(st["pnl_total"]), 4)
|
||||
st["premium_total"] = round(float(st["premium_total"]), 4)
|
||||
st["avg_win"] = round(sum(wins) / len(wins), 4) if wins else 0.0
|
||||
st["avg_loss"] = round(sum(losses) / len(losses), 4) if losses else 0.0
|
||||
st["max_win"] = round(max(wins), 4) if wins else 0.0
|
||||
st["max_loss"] = round(min(losses), 4) if losses else 0.0
|
||||
st["win_rate"] = round(st["win_count"] / total * 100, 1) if total else 0.0
|
||||
if wins and losses and abs(st["avg_loss"]) > 1e-9:
|
||||
st["profit_loss_ratio"] = round(abs(st["avg_win"] / st["avg_loss"]), 2)
|
||||
for ex, b in by_ex.items():
|
||||
b["pnl_total"] = round(float(b["pnl_total"]), 4)
|
||||
b["premium_total"] = round(float(b["premium_total"]), 4)
|
||||
b["sick_count"] = 0
|
||||
b["sick_pct"] = 0.0
|
||||
b["pnl_ex_sick"] = b["pnl_total"]
|
||||
b["avg_win"] = 0.0
|
||||
b["avg_loss"] = 0.0
|
||||
b["max_win"] = 0.0
|
||||
b["max_loss"] = 0.0
|
||||
b["win_rate"] = (
|
||||
round(b["win_count"] / b["open_count"] * 100, 1) if b["open_count"] else 0.0
|
||||
)
|
||||
b["profit_loss_ratio"] = 0.0
|
||||
b["turnover_total"] = 0.0
|
||||
b["commission_total"] = 0.0
|
||||
for src, b in by_src.items():
|
||||
b["pnl_total"] = round(float(b["pnl_total"]), 4)
|
||||
st["by_exchange"] = by_ex
|
||||
st["by_source_type"] = by_src
|
||||
return st
|
||||
|
||||
|
||||
def list_daily_options_trades(
|
||||
trading_day: str = "",
|
||||
*,
|
||||
period: str = "",
|
||||
date_from: str = "",
|
||||
date_to: str = "",
|
||||
exchange_key: str = "",
|
||||
filter_profit: bool = False,
|
||||
filter_loss: bool = False,
|
||||
search: str = "",
|
||||
source_type: str = "",
|
||||
db_path: Path | None = None,
|
||||
) -> dict[str, Any]:
|
||||
init_options_archive_db(db_path)
|
||||
p = (period or "today").strip().lower() or "today"
|
||||
start_ms, end_ms, df, dt, period_label = resolve_period_bounds(
|
||||
period=p,
|
||||
trading_day=trading_day,
|
||||
date_from=date_from,
|
||||
date_to=date_to,
|
||||
)
|
||||
ex_filter = (exchange_key or "").strip().lower()
|
||||
src_filter = (source_type or "").strip().lower()
|
||||
conn = _connect(db_path)
|
||||
try:
|
||||
params: list[Any] = [start_ms, end_ms]
|
||||
where = "closed_at_ms IS NOT NULL AND closed_at_ms >= ? AND closed_at_ms < ?"
|
||||
where += " AND COALESCE(excluded_as_hedge_leg,0)=0"
|
||||
if ex_filter:
|
||||
where += " AND exchange_key=?"
|
||||
params.append(ex_filter)
|
||||
if src_filter:
|
||||
where += " AND LOWER(COALESCE(source_type,''))=?"
|
||||
params.append(src_filter)
|
||||
rows = conn.execute(
|
||||
f"""
|
||||
SELECT * FROM archive_options_trade_cache
|
||||
WHERE {where}
|
||||
ORDER BY closed_at_ms DESC, history_key DESC
|
||||
""",
|
||||
params,
|
||||
).fetchall()
|
||||
trades: list[dict[str, Any]] = []
|
||||
q = (search or "").strip().lower()
|
||||
for r in rows:
|
||||
td = _options_row_to_dict(r)
|
||||
pnl = float(td.get("pnl_amount") or 0)
|
||||
if filter_profit and pnl <= 0.0001:
|
||||
continue
|
||||
if filter_loss and pnl >= -0.0001:
|
||||
continue
|
||||
if q:
|
||||
blob = " ".join(
|
||||
str(td.get(k) or "")
|
||||
for k in (
|
||||
"underlying",
|
||||
"inst_id",
|
||||
"exchange_key",
|
||||
"source_type",
|
||||
"source_label",
|
||||
"opt_type",
|
||||
"strategy_tag",
|
||||
"result_tag",
|
||||
"direction",
|
||||
)
|
||||
).lower()
|
||||
if q not in blob:
|
||||
continue
|
||||
trades.append(td)
|
||||
return {
|
||||
"period": p,
|
||||
"period_label": period_label,
|
||||
"trading_day": dt,
|
||||
"date_from": df,
|
||||
"date_to": dt,
|
||||
"product": "options",
|
||||
"trades": trades,
|
||||
"stats": _compute_options_period_stats(trades),
|
||||
}
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def list_archive_options_calendar(
|
||||
year: int,
|
||||
month: int,
|
||||
*,
|
||||
exchange_key: str = "",
|
||||
db_path: Path | None = None,
|
||||
reset_hour: int = TRADING_DAY_RESET_HOUR,
|
||||
) -> dict[str, Any]:
|
||||
init_options_archive_db(db_path)
|
||||
y = int(year)
|
||||
m = int(month)
|
||||
if m < 1 or m > 12:
|
||||
raise ValueError("month 无效")
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
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")
|
||||
start_ms, _ = trading_day_bounds_ms(first, reset_hour=reset_hour)
|
||||
_, end_ms = trading_day_bounds_ms(last, reset_hour=reset_hour)
|
||||
ex_filter = (exchange_key or "").strip().lower()
|
||||
conn = _connect(db_path)
|
||||
try:
|
||||
params: list[Any] = [start_ms, end_ms]
|
||||
where = (
|
||||
"closed_at_ms IS NOT NULL AND closed_at_ms >= ? AND closed_at_ms < ?"
|
||||
" AND COALESCE(excluded_as_hedge_leg,0)=0"
|
||||
)
|
||||
if ex_filter:
|
||||
where += " AND exchange_key=?"
|
||||
params.append(ex_filter)
|
||||
rows = conn.execute(
|
||||
f"SELECT * FROM archive_options_trade_cache WHERE {where}",
|
||||
params,
|
||||
).fetchall()
|
||||
days: dict[str, dict[str, Any]] = {}
|
||||
for r in rows:
|
||||
td = _options_row_to_dict(r)
|
||||
closed_ms = td.get("closed_at_ms") or parse_wall_clock_ms(td.get("closed_at"))
|
||||
if not closed_ms:
|
||||
continue
|
||||
day = ms_to_trading_day(int(closed_ms), reset_hour=reset_hour)
|
||||
if not day or day < first or day > last:
|
||||
continue
|
||||
bucket = days.setdefault(
|
||||
day,
|
||||
{
|
||||
"trading_day": day,
|
||||
"open_count": 0,
|
||||
"sick_count": 0,
|
||||
"pnl_total": 0.0,
|
||||
"turnover_total": 0.0,
|
||||
"commission_total": 0.0,
|
||||
"has_sick": False,
|
||||
},
|
||||
)
|
||||
bucket["open_count"] += 1
|
||||
bucket["pnl_total"] += float(td.get("pnl_amount") or 0)
|
||||
for d in days.values():
|
||||
d["pnl_total"] = round(float(d["pnl_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,
|
||||
"product": "options",
|
||||
"days": days,
|
||||
"month_pnl_total": round(month_pnl, 4),
|
||||
"month_open_count": month_count,
|
||||
}
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def sync_options_exchange_archive(
|
||||
exchange_key: str,
|
||||
trades: list[dict[str, Any]],
|
||||
*,
|
||||
db_path: Path | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""仅缓存期权交易,不做 K 线."""
|
||||
r = upsert_options_trades_cache(
|
||||
exchange_key, trades, db_path=db_path, prune_missing=True
|
||||
)
|
||||
return {
|
||||
"ok": True,
|
||||
"exchange_key": (exchange_key or "").strip().lower(),
|
||||
"product": "options",
|
||||
"trades_upserted": r.get("upserted", 0),
|
||||
"trades_removed": r.get("removed", 0),
|
||||
"trade_count": len(trades or []),
|
||||
}
|
||||
@@ -10,12 +10,24 @@ from typing import Any
|
||||
|
||||
from lib.paths import REPO_ROOT
|
||||
|
||||
STRATEGY_EXCHANGES: tuple[str, ...] = ("playbook", "behavior", "binance", "okx", "gate")
|
||||
STRATEGY_EXCHANGES: tuple[str, ...] = (
|
||||
"playbook_v2",
|
||||
"playbook",
|
||||
"behavior",
|
||||
"binance",
|
||||
"okx",
|
||||
"gate",
|
||||
)
|
||||
|
||||
STRATEGY_META: dict[str, dict[str, str]] = {
|
||||
"playbook_v2": {
|
||||
"label": "执行手册v2",
|
||||
"title": "交易执行手册 v2(期权 / 合约 · 无对冲)",
|
||||
"md_rel": "docs/交易执行手册-v2-期权与合约.md",
|
||||
},
|
||||
"playbook": {
|
||||
"label": "执行手册",
|
||||
"title": "交易执行手册(期权为主 · Gate 为辅)",
|
||||
"label": "执行手册v1",
|
||||
"title": "交易执行手册 v1(期权为主 · Gate 为辅 · 含对冲)",
|
||||
# 相对仓库根;其余条目用 md_file 相对 docs/strategy
|
||||
"md_rel": "docs/交易执行手册-期权与Gate.md",
|
||||
},
|
||||
@@ -224,11 +236,30 @@ def load_strategy_payload(exchange_key: str) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def strategy_meta_payload() -> dict[str, Any]:
|
||||
tabs = [
|
||||
{"key": k, "label": STRATEGY_META[k]["label"], "title": STRATEGY_META[k]["title"]}
|
||||
for k in STRATEGY_EXCHANGES
|
||||
]
|
||||
_STRATEGY_TAB_DISPLAY_PREF: dict[str, str] = {
|
||||
"playbook_v2": "show_strategy_playbook_v2",
|
||||
"playbook": "show_strategy_playbook",
|
||||
"behavior": "show_strategy_behavior",
|
||||
"binance": "show_strategy_binance",
|
||||
"okx": "show_strategy_okx",
|
||||
"gate": "show_strategy_gate",
|
||||
}
|
||||
|
||||
|
||||
def strategy_meta_payload(display: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
prefs = display if isinstance(display, dict) else {}
|
||||
tabs = []
|
||||
for k in STRATEGY_EXCHANGES:
|
||||
pref_key = _STRATEGY_TAB_DISPLAY_PREF.get(k)
|
||||
if pref_key and prefs.get(pref_key) is False:
|
||||
continue
|
||||
tabs.append(
|
||||
{
|
||||
"key": k,
|
||||
"label": STRATEGY_META[k]["label"],
|
||||
"title": STRATEGY_META[k]["title"],
|
||||
}
|
||||
)
|
||||
return {"ok": True, "exchanges": tabs}
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ DISPLAY_RUNTIME_PREFIX = "display."
|
||||
|
||||
DEFAULT_INSTANCE_DISPLAY: dict[str, bool] = {
|
||||
"show_nav_dashboard": False,
|
||||
"show_nav_key_monitor": True,
|
||||
"show_nav_trade": True,
|
||||
"show_nav_strategy": True,
|
||||
"show_nav_strategy_records": True,
|
||||
"show_nav_records": True,
|
||||
@@ -28,6 +30,8 @@ DEFAULT_INSTANCE_DISPLAY: dict[str, bool] = {
|
||||
|
||||
DISPLAY_LABELS: dict[str, str] = {
|
||||
"show_nav_dashboard": "数据看板",
|
||||
"show_nav_key_monitor": "关键位监控",
|
||||
"show_nav_trade": "实盘下单",
|
||||
"show_nav_strategy": "策略交易",
|
||||
"show_nav_strategy_records": "策略交易记录",
|
||||
"show_nav_records": "交易记录与复盘",
|
||||
@@ -47,6 +51,8 @@ DISPLAY_LABELS: dict[str, str] = {
|
||||
|
||||
NAV_TAB_ALLOWED: dict[str, str] = {
|
||||
"dashboard": "show_nav_dashboard",
|
||||
"key_monitor": "show_nav_key_monitor",
|
||||
"trade": "show_nav_trade",
|
||||
"strategy": "show_nav_strategy",
|
||||
"strategy_records": "show_nav_strategy_records",
|
||||
"records": "show_nav_records",
|
||||
@@ -110,6 +116,8 @@ def tab_allowed(tab: str, display: Optional[dict[str, bool]] = None) -> bool:
|
||||
def display_meta_for_ui() -> list[dict[str, Any]]:
|
||||
nav_keys = [
|
||||
"show_nav_dashboard",
|
||||
"show_nav_key_monitor",
|
||||
"show_nav_trade",
|
||||
"show_nav_strategy",
|
||||
"show_nav_strategy_records",
|
||||
"show_nav_records",
|
||||
|
||||
@@ -84,6 +84,11 @@ def embed_shell_enabled() -> bool:
|
||||
return (os.getenv("HUB_EMBED_SHELL") or "1").strip().lower() in ("1", "true", "yes", "on")
|
||||
|
||||
|
||||
_SETTINGS_SUB_TABS = frozenset(
|
||||
{"nav", "password", "transfer", "export", "options_swap", "options_transfer"}
|
||||
)
|
||||
|
||||
|
||||
def redirect_to_embed_shell_if_enabled(page: str):
|
||||
"""直连 /trade 等整页路由时,重定向到 embed 壳(顶栏常驻,tab 软切换)."""
|
||||
if not embed_shell_enabled():
|
||||
@@ -93,6 +98,12 @@ def redirect_to_embed_shell_if_enabled(page: str):
|
||||
if (request.path or "").rstrip("/") == "/embed":
|
||||
return None
|
||||
q = {k: v for k, v in request.args.items()}
|
||||
# embed 的 tab=页面名;系统设置内页签用 settings_tab,避免 /settings?tab=transfer 被覆盖成 tab=settings
|
||||
if (page or "").strip() == "settings":
|
||||
sub = (q.get("settings_tab") or "").strip()
|
||||
legacy = (q.get("tab") or "").strip()
|
||||
if not sub and legacy in _SETTINGS_SUB_TABS:
|
||||
q["settings_tab"] = legacy
|
||||
q["tab"] = page
|
||||
q["embed"] = "1"
|
||||
return redirect("/embed?" + urlencode(q))
|
||||
@@ -115,6 +126,11 @@ def rewrite_embed_dest(path: str, hub_theme: str | None = None) -> str:
|
||||
tab = path_to_embed_tab(split.path)
|
||||
q = dict(parse_qsl(split.query, keep_blank_values=True))
|
||||
if tab:
|
||||
if tab == "settings":
|
||||
sub = (q.get("settings_tab") or "").strip()
|
||||
legacy = (q.get("tab") or "").strip()
|
||||
if not sub and legacy in _SETTINGS_SUB_TABS:
|
||||
q["settings_tab"] = legacy
|
||||
q["tab"] = tab
|
||||
q["embed"] = "1"
|
||||
ht = (hub_theme or q.get("hub_theme") or "").strip().lower()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{# 系统设置 · 导航显示开关(SSR 预渲染,保存仍走 API) #}
|
||||
<div class="settings-tab-inner" id="display-prefs-card">
|
||||
<h2>导航显示</h2>
|
||||
<p class="settings-env-hint">以下开关控制顶栏导航与系统设置内区块是否显示,保存后立即生效.关键位监控,实盘下单,系统设置为固定项.</p>
|
||||
<p class="settings-env-hint">以下开关控制顶栏导航与系统设置内区块是否显示,保存后立即生效.系统设置为固定项.</p>
|
||||
<div id="display-prefs-form" class="display-prefs-form" data-prefs-ssr="1">
|
||||
{% if display_meta %}
|
||||
{% for group in display_meta %}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<link rel="stylesheet" href="/static/instance_theme_early.css?v=4">
|
||||
<link rel="stylesheet" href="/static/account_risk_badge.css?v=4">
|
||||
<link rel="stylesheet" href="/static/instance_page.css?v=11">
|
||||
<link rel="stylesheet" href="/static/instance_theme.css?v=105">
|
||||
<link rel="stylesheet" href="/static/instance_theme.css?v=108">
|
||||
<script src="/static/account_risk_badge.js?v=4"></script>
|
||||
<meta name="theme-color" content="#0b0d14">
|
||||
<title>{{ pwa_app_name }}</title>
|
||||
@@ -31,8 +31,8 @@
|
||||
</div>
|
||||
<nav class="top-nav embed-top-nav" aria-label="实例导航">
|
||||
<a href="/dashboard" data-embed-tab="dashboard" class="{% if initial_tab == 'dashboard' %}active{% endif %}"{% if not display.show_nav_dashboard %} style="display:none"{% endif %}>数据看板</a>
|
||||
<a href="/key_monitor" data-embed-tab="key_monitor" class="{% if initial_tab == 'key_monitor' %}active{% endif %}">关键位监控</a>
|
||||
<a href="/trade" data-embed-tab="trade" class="{% if initial_tab == 'trade' %}active{% endif %}">实盘下单</a>
|
||||
<a href="/key_monitor" data-embed-tab="key_monitor" class="{% if initial_tab == 'key_monitor' %}active{% endif %}"{% if not display.show_nav_key_monitor %} style="display:none"{% endif %}>关键位监控</a>
|
||||
<a href="/trade" data-embed-tab="trade" class="{% if initial_tab == 'trade' %}active{% endif %}"{% if not display.show_nav_trade %} style="display:none"{% endif %}>实盘下单</a>
|
||||
{% if not intraday_discipline and display.show_nav_strategy %}
|
||||
<a href="/strategy" data-embed-tab="strategy" class="{% if initial_tab == 'strategy' %}active{% endif %}">策略交易</a>
|
||||
{% endif %}
|
||||
@@ -93,6 +93,54 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 手机端主导航(≤720px);桌面不显示 -->
|
||||
<nav id="inst-mobile-tabbar" class="inst-mobile-tabbar" aria-label="手机主导航">
|
||||
<a href="/trade" class="inst-m-tab{% if initial_tab == 'trade' %} active{% endif %}" data-embed-tab="trade"{% if not display.show_nav_trade %} style="display:none"{% endif %}>下单</a>
|
||||
<a href="/key_monitor" class="inst-m-tab{% if initial_tab == 'key_monitor' %} active{% endif %}" data-embed-tab="key_monitor"{% if not display.show_nav_key_monitor %} style="display:none"{% endif %}>关键位</a>
|
||||
{% if options_nav_visible and display.show_nav_options %}
|
||||
<a href="/options" class="inst-m-tab{% if initial_tab == 'options' %} active{% endif %}" data-embed-tab="options">期权</a>
|
||||
{% endif %}
|
||||
<button type="button" class="inst-m-tab" data-embed-tab="more" id="inst-m-tab-more" aria-haspopup="dialog" aria-expanded="false">更多</button>
|
||||
</nav>
|
||||
<div id="inst-mobile-more" class="inst-mobile-more" aria-hidden="true">
|
||||
<div class="inst-mobile-more-backdrop" id="inst-mobile-more-backdrop"></div>
|
||||
<div class="inst-mobile-more-sheet" role="dialog" aria-modal="true" aria-labelledby="inst-mobile-more-title">
|
||||
<div class="inst-mobile-more-handle" aria-hidden="true"></div>
|
||||
<h2 id="inst-mobile-more-title" class="inst-mobile-more-title">更多</h2>
|
||||
<p class="inst-mobile-more-hint">次要页面 · 完整界面请用电脑</p>
|
||||
<nav class="inst-mobile-more-nav" aria-label="更多页面">
|
||||
<a href="/dashboard" data-embed-tab="dashboard"{% if not display.show_nav_dashboard %} style="display:none"{% endif %}>数据看板</a>
|
||||
{% if display.show_nav_records %}
|
||||
<a href="/records" data-embed-tab="records">交易记录</a>
|
||||
{% endif %}
|
||||
{% if not intraday_discipline and display.show_nav_strategy %}
|
||||
<a href="/strategy" data-embed-tab="strategy">策略交易</a>
|
||||
{% endif %}
|
||||
{% if not intraday_discipline and display.show_nav_strategy_records %}
|
||||
<a href="/strategy/records" data-embed-tab="strategy_records">策略记录</a>
|
||||
{% endif %}
|
||||
{% if display.show_nav_stats %}
|
||||
<a href="/stats" data-embed-tab="stats">统计分析</a>
|
||||
{% endif %}
|
||||
{% if options_nav_visible and display.show_nav_options_review %}
|
||||
<a href="/options/review" data-embed-tab="options_review">期权复盘</a>
|
||||
{% endif %}
|
||||
{% if hedge_plan_nav_visible and display.show_nav_hedge_plan %}
|
||||
<a href="/hedge-plan" data-embed-tab="hedge_plan">对冲计划</a>
|
||||
{% endif %}
|
||||
{% if display.show_nav_risk_policy %}
|
||||
<a href="/risk_policy" data-embed-tab="risk_policy">风控说明</a>
|
||||
{% endif %}
|
||||
<a href="/system_guide" data-embed-tab="system_guide"{% if not display.show_nav_system_guide %} style="display:none"{% endif %}>系统说明</a>
|
||||
{% if display.show_nav_env_config %}
|
||||
<a href="/env_config" data-embed-tab="env_config">env配置</a>
|
||||
{% endif %}
|
||||
<a href="/settings" data-embed-tab="settings">系统设置</a>
|
||||
</nav>
|
||||
<button type="button" class="inst-mobile-more-close" id="inst-mobile-more-close">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/instance_ui.js?v=10"></script>
|
||||
<script src="/static/journal_upload_slots.js?v=4"></script>
|
||||
<script src="/static/instance_records_mobile.js?v=2"></script>
|
||||
@@ -118,8 +166,9 @@ const ORDER_ENTRY_MODEL_CODE_TO_CATEGORY = {{ entry_model_code_to_category | toj
|
||||
<script>
|
||||
window.__INSTANCE_DISPLAY__ = {{ display | tojson }};
|
||||
</script>
|
||||
<script src="/static/instance_settings_prefs.js?v=15"></script>
|
||||
<script src="/static/instance_settings_prefs.js?v=16"></script>
|
||||
<script src="/static/instance_live.js?v=6"></script>
|
||||
<script src="/static/instance_embed.js?v=27"></script>
|
||||
<script src="/static/instance_embed.js?v=29"></script>
|
||||
<script src="/static/instance_mobile_nav.js?v=2"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -118,8 +118,8 @@
|
||||
</div>
|
||||
<div class="top-nav">
|
||||
<a href="/dashboard" data-embed-tab="dashboard" class="{% if page == 'dashboard' %}active{% endif %}"{% if not display.show_nav_dashboard %} style="display:none"{% endif %}>数据看板</a>
|
||||
<a href="/key_monitor" class="{% if page == 'key_monitor' %}active{% endif %}">关键位监控</a>
|
||||
<a href="/trade" class="{% if page == 'trade' %}active{% endif %}">实盘下单</a>
|
||||
<a href="/key_monitor" class="{% if page == 'key_monitor' %}active{% endif %}"{% if not display.show_nav_key_monitor %} style="display:none"{% endif %}>关键位监控</a>
|
||||
<a href="/trade" class="{% if page == 'trade' %}active{% endif %}"{% if not display.show_nav_trade %} style="display:none"{% endif %}>实盘下单</a>
|
||||
{% if not intraday_discipline and display.show_nav_strategy %}
|
||||
<a href="/strategy" class="{% if page in ('strategy', 'strategy_trend', 'strategy_roll') %}active{% endif %}">策略交易</a>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{# 统一顶栏:状态 + 筛选(上)· 统计条(下) #}
|
||||
<div class="instance-header-panel card">
|
||||
<div class="instance-header-toolbar">
|
||||
<div class="instance-header-toolbar-filter">
|
||||
<div class="instance-header-toolbar-filter instance-desktop-only">
|
||||
<span class="list-window-label" title="列表按 UTC 时间筛选,默认本月">UTC {{ list_window.label }}</span>
|
||||
<label class="list-window-preset">预设
|
||||
<select id="win-preset-select" onchange="toggleListWindowCustom()">
|
||||
@@ -37,4 +37,18 @@
|
||||
<div class="instance-header-stats-wrap instance-desktop-only">
|
||||
{% include 'instance_header_stats.html' %}
|
||||
</div>
|
||||
<div class="instance-header-phone-strip instance-phone-only" aria-label="手机资金摘要">
|
||||
<span class="inst-phone-chip">
|
||||
<em>交易</em>
|
||||
<b data-funds-field="current-capital">{{ funds_fmt(current_capital) }}U</b>
|
||||
</span>
|
||||
<span class="inst-phone-chip">
|
||||
<em>资金</em>
|
||||
<b data-funds-field="total-capital">{% if funding_usdt is not none %}{{ funds_fmt(funding_usdt) }}U{% else %}—{% endif %}</b>
|
||||
</span>
|
||||
<span class="inst-phone-chip">
|
||||
<em>总资</em>
|
||||
<b data-funds-field="total-funds">{% if total_funds is not none %}{{ funds_fmt(total_funds) }}U{% else %}—{% endif %}</b>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,9 +6,21 @@
|
||||
</div>
|
||||
|
||||
{% if settings_tabs %}
|
||||
<div class="env-config-body card settings-config-body">
|
||||
{% set _sub = (request.args.get('settings_tab') or '').strip() %}
|
||||
{% set _legacy_tab = (request.args.get('tab') or '').strip() %}
|
||||
{% set ns = namespace(active_idx=0, active_key='') %}
|
||||
{% for tab in settings_tabs %}
|
||||
{% if _sub and tab.key == _sub %}
|
||||
{% set ns.active_idx = loop.index0 %}
|
||||
{% set ns.active_key = tab.key %}
|
||||
{% elif (not _sub) and _legacy_tab and tab.key == _legacy_tab %}
|
||||
{% set ns.active_idx = loop.index0 %}
|
||||
{% set ns.active_key = tab.key %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<div class="env-config-body card settings-config-body" data-settings-active-tab="{{ ns.active_key }}">
|
||||
{% for tab in settings_tabs %}
|
||||
<input type="radio" name="settings-section" id="settings-sec-{{ loop.index0 }}" class="env-tab-radio"{% if loop.first %} checked{% endif %}>
|
||||
<input type="radio" name="settings-section" id="settings-sec-{{ loop.index0 }}" class="env-tab-radio" data-settings-tab="{{ tab.key }}"{% if loop.index0 == ns.active_idx %} checked{% endif %}>
|
||||
{% endfor %}
|
||||
<div class="env-config-tabs" role="tablist" aria-label="系统设置分类">
|
||||
{% for tab in settings_tabs %}
|
||||
@@ -52,3 +64,14 @@
|
||||
{% include 'options_settings_panel.html' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
var q = new URLSearchParams(window.location.search || "");
|
||||
var key = (q.get("settings_tab") || "").trim();
|
||||
if (!key) return;
|
||||
var radio = document.querySelector('input.env-tab-radio[data-settings-tab="' + key + '"]');
|
||||
if (radio) radio.checked = true;
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -95,6 +95,14 @@ def init_options_tables(conn: sqlite3.Connection) -> None:
|
||||
ON options_target_monitors(status)
|
||||
"""
|
||||
)
|
||||
for ddl in (
|
||||
"ALTER TABLE options_trades ADD COLUMN wechat_open_sent INTEGER DEFAULT 0",
|
||||
"ALTER TABLE options_trades ADD COLUMN wechat_close_sent INTEGER DEFAULT 0",
|
||||
):
|
||||
try:
|
||||
conn.execute(ddl)
|
||||
except Exception:
|
||||
pass
|
||||
init_options_review_tables(conn)
|
||||
|
||||
|
||||
|
||||
@@ -237,6 +237,7 @@ def sync_open_options_trades(
|
||||
*,
|
||||
live_inst_ids: set[str],
|
||||
fetch_history_fn: Callable[[str], list[dict[str, Any]]],
|
||||
notify_cfg: dict[str, Any] | None = None,
|
||||
) -> int:
|
||||
"""
|
||||
交易所已无持仓时,将本地 open 记录同步为 closed.
|
||||
@@ -319,6 +320,24 @@ def sync_open_options_trades(
|
||||
),
|
||||
)
|
||||
updated += 1
|
||||
if notify_cfg is not None:
|
||||
try:
|
||||
from lib.options.options_notify_lib import notify_options_close
|
||||
|
||||
reason = "到期结算" if close_reason == "expired" else "交易所平仓"
|
||||
notify_options_close(
|
||||
notify_cfg,
|
||||
conn,
|
||||
inst_id=inst_id,
|
||||
reason=reason,
|
||||
trade_id=int(row["id"]),
|
||||
premium_paid=paid,
|
||||
premium_received=prem_recv,
|
||||
realized_pnl=realized_pnl,
|
||||
close_quote=close_quote,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return updated
|
||||
|
||||
|
||||
@@ -414,6 +433,7 @@ def options_monitor_loop(
|
||||
close_fn=target_close_fn,
|
||||
send_wechat=send_wechat,
|
||||
account_label=account_label,
|
||||
cfg={"send_wechat": send_wechat, "account_label": account_label},
|
||||
)
|
||||
if sync_trades_fn is not None:
|
||||
sync_trades_fn(conn)
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
"""OKX 期权开仓/平仓企业微信推送(必发,幂等落库标记)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
|
||||
def _fmt(v: Any, d: int = 4) -> str:
|
||||
try:
|
||||
if v is None or v == "":
|
||||
return "—"
|
||||
return f"{float(v):.{d}f}"
|
||||
except (TypeError, ValueError):
|
||||
return str(v)
|
||||
|
||||
|
||||
def _opt_type_label(opt_type: Any) -> str:
|
||||
t = str(opt_type or "").strip().upper()
|
||||
if t in ("C", "CALL"):
|
||||
return "Call"
|
||||
if t in ("P", "PUT"):
|
||||
return "Put"
|
||||
return t or "—"
|
||||
|
||||
|
||||
def ensure_options_notify_columns(conn: sqlite3.Connection) -> None:
|
||||
for ddl in (
|
||||
"ALTER TABLE options_trades ADD COLUMN wechat_open_sent INTEGER DEFAULT 0",
|
||||
"ALTER TABLE options_trades ADD COLUMN wechat_close_sent INTEGER DEFAULT 0",
|
||||
):
|
||||
try:
|
||||
conn.execute(ddl)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def notify_options_send(cfg: dict[str, Any], content: str) -> bool:
|
||||
send: Optional[Callable[[str], Any]] = cfg.get("send_wechat")
|
||||
if not callable(send):
|
||||
return False
|
||||
try:
|
||||
send(content)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def build_options_open_message(
|
||||
*,
|
||||
account_label: str,
|
||||
inst_id: str,
|
||||
underlying: str = "",
|
||||
opt_type: Any = None,
|
||||
sheets: Any = None,
|
||||
premium_paid: Any = None,
|
||||
open_quote: Any = None,
|
||||
target_index: Any = None,
|
||||
signal_note: str = "",
|
||||
trade_id: Any = None,
|
||||
) -> str:
|
||||
lines = [
|
||||
"【OKX期权·开仓】",
|
||||
f"账户:{account_label or 'OKX期权'}",
|
||||
]
|
||||
if trade_id is not None:
|
||||
lines.append(f"本地单号:#{trade_id}")
|
||||
lines.extend(
|
||||
[
|
||||
f"合约:{inst_id}",
|
||||
f"标的:{(underlying or '—')} · {_opt_type_label(opt_type)}",
|
||||
f"张数:{sheets if sheets is not None else '—'}",
|
||||
f"开仓报价:{_fmt(open_quote)} USDC",
|
||||
f"权利金:{_fmt(premium_paid)} USDC",
|
||||
]
|
||||
)
|
||||
if target_index is not None and str(target_index).strip() != "":
|
||||
try:
|
||||
lines.append(f"目标指数:{float(target_index):g}")
|
||||
except (TypeError, ValueError):
|
||||
lines.append(f"目标指数:{target_index}")
|
||||
if signal_note:
|
||||
lines.append(f"备注:{signal_note[:200]}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def build_options_close_message(
|
||||
*,
|
||||
account_label: str,
|
||||
inst_id: str,
|
||||
reason: str = "",
|
||||
underlying: str = "",
|
||||
opt_type: Any = None,
|
||||
sheets: Any = None,
|
||||
premium_paid: Any = None,
|
||||
premium_received: Any = None,
|
||||
realized_pnl: Any = None,
|
||||
close_quote: Any = None,
|
||||
target_index: Any = None,
|
||||
trigger_idx: Any = None,
|
||||
trade_id: Any = None,
|
||||
) -> str:
|
||||
lines = [
|
||||
"【OKX期权·平仓】",
|
||||
f"账户:{account_label or 'OKX期权'}",
|
||||
]
|
||||
if trade_id is not None:
|
||||
lines.append(f"本地单号:#{trade_id}")
|
||||
lines.extend(
|
||||
[
|
||||
f"合约:{inst_id}",
|
||||
f"标的:{(underlying or '—')} · {_opt_type_label(opt_type)}",
|
||||
f"原因:{(reason or '平仓').strip()}",
|
||||
f"张数:{sheets if sheets is not None else '—'}",
|
||||
f"平仓报价:{_fmt(close_quote)} USDC",
|
||||
f"已付/收回:{_fmt(premium_paid)} / {_fmt(premium_received)} USDC",
|
||||
f"实现盈亏:{_fmt(realized_pnl, 4)} USDC",
|
||||
]
|
||||
)
|
||||
if target_index is not None and str(target_index).strip() != "":
|
||||
try:
|
||||
lines.append(f"目标指数:{float(target_index):g}")
|
||||
except (TypeError, ValueError):
|
||||
lines.append(f"目标指数:{target_index}")
|
||||
if trigger_idx is not None and str(trigger_idx).strip() != "":
|
||||
try:
|
||||
lines.append(f"触发指数:{float(trigger_idx):g}")
|
||||
except (TypeError, ValueError):
|
||||
lines.append(f"触发指数:{trigger_idx}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def notify_options_open(
|
||||
cfg: dict[str, Any],
|
||||
conn: sqlite3.Connection | None,
|
||||
*,
|
||||
trade_id: int | None,
|
||||
inst_id: str,
|
||||
underlying: str = "",
|
||||
opt_type: Any = None,
|
||||
sheets: Any = None,
|
||||
premium_paid: Any = None,
|
||||
open_quote: Any = None,
|
||||
target_index: Any = None,
|
||||
signal_note: str = "",
|
||||
) -> bool:
|
||||
ensure_options_notify_columns(conn) if conn is not None else None
|
||||
if conn is not None and trade_id is not None:
|
||||
row = conn.execute(
|
||||
"SELECT wechat_open_sent FROM options_trades WHERE id=?",
|
||||
(int(trade_id),),
|
||||
).fetchone()
|
||||
if row and int(row["wechat_open_sent"] or 0):
|
||||
return False
|
||||
msg = build_options_open_message(
|
||||
account_label=str(cfg.get("account_label") or "OKX期权"),
|
||||
inst_id=inst_id,
|
||||
underlying=underlying,
|
||||
opt_type=opt_type,
|
||||
sheets=sheets,
|
||||
premium_paid=premium_paid,
|
||||
open_quote=open_quote,
|
||||
target_index=target_index,
|
||||
signal_note=signal_note,
|
||||
trade_id=trade_id,
|
||||
)
|
||||
ok = notify_options_send(cfg, msg)
|
||||
if ok and conn is not None and trade_id is not None:
|
||||
conn.execute(
|
||||
"UPDATE options_trades SET wechat_open_sent=1 WHERE id=?",
|
||||
(int(trade_id),),
|
||||
)
|
||||
try:
|
||||
conn.commit()
|
||||
except Exception:
|
||||
pass
|
||||
return ok
|
||||
|
||||
|
||||
def _load_trade_row(conn: sqlite3.Connection, trade_id: int) -> dict[str, Any] | None:
|
||||
row = conn.execute("SELECT * FROM options_trades WHERE id=?", (int(trade_id),)).fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
|
||||
def notify_options_close(
|
||||
cfg: dict[str, Any],
|
||||
conn: sqlite3.Connection | None,
|
||||
*,
|
||||
inst_id: str,
|
||||
reason: str = "平仓",
|
||||
trade_id: int | None = None,
|
||||
underlying: str = "",
|
||||
opt_type: Any = None,
|
||||
sheets: Any = None,
|
||||
premium_paid: Any = None,
|
||||
premium_received: Any = None,
|
||||
realized_pnl: Any = None,
|
||||
close_quote: Any = None,
|
||||
target_index: Any = None,
|
||||
trigger_idx: Any = None,
|
||||
force: bool = False,
|
||||
) -> bool:
|
||||
"""平仓必发.默认按 trade_id / 同合约未标记行幂等."""
|
||||
if conn is not None:
|
||||
ensure_options_notify_columns(conn)
|
||||
rows: list[dict[str, Any]] = []
|
||||
if conn is not None and trade_id is not None:
|
||||
r = _load_trade_row(conn, int(trade_id))
|
||||
if r:
|
||||
rows = [r]
|
||||
elif conn is not None and inst_id:
|
||||
q = conn.execute(
|
||||
"""
|
||||
SELECT * FROM options_trades
|
||||
WHERE inst_id=? AND status='closed'
|
||||
AND COALESCE(wechat_close_sent,0)=0
|
||||
ORDER BY id DESC
|
||||
LIMIT 20
|
||||
""",
|
||||
(inst_id,),
|
||||
).fetchall()
|
||||
rows = [dict(x) for x in q]
|
||||
if not rows and force:
|
||||
q2 = conn.execute(
|
||||
"""
|
||||
SELECT * FROM options_trades
|
||||
WHERE inst_id=? AND status='closed'
|
||||
ORDER BY id DESC LIMIT 1
|
||||
""",
|
||||
(inst_id,),
|
||||
).fetchone()
|
||||
if q2:
|
||||
rows = [dict(q2)]
|
||||
|
||||
if rows:
|
||||
# 同次平仓可能多腿:合并一条推送,逐条标记
|
||||
total_paid = sum(float(r.get("premium_paid") or 0) for r in rows)
|
||||
total_recv = sum(float(r.get("premium_received") or 0) for r in rows if r.get("premium_received") is not None)
|
||||
pnls = [float(r["realized_pnl"]) for r in rows if r.get("realized_pnl") is not None]
|
||||
total_pnl = sum(pnls) if pnls else None
|
||||
if total_pnl is None and (premium_received is not None or realized_pnl is not None):
|
||||
total_pnl = realized_pnl
|
||||
total_recv = premium_received if premium_received is not None else total_recv
|
||||
total_paid = premium_paid if premium_paid is not None else total_paid
|
||||
head = rows[0]
|
||||
pending = [r for r in rows if not int(r.get("wechat_close_sent") or 0)]
|
||||
if not pending and not force:
|
||||
return False
|
||||
msg = build_options_close_message(
|
||||
account_label=str(cfg.get("account_label") or "OKX期权"),
|
||||
inst_id=inst_id or str(head.get("inst_id") or ""),
|
||||
reason=reason,
|
||||
underlying=underlying or str(head.get("underlying") or ""),
|
||||
opt_type=opt_type or head.get("opt_type"),
|
||||
sheets=sheets if sheets is not None else sum(int(r.get("sheets") or 0) for r in rows),
|
||||
premium_paid=total_paid,
|
||||
premium_received=total_recv if rows else premium_received,
|
||||
realized_pnl=total_pnl,
|
||||
close_quote=close_quote if close_quote is not None else head.get("close_quote"),
|
||||
target_index=target_index,
|
||||
trigger_idx=trigger_idx,
|
||||
trade_id=head.get("id") if len(rows) == 1 else None,
|
||||
)
|
||||
ok = notify_options_send(cfg, msg)
|
||||
if ok and conn is not None:
|
||||
for r in pending or rows:
|
||||
conn.execute(
|
||||
"UPDATE options_trades SET wechat_close_sent=1 WHERE id=?",
|
||||
(int(r["id"]),),
|
||||
)
|
||||
try:
|
||||
conn.commit()
|
||||
except Exception:
|
||||
pass
|
||||
return ok
|
||||
|
||||
# 无库行时仍发一条(尽量不丢提醒)
|
||||
msg = build_options_close_message(
|
||||
account_label=str(cfg.get("account_label") or "OKX期权"),
|
||||
inst_id=inst_id,
|
||||
reason=reason,
|
||||
underlying=underlying,
|
||||
opt_type=opt_type,
|
||||
sheets=sheets,
|
||||
premium_paid=premium_paid,
|
||||
premium_received=premium_received,
|
||||
realized_pnl=realized_pnl,
|
||||
close_quote=close_quote,
|
||||
target_index=target_index,
|
||||
trigger_idx=trigger_idx,
|
||||
trade_id=trade_id,
|
||||
)
|
||||
return notify_options_send(cfg, msg)
|
||||
|
||||
|
||||
def notify_options_close_trade_ids(
|
||||
cfg: dict[str, Any],
|
||||
conn: sqlite3.Connection,
|
||||
trade_ids: list[int],
|
||||
*,
|
||||
reason: str,
|
||||
) -> bool:
|
||||
ids = [int(x) for x in trade_ids if x is not None]
|
||||
if not ids:
|
||||
return False
|
||||
ensure_options_notify_columns(conn)
|
||||
placeholders = ",".join("?" for _ in ids)
|
||||
rows = conn.execute(
|
||||
f"""
|
||||
SELECT * FROM options_trades
|
||||
WHERE id IN ({placeholders}) AND COALESCE(wechat_close_sent,0)=0
|
||||
""",
|
||||
ids,
|
||||
).fetchall()
|
||||
if not rows:
|
||||
return False
|
||||
first = dict(rows[0])
|
||||
return notify_options_close(
|
||||
cfg,
|
||||
conn,
|
||||
inst_id=str(first.get("inst_id") or ""),
|
||||
reason=reason,
|
||||
trade_id=int(first["id"]) if len(rows) == 1 else None,
|
||||
underlying=str(first.get("underlying") or ""),
|
||||
opt_type=first.get("opt_type"),
|
||||
sheets=sum(int(r["sheets"] or 0) for r in rows),
|
||||
premium_paid=sum(float(r["premium_paid"] or 0) for r in rows),
|
||||
premium_received=sum(float(r["premium_received"] or 0) for r in rows if r["premium_received"] is not None),
|
||||
realized_pnl=sum(float(r["realized_pnl"]) for r in rows if r["realized_pnl"] is not None),
|
||||
close_quote=first.get("close_quote"),
|
||||
)
|
||||
@@ -259,6 +259,11 @@ def eth_amount_from_sheets(sheets: int, ct_mult: float = 0.01) -> float:
|
||||
return round(int(sheets) * float(ct_mult), 8)
|
||||
|
||||
|
||||
def resolve_budget_full_usdc(trading_usdc: float, trade_budget_usdc: float) -> float:
|
||||
"""按可用余额打满:余额大于预算用预算,否则用余额."""
|
||||
return min(float(trading_usdc), float(trade_budget_usdc))
|
||||
|
||||
|
||||
def calc_order_size(
|
||||
*,
|
||||
quote_per_unit: float,
|
||||
|
||||
@@ -163,13 +163,18 @@ def _require_options_ex(cfg: dict[str, Any]):
|
||||
|
||||
|
||||
def _budget_full_usdc(cfg: dict[str, Any], ex: Any) -> tuple[float | None, str]:
|
||||
"""交易账户 USDC 可用余额(由 calc_order_size 再乘 budget_buffer 留余量)."""
|
||||
"""打满可用额度 = min(交易户可用 USDC, 单笔预算);calc_order_size 再乘 budget_buffer."""
|
||||
from lib.exchange.okx_options_lib import fetch_options_trading_usdc
|
||||
from lib.options.options_pricing_lib import resolve_budget_full_usdc
|
||||
|
||||
raw = fetch_options_trading_usdc(ex)
|
||||
if raw is None or float(raw) <= 0:
|
||||
return None, "交易账户 USDC 可用余额不足"
|
||||
return float(raw), ""
|
||||
trading = float(raw)
|
||||
cap = _env_float("OKX_OPTIONS_TRADE_BUDGET_USDC", float(cfg.get("trade_budget") or 10.0))
|
||||
if cap <= 0:
|
||||
return None, "单笔预算无效(OKX_OPTIONS_TRADE_BUDGET_USDC)"
|
||||
return resolve_budget_full_usdc(trading, float(cap)), ""
|
||||
|
||||
|
||||
def _open_premium_paid(cfg: dict[str, Any], inst_id: str) -> float | None:
|
||||
@@ -640,11 +645,15 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
conn = cfg["get_db"]()
|
||||
trade_id = None
|
||||
target_mon = None
|
||||
open_underlying = ""
|
||||
open_opt_type = None
|
||||
try:
|
||||
init_options_tables(conn)
|
||||
meta = q.get("meta") or {}
|
||||
u = str(meta.get("uly") or inst_id).split("-")[0]
|
||||
opt_type = meta.get("optType")
|
||||
open_underlying = u
|
||||
open_opt_type = opt_type
|
||||
cur = conn.execute(
|
||||
"""
|
||||
INSERT INTO options_trades
|
||||
@@ -683,9 +692,30 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
finally:
|
||||
conn.close()
|
||||
from lib.exchange.okx_options_lib import invalidate_option_positions_cache
|
||||
from lib.options.options_notify_lib import notify_options_open
|
||||
|
||||
invalidate_option_positions_cache()
|
||||
_sync_options_trades(cfg, force=True)
|
||||
try:
|
||||
conn_n = cfg["get_db"]()
|
||||
try:
|
||||
notify_options_open(
|
||||
cfg,
|
||||
conn_n,
|
||||
trade_id=trade_id,
|
||||
inst_id=inst_id,
|
||||
underlying=open_underlying,
|
||||
opt_type=open_opt_type,
|
||||
sheets=sheets,
|
||||
premium_paid=sizing.get("total_premium"),
|
||||
open_quote=float(ask) if ask is not None else None,
|
||||
target_index=target_index,
|
||||
signal_note=signal_note,
|
||||
)
|
||||
finally:
|
||||
conn_n.close()
|
||||
except Exception:
|
||||
pass
|
||||
return jsonify(
|
||||
{
|
||||
"ok": True,
|
||||
@@ -938,11 +968,21 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
if result.get("fully_closed"):
|
||||
try:
|
||||
from lib.options.options_target_lib import cancel_target_monitor
|
||||
from lib.options.options_notify_lib import notify_options_close
|
||||
|
||||
conn2 = cfg["get_db"]()
|
||||
try:
|
||||
cancel_target_monitor(conn2, inst_id=inst_id)
|
||||
conn2.commit()
|
||||
notify_options_close(
|
||||
cfg,
|
||||
conn2,
|
||||
inst_id=inst_id,
|
||||
reason="手动平仓",
|
||||
sheets=result.get("submitted_sheets"),
|
||||
premium_received=result.get("premium_received"),
|
||||
close_quote=result.get("locked_bid_px") or result.get("bid"),
|
||||
)
|
||||
finally:
|
||||
conn2.close()
|
||||
except Exception:
|
||||
@@ -1253,6 +1293,7 @@ def _start_monitor_thread(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
conn,
|
||||
live_inst_ids=live_ids,
|
||||
fetch_history_fn=lambda inst_id: fetch_option_position_history(ex, inst_id),
|
||||
notify_cfg=cfg,
|
||||
)
|
||||
|
||||
def _target_close(inst_id: str) -> dict[str, Any]:
|
||||
|
||||
@@ -292,6 +292,7 @@ def close_option_by_bid_depth(
|
||||
|
||||
|
||||
def _notify_target_close(
|
||||
cfg: dict[str, Any] | None,
|
||||
send_wechat: Callable[[str], None] | None,
|
||||
*,
|
||||
account_label: str,
|
||||
@@ -299,7 +300,28 @@ def _notify_target_close(
|
||||
target: float,
|
||||
idx: float,
|
||||
result: dict[str, Any],
|
||||
conn: Any = None,
|
||||
) -> None:
|
||||
"""目标位平仓推送:优先走统一平仓必发(幂等);无 cfg 时回退旧文案."""
|
||||
if result.get("fully_closed") or result.get("already_flat"):
|
||||
if cfg is not None:
|
||||
try:
|
||||
from lib.options.options_notify_lib import notify_options_close
|
||||
|
||||
notify_options_close(
|
||||
cfg,
|
||||
conn,
|
||||
inst_id=inst_id,
|
||||
reason="目标位平仓",
|
||||
sheets=result.get("submitted_sheets"),
|
||||
premium_received=result.get("premium_received"),
|
||||
close_quote=result.get("locked_bid_px") or result.get("bid"),
|
||||
target_index=target,
|
||||
trigger_idx=idx,
|
||||
)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
if not send_wechat:
|
||||
return
|
||||
try:
|
||||
@@ -313,6 +335,7 @@ def _notify_target_close(
|
||||
f"触发指数:{idx:g}",
|
||||
f"提交张数:{result.get('submitted_sheets') or '—'}",
|
||||
f"预估收回:{result.get('premium_received') if result.get('premium_received') is not None else '—'} USDC",
|
||||
f"状态:{'已全平' if (result.get('fully_closed') or result.get('already_flat')) else '挂单中/部分'}",
|
||||
]
|
||||
)
|
||||
)
|
||||
@@ -339,6 +362,7 @@ def run_options_target_closes(
|
||||
index_fn: Callable[[dict[str, Any]], float | None] | None = None,
|
||||
send_wechat: Callable[[str], None] | None = None,
|
||||
account_label: str = "OKX期权",
|
||||
cfg: dict[str, Any] | None = None,
|
||||
) -> int:
|
||||
"""
|
||||
扫描 active 目标委托;指数到位后限价平仓.
|
||||
@@ -433,11 +457,13 @@ def run_options_target_closes(
|
||||
_commit_monitor(conn)
|
||||
triggered += 1
|
||||
_notify_target_close(
|
||||
cfg,
|
||||
send_wechat,
|
||||
account_label=account_label,
|
||||
inst_id=inst_id,
|
||||
target=target,
|
||||
idx=idx,
|
||||
result=result,
|
||||
conn=conn,
|
||||
)
|
||||
return triggered
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<div class="options-page-wrap" style="grid-column:1/-1" id="options-root"
|
||||
data-default-underly="{{ options_default_underly | default('ETH') }}"
|
||||
data-budget-buffer="{{ options_budget_buffer | default(0.95) }}"
|
||||
data-trade-budget="{{ options_trade_budget | default(10) }}"
|
||||
data-ask-liq-filter="{% if options_chain_ask_liq_filter is defined %}{{ '1' if options_chain_ask_liq_filter else '0' }}{% else %}1{% endif %}">
|
||||
{% if not options_enabled %}
|
||||
<div class="flash" style="margin-bottom:12px">期权 API 未启用:请在 <code>crypto_monitor_okx/.env</code> 设置 <code>OKX_OPTIONS_ENABLED=true</code> 及主账户 <code>OKX_OPTIONS_API_*</code>,然后 <code>pm2 restart crypto_okx --update-env</code>.</div>
|
||||
@@ -18,7 +19,7 @@
|
||||
<li>环境配置「链上仅显示有卖一」开启时,隐藏无真实卖一或深度不足 1 张的合约(估算价 <strong>~</strong> 亦不显示)。</li>
|
||||
<li><strong>开仓只认真实卖一价且卖一深度≥1</strong>;无深度时面板显示参考标记价并禁用买入。</li>
|
||||
<li>链展示近 <span id="opt-chain-dte">14</span> 日到期;<strong>T 型</strong>默认 ATM ±5 档,可展开全部。</li>
|
||||
<li>「按可用余额打满」可用额度 = min(交易 USDC × 预算缓冲 <strong id="opt-budget-buf">{{ '%.2f'|format(options_budget_buffer|default(0.95)|float) }}</strong>, 单笔预算);可在 env「预算缓冲比例」改。</li>
|
||||
<li>「按可用余额打满」可用额度 = min(交易户可用 USDC, 单笔预算 <strong id="opt-trade-budget">{{ '%.2f'|format(options_trade_budget|default(10)|float) }}</strong>),再 × 预算缓冲 <strong id="opt-budget-buf">{{ '%.2f'|format(options_budget_buffer|default(0.95)|float) }}</strong> 算张数(env 可改)。</li>
|
||||
<li>平仓仅买一限价,详见说明文档。</li>
|
||||
</ul>
|
||||
<p><a href="/options/guide" target="_blank" rel="noopener">打开《期权开平仓与监控说明》</a></p>
|
||||
@@ -133,6 +134,9 @@
|
||||
<input type="number" id="opt-eth-amount" min="0.01" step="0.01" placeholder="如 0.5" style="display:none"
|
||||
autocomplete="off" inputmode="decimal" data-lpignore="true" data-1p-ignore="true" data-form-type="other">
|
||||
</div>
|
||||
<p class="muted opt-budget-full-hint" id="opt-budget-full-hint" style="display:none;margin:6px 0 0;font-size:.82rem;line-height:1.4">
|
||||
余额 > 单笔预算(<span id="opt-budget-full-cap">{{ '%.2f'|format(options_trade_budget|default(10)|float) }}</span>U)时按预算;余额不足时按余额;再乘预算缓冲算张数。
|
||||
</p>
|
||||
<input type="text" id="opt-signal-note" name="opt_signal_note" class="opt-signal-note" placeholder="备注(关键位说明)"
|
||||
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
|
||||
data-lpignore="true" data-1p-ignore="true" data-form-type="other" readonly>
|
||||
@@ -316,4 +320,4 @@
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/options_expiry_countdown.js?v=1"></script>
|
||||
<script src="/static/options_panel.js?v=49"></script>
|
||||
<script src="/static/options_panel.js?v=50"></script>
|
||||
|
||||
+150
-20
@@ -79,6 +79,12 @@ from lib.hub.hub_symbol_archive_lib import (
|
||||
update_review_quote,
|
||||
upsert_trade_overlay,
|
||||
)
|
||||
from lib.hub.hub_options_archive_lib import (
|
||||
init_options_archive_db,
|
||||
list_archive_options_calendar,
|
||||
list_daily_options_trades,
|
||||
sync_options_exchange_archive,
|
||||
)
|
||||
from lib.hub.hub_entry_plan_lib import (
|
||||
compute_entry_plan_stats,
|
||||
create_entry_plan,
|
||||
@@ -355,9 +361,11 @@ def _schedule_board_refresh() -> None:
|
||||
async def _run_archive_sync_once() -> dict:
|
||||
global _last_archive_sync
|
||||
init_archive_db()
|
||||
init_options_archive_db()
|
||||
settings = load_settings()
|
||||
targets = enabled_exchanges(settings)
|
||||
results: list[dict] = []
|
||||
options_results: list[dict] = []
|
||||
for ex in targets:
|
||||
ex_key = str(ex.get("key") or "").strip().lower()
|
||||
if not ex_key:
|
||||
@@ -390,34 +398,71 @@ async def _run_archive_sync_once() -> dict:
|
||||
"msg": msg,
|
||||
}
|
||||
)
|
||||
else:
|
||||
trades = trades_resp.get("trades") or []
|
||||
for t in trades:
|
||||
if isinstance(t, dict):
|
||||
t["exchange_key"] = ex_key
|
||||
|
||||
def remote_fetch(**kwargs):
|
||||
return _fetch_instance_ohlcv_sync(
|
||||
ex,
|
||||
symbol=kwargs.get("symbol") or "",
|
||||
timeframe=kwargs.get("timeframe") or "5m",
|
||||
since_ms=kwargs.get("since_ms"),
|
||||
limit=int(kwargs.get("limit") or 500),
|
||||
)
|
||||
|
||||
r = await asyncio.to_thread(
|
||||
sync_exchange_symbol_archives,
|
||||
ex_key,
|
||||
trades,
|
||||
remote_fetch,
|
||||
)
|
||||
r["name"] = ex.get("name")
|
||||
r["trade_count"] = len(trades)
|
||||
results.append(r)
|
||||
|
||||
caps = [str(x).lower() for x in (ex.get("capabilities") or [])]
|
||||
if "options" not in caps:
|
||||
continue
|
||||
trades = trades_resp.get("trades") or []
|
||||
for t in trades:
|
||||
opt_resp = await asyncio.to_thread(
|
||||
_fetch_instance_options_review_archive_sync,
|
||||
ex,
|
||||
days=ARCHIVE_TRADE_DAYS,
|
||||
limit=ARCHIVE_TRADE_LIMIT,
|
||||
)
|
||||
if not opt_resp.get("ok"):
|
||||
options_results.append(
|
||||
{
|
||||
"exchange_key": ex_key,
|
||||
"name": ex.get("name"),
|
||||
"ok": False,
|
||||
"status": opt_resp.get("status"),
|
||||
"msg": opt_resp.get("msg")
|
||||
or opt_resp.get("error")
|
||||
or opt_resp.get("detail")
|
||||
or "拉取期权复盘失败",
|
||||
"product": "options",
|
||||
}
|
||||
)
|
||||
continue
|
||||
opt_trades = opt_resp.get("trades") or []
|
||||
for t in opt_trades:
|
||||
if isinstance(t, dict):
|
||||
t["exchange_key"] = ex_key
|
||||
|
||||
def remote_fetch(**kwargs):
|
||||
return _fetch_instance_ohlcv_sync(
|
||||
ex,
|
||||
symbol=kwargs.get("symbol") or "",
|
||||
timeframe=kwargs.get("timeframe") or "5m",
|
||||
since_ms=kwargs.get("since_ms"),
|
||||
limit=int(kwargs.get("limit") or 500),
|
||||
)
|
||||
|
||||
r = await asyncio.to_thread(
|
||||
sync_exchange_symbol_archives,
|
||||
orow = await asyncio.to_thread(
|
||||
sync_options_exchange_archive,
|
||||
ex_key,
|
||||
trades,
|
||||
remote_fetch,
|
||||
opt_trades,
|
||||
)
|
||||
r["name"] = ex.get("name")
|
||||
r["trade_count"] = len(trades)
|
||||
results.append(r)
|
||||
orow["name"] = ex.get("name")
|
||||
options_results.append(orow)
|
||||
out = {
|
||||
"ok": True,
|
||||
"exchanges": len(targets),
|
||||
"results": results,
|
||||
"options_results": options_results,
|
||||
"updated_at": __import__("datetime").datetime.now().isoformat(timespec="seconds"),
|
||||
}
|
||||
_last_archive_sync = out
|
||||
@@ -1119,6 +1164,16 @@ class SettingsDisplayBody(BaseModel):
|
||||
show_nav_amp_stats: bool = True
|
||||
show_nav_help: bool = True
|
||||
show_nav_logs: bool = True
|
||||
show_monitor_binance: bool = True
|
||||
show_monitor_okx_perp: bool = True
|
||||
show_monitor_okx_options: bool = True
|
||||
show_monitor_gate: bool = True
|
||||
show_strategy_playbook_v2: bool = True
|
||||
show_strategy_playbook: bool = True
|
||||
show_strategy_behavior: bool = True
|
||||
show_strategy_binance: bool = True
|
||||
show_strategy_okx: bool = True
|
||||
show_strategy_gate: bool = True
|
||||
|
||||
|
||||
class SupervisorSettingsBody(BaseModel):
|
||||
@@ -1355,6 +1410,34 @@ def _fetch_instance_trades_archive_sync(
|
||||
return {"ok": False, "msg": str(e)}
|
||||
|
||||
|
||||
def _fetch_instance_options_review_archive_sync(
|
||||
ex: dict,
|
||||
*,
|
||||
days: int = 365,
|
||||
limit: int = 2000,
|
||||
) -> dict:
|
||||
base = (ex.get("flask_url") or "").rstrip("/")
|
||||
if not base:
|
||||
return {"ok": False, "msg": "未配置 flask_url"}
|
||||
params = {"days": str(int(days)), "limit": str(int(limit))}
|
||||
url = f"{base}/api/hub/options/review/archive?{urlencode(params)}"
|
||||
try:
|
||||
with httpx.Client(timeout=max(HUB_FLASK_TIMEOUT, 120.0)) as client:
|
||||
r = client.get(url, headers=_hub_headers())
|
||||
if r.status_code >= 400:
|
||||
parsed = _parse_http_json_body(r)
|
||||
parsed.setdefault("ok", False)
|
||||
parsed.setdefault("status", r.status_code)
|
||||
return parsed
|
||||
data = r.json() if r.content else {}
|
||||
if isinstance(data, dict):
|
||||
data.setdefault("ok", True)
|
||||
return data
|
||||
return {"ok": False, "msg": "无效 JSON"}
|
||||
except Exception as e:
|
||||
return {"ok": False, "msg": str(e)}
|
||||
|
||||
|
||||
def _fetch_instance_ohlcv_sync(
|
||||
ex: dict,
|
||||
*,
|
||||
@@ -3135,6 +3218,52 @@ def api_archive_calendar(
|
||||
return {"ok": True, **payload}
|
||||
|
||||
|
||||
@app.get("/api/archive/options/daily-trades")
|
||||
def api_archive_options_daily_trades(
|
||||
period: str = "",
|
||||
trading_day: str = "",
|
||||
date_from: str = "",
|
||||
date_to: str = "",
|
||||
exchange_key: str = "",
|
||||
filter_profit: str = "",
|
||||
filter_loss: str = "",
|
||||
search: str = "",
|
||||
source_type: str = "",
|
||||
):
|
||||
init_options_archive_db()
|
||||
payload = list_daily_options_trades(
|
||||
trading_day=trading_day,
|
||||
period=period or "today",
|
||||
date_from=date_from,
|
||||
date_to=date_to,
|
||||
exchange_key=exchange_key,
|
||||
filter_profit=(filter_profit or "").lower() in ("1", "true", "yes", "on"),
|
||||
filter_loss=(filter_loss or "").lower() in ("1", "true", "yes", "on"),
|
||||
search=search,
|
||||
source_type=source_type,
|
||||
)
|
||||
return {"ok": True, **payload}
|
||||
|
||||
|
||||
@app.get("/api/archive/options/calendar")
|
||||
def api_archive_options_calendar(
|
||||
year: int = 0,
|
||||
month: int = 0,
|
||||
exchange_key: str = "",
|
||||
):
|
||||
init_options_archive_db()
|
||||
if year <= 0 or month <= 0:
|
||||
td = today_trading_day()
|
||||
parts = td.split("-")
|
||||
year = int(parts[0])
|
||||
month = int(parts[1])
|
||||
try:
|
||||
payload = list_archive_options_calendar(year, month, exchange_key=exchange_key)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e)) from e
|
||||
return {"ok": True, **payload}
|
||||
|
||||
|
||||
@app.get("/api/archive/quotes")
|
||||
def api_archive_quotes():
|
||||
init_archive_db()
|
||||
@@ -3348,7 +3477,8 @@ async def api_archive_sync():
|
||||
|
||||
@app.get("/api/strategy/meta")
|
||||
def api_strategy_meta():
|
||||
return strategy_meta_payload()
|
||||
display = (load_settings() or {}).get("display") or {}
|
||||
return strategy_meta_payload(display)
|
||||
|
||||
|
||||
@app.get("/api/help/meta")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""交易教练用的执行手册短摘要(来源 docs/交易执行手册-期权与Gate.md)."""
|
||||
"""交易教练用的执行手册短摘要(现行 v2:无对冲)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
@@ -6,19 +6,21 @@ from pathlib import Path
|
||||
from lib.paths import REPO_ROOT
|
||||
|
||||
# 控制 token:保持简短;手册大改时同步修订本摘要.
|
||||
_PLAYBOOK_BRIEF = """【用户策略执行手册·摘要】(来源:docs/交易执行手册-期权与Gate.md + docs/交易行为准则-开单三检.md)
|
||||
开单防火墙(强制):信号判断(核心点位是否清晰)→流程确认(资金/单笔敞口超限则暂停)→情绪自检(符合系统才做;怕踏空/回本/证明自己→放弃)。三检不过不开。成败先看三检是否跑完,不看这笔盈亏。
|
||||
一句话:横盘对冲(可偏置实值);突破用一天期权;假破确认后小仓合约加强;先过方向/空间/值不值得;期权不手平;一位置两次,错完收工;单笔小亏、组合回撤可控.
|
||||
分工:OKX 期权=主业;Gate 合约=辅业;其它账户暂不做.同一时段尽量只让一边说话.
|
||||
入场三类:①横盘较久→期期对冲(一天 Call+Put,总权利金约10U),期间一般不开 Gate;②方向/空间/值不值得过关且结构突破→一天期权方向单,默认不上合约;③已有突破期权后出现反向假破确认→Gate 小仓加强(加重暴露,按一笔故事控风险).
|
||||
仓位(总资约800U):单笔期权约10U且一次一仓;期期对冲合计约10U;Gate 保证金约50U×约10x,止损约5U,单笔最亏约≤10U;日最坏约≤20U.
|
||||
期权纪律:不手动平仓,只认规则止盈或到期(紧急手平非策略样本);默认一天期,尽量北京时间16:00后开次日到期.
|
||||
Gate 纪律:只做很明确位置;同一位置最多两次机会(结构突破/假突破);两次都错→当日收工.
|
||||
教练用法:对照上述纪律讨论执行与心态;开单前优先提醒三检;勿另造策略或鼓励期权手平/超仓."""
|
||||
_PLAYBOOK_BRIEF = """【用户策略执行手册·摘要】(来源:docs/交易执行手册-v2-期权与合约.md + docs/交易行为准则-开单三检.md)
|
||||
开单防火墙(强制):信号判断→流程确认→情绪自检。三检不过不开。成败先看三检是否跑完。
|
||||
主链条(强制):1H方向(含N字)→空间(支撑/阻力,至少约≥2%)→结构(约8h+/48×15m:收敛/两段回调/箱体/假突破等)→定损盈(结构突破=外沿;假突破=针尖)→选工具(只剩期权或合约)。
|
||||
丢掉对冲:不做期期对冲/偏置对冲;对冲易产生「有保护就能多做」的幻觉。日更不是目标,过检才开。
|
||||
一句话:1H定方向→量空间→等够结构→按模型定损盈→只在期权与合约里选;期权不手平;Gate一位置两次,错完收工;珍惜机会。
|
||||
分工:OKX期权与Gate合约;其它账户暂不做。同一时段尽量只让一边说话。
|
||||
入场:①主链条过关→一天期期权方向单(空间够可优先虚值);②极明确位置→Gate合约;独立假破优先只做合约或空仓。明确不做横盘双买对冲。
|
||||
仓位(总资约800U):单笔期权约10U且一次一仓;Gate保证金约50U×约10x,止损约5U,单笔最亏约≤10U;日最坏约≤20U。
|
||||
期权纪律:开仓后中间不手动平仓,只认规则止盈或到期;默认一天期,尽量北京时间16:00后开次日到期。
|
||||
Gate纪律:只做很明确位置;同一位置最多两次(结构突破/假突破);两次都错→当日收工。
|
||||
教练用法:对照上述纪律讨论执行与心态;开单前优先提醒主链条与三检;勿另造策略或鼓励对冲/期权手平/超仓/每天默认开期权。"""
|
||||
|
||||
|
||||
def playbook_md_path() -> Path:
|
||||
return REPO_ROOT / "docs" / "交易执行手册-期权与Gate.md"
|
||||
return REPO_ROOT / "docs" / "交易执行手册-v2-期权与合约.md"
|
||||
|
||||
|
||||
def format_playbook_brief_for_chat(max_chars: int = 1200) -> str:
|
||||
|
||||
@@ -34,6 +34,18 @@ DEFAULT_DISPLAY = {
|
||||
"show_nav_amp_stats": True,
|
||||
"show_nav_help": True,
|
||||
"show_nav_logs": True,
|
||||
# 监控区卡片(仅隐藏界面,不关闭账户)
|
||||
"show_monitor_binance": True,
|
||||
"show_monitor_okx_perp": True,
|
||||
"show_monitor_okx_options": True,
|
||||
"show_monitor_gate": True,
|
||||
# 策略说明页签
|
||||
"show_strategy_playbook_v2": True,
|
||||
"show_strategy_playbook": True,
|
||||
"show_strategy_behavior": True,
|
||||
"show_strategy_binance": True,
|
||||
"show_strategy_okx": True,
|
||||
"show_strategy_gate": True,
|
||||
}
|
||||
|
||||
DEFAULT_EXCHANGES = [
|
||||
|
||||
+155
-115
@@ -3501,6 +3501,18 @@ button.btn-sm {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.settings-display-subtitle {
|
||||
margin: 16px 0 6px;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.settings-display-subtitle + .settings-display-hint {
|
||||
margin-top: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.settings-display-hint {
|
||||
margin: 8px 0 0;
|
||||
font-size: 0.78rem;
|
||||
@@ -8045,6 +8057,34 @@ body.funds-fullscreen-open {
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
}
|
||||
.archive-product-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
.archive-product-tab {
|
||||
border: 1px solid var(--border-soft);
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
padding: 8px 18px;
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
font-family: var(--font);
|
||||
font-size: 0.88rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.archive-product-tab.is-active {
|
||||
background: rgba(16, 185, 129, 0.2);
|
||||
border-color: rgba(16, 185, 129, 0.55);
|
||||
color: var(--text);
|
||||
}
|
||||
body.archive-product-options .archive-toolbar-desktop[data-archive-perp-only],
|
||||
body.archive-product-options #archive-btn-chart-toggle,
|
||||
body.archive-product-options #archive-filter-sick,
|
||||
body.archive-product-options #archive-tab-viz {
|
||||
display: none !important;
|
||||
}
|
||||
.archive-content-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -11008,118 +11048,118 @@ html[data-theme="light"] .hub-logs-card-hint {
|
||||
.amp-form { grid-template-columns: 1fr 1fr; }
|
||||
.amp-actions { grid-column: 1 / -1; }
|
||||
}
|
||||
|
||||
/* --- strategy compare --- */
|
||||
#page-compare .toolbar {
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.cmp-form { display: flex; flex-direction: column; gap: 14px; margin-bottom: 16px; }
|
||||
.cmp-form .card,
|
||||
.cmp-common-card,
|
||||
.cmp-sum-card,
|
||||
.cmp-rec-card {
|
||||
padding: 18px 20px;
|
||||
}
|
||||
.cmp-common-card h2,
|
||||
.cmp-form .card h2 {
|
||||
margin: 0 0 14px;
|
||||
font-size: 15px;
|
||||
}
|
||||
.cmp-subhead {
|
||||
margin: 16px 0 10px;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
.cmp-form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 14px 16px;
|
||||
}
|
||||
.cmp-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.cmp-field input,
|
||||
.cmp-field select {
|
||||
background: var(--inset-surface);
|
||||
border: 1px solid var(--border-soft);
|
||||
border-radius: 8px;
|
||||
color: var(--text);
|
||||
padding: 9px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.cmp-input-cols {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
.cmp-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.cmp-sum-card h3 { margin: 0 0 12px; font-size: 14px; }
|
||||
.cmp-sum-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
margin: 6px 0;
|
||||
color: var(--muted);
|
||||
}
|
||||
.cmp-sum-row strong { color: var(--text); font-weight: 600; }
|
||||
.cmp-muted { color: var(--muted); font-size: 12px; margin: 0; }
|
||||
.cmp-table-wrap { margin-bottom: 16px; }
|
||||
.cmp-table-scroll { overflow-x: auto; }
|
||||
.cmp-table {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
font-size: 13px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
}
|
||||
.cmp-table th,
|
||||
.cmp-table td {
|
||||
border-bottom: 1px solid var(--border-soft);
|
||||
padding: 14px 16px;
|
||||
vertical-align: top;
|
||||
text-align: left;
|
||||
}
|
||||
.cmp-table th:first-child,
|
||||
.cmp-table td:first-child { width: 22%; }
|
||||
.cmp-table tr:last-child td { border-bottom: none; }
|
||||
.cmp-cell-note {
|
||||
margin-top: 6px;
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
line-height: 1.4;
|
||||
}
|
||||
.cmp-pnl-pos { color: var(--green); font-weight: 600; }
|
||||
.cmp-pnl-neg { color: var(--red); font-weight: 600; }
|
||||
.cmp-rec-head { font-size: 16px; margin-bottom: 8px; }
|
||||
.cmp-rec-reason { margin: 0 0 10px; color: var(--muted); font-size: 13px; }
|
||||
.cmp-rec-list { margin: 0; padding-left: 20px; font-size: 13px; line-height: 1.55; }
|
||||
.cmp-warn { margin-top: 12px; font-size: 12px; color: var(--warn, #e6a23c); }
|
||||
.cmp-foot-note { margin: 12px 0 0; font-size: 11px; color: var(--muted); }
|
||||
@media (max-width: 900px) {
|
||||
.cmp-form .card,
|
||||
.cmp-common-card,
|
||||
.cmp-sum-card,
|
||||
.cmp-rec-card {
|
||||
padding: 16px;
|
||||
}
|
||||
.cmp-form-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.cmp-input-cols,
|
||||
.cmp-summary { grid-template-columns: 1fr; }
|
||||
.cmp-table th,
|
||||
.cmp-table td { padding: 12px 14px; }
|
||||
}
|
||||
|
||||
/* --- strategy compare --- */
|
||||
#page-compare .toolbar {
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.cmp-form { display: flex; flex-direction: column; gap: 14px; margin-bottom: 16px; }
|
||||
.cmp-form .card,
|
||||
.cmp-common-card,
|
||||
.cmp-sum-card,
|
||||
.cmp-rec-card {
|
||||
padding: 18px 20px;
|
||||
}
|
||||
.cmp-common-card h2,
|
||||
.cmp-form .card h2 {
|
||||
margin: 0 0 14px;
|
||||
font-size: 15px;
|
||||
}
|
||||
.cmp-subhead {
|
||||
margin: 16px 0 10px;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
.cmp-form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 14px 16px;
|
||||
}
|
||||
.cmp-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.cmp-field input,
|
||||
.cmp-field select {
|
||||
background: var(--inset-surface);
|
||||
border: 1px solid var(--border-soft);
|
||||
border-radius: 8px;
|
||||
color: var(--text);
|
||||
padding: 9px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.cmp-input-cols {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
.cmp-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.cmp-sum-card h3 { margin: 0 0 12px; font-size: 14px; }
|
||||
.cmp-sum-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
margin: 6px 0;
|
||||
color: var(--muted);
|
||||
}
|
||||
.cmp-sum-row strong { color: var(--text); font-weight: 600; }
|
||||
.cmp-muted { color: var(--muted); font-size: 12px; margin: 0; }
|
||||
.cmp-table-wrap { margin-bottom: 16px; }
|
||||
.cmp-table-scroll { overflow-x: auto; }
|
||||
.cmp-table {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
font-size: 13px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
}
|
||||
.cmp-table th,
|
||||
.cmp-table td {
|
||||
border-bottom: 1px solid var(--border-soft);
|
||||
padding: 14px 16px;
|
||||
vertical-align: top;
|
||||
text-align: left;
|
||||
}
|
||||
.cmp-table th:first-child,
|
||||
.cmp-table td:first-child { width: 22%; }
|
||||
.cmp-table tr:last-child td { border-bottom: none; }
|
||||
.cmp-cell-note {
|
||||
margin-top: 6px;
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
line-height: 1.4;
|
||||
}
|
||||
.cmp-pnl-pos { color: var(--green); font-weight: 600; }
|
||||
.cmp-pnl-neg { color: var(--red); font-weight: 600; }
|
||||
.cmp-rec-head { font-size: 16px; margin-bottom: 8px; }
|
||||
.cmp-rec-reason { margin: 0 0 10px; color: var(--muted); font-size: 13px; }
|
||||
.cmp-rec-list { margin: 0; padding-left: 20px; font-size: 13px; line-height: 1.55; }
|
||||
.cmp-warn { margin-top: 12px; font-size: 12px; color: var(--warn, #e6a23c); }
|
||||
.cmp-foot-note { margin: 12px 0 0; font-size: 11px; color: var(--muted); }
|
||||
@media (max-width: 900px) {
|
||||
.cmp-form .card,
|
||||
.cmp-common-card,
|
||||
.cmp-sum-card,
|
||||
.cmp-rec-card {
|
||||
padding: 16px;
|
||||
}
|
||||
.cmp-form-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.cmp-input-cols,
|
||||
.cmp-summary { grid-template-columns: 1fr; }
|
||||
.cmp-table th,
|
||||
.cmp-table td { padding: 12px 14px; }
|
||||
}
|
||||
|
||||
@@ -9,12 +9,42 @@
|
||||
return !!d[key];
|
||||
}
|
||||
|
||||
window.hubDisplayPref = displayPref;
|
||||
|
||||
function showAccountPnlPref() {
|
||||
return displayPref("show_account_pnl", true);
|
||||
}
|
||||
|
||||
window.hubShowAccountPnlPref = showAccountPnlPref;
|
||||
|
||||
function showMonitorBinancePref() {
|
||||
return displayPref("show_monitor_binance", true);
|
||||
}
|
||||
|
||||
function showMonitorOkxPerpPref() {
|
||||
return displayPref("show_monitor_okx_perp", true);
|
||||
}
|
||||
|
||||
function showMonitorOkxOptionsPref() {
|
||||
return displayPref("show_monitor_okx_options", true);
|
||||
}
|
||||
|
||||
function showMonitorGatePref() {
|
||||
return displayPref("show_monitor_gate", true);
|
||||
}
|
||||
|
||||
function monitorExchangeKeyVisible(key) {
|
||||
const k = String(key || "").toLowerCase();
|
||||
if (k === "binance") return showMonitorBinancePref();
|
||||
if (k === "gate") return showMonitorGatePref();
|
||||
if (k === "okx") return showMonitorOkxPerpPref() || showMonitorOkxOptionsPref();
|
||||
return true;
|
||||
}
|
||||
|
||||
function filterVisibleMonitorRows(rows) {
|
||||
return (rows || []).filter((r) => monitorExchangeKeyVisible(r && r.key));
|
||||
}
|
||||
|
||||
function showNavFundsPref() {
|
||||
return displayPref("show_nav_funds", true);
|
||||
}
|
||||
@@ -157,32 +187,33 @@
|
||||
|
||||
function syncDisplayPrefsUI(data) {
|
||||
const d = (data && data.display) || {};
|
||||
const pnlCb = document.getElementById("pref-show-account-pnl");
|
||||
const fundsCb = document.getElementById("pref-show-nav-funds");
|
||||
const dashCb = document.getElementById("pref-show-nav-dashboard");
|
||||
const planCb = document.getElementById("pref-show-nav-plan");
|
||||
const archiveCb = document.getElementById("pref-show-nav-archive");
|
||||
const quotesCb = document.getElementById("pref-show-nav-quotes");
|
||||
const aiCb = document.getElementById("pref-show-nav-ai");
|
||||
const calcCb = document.getElementById("pref-show-nav-calculator");
|
||||
const compareCb = document.getElementById("pref-show-nav-compare");
|
||||
const strategyCb = document.getElementById("pref-show-nav-strategy");
|
||||
const ampCb = document.getElementById("pref-show-nav-amp-stats");
|
||||
const helpCb = document.getElementById("pref-show-nav-help");
|
||||
const logsCb = document.getElementById("pref-show-nav-logs");
|
||||
if (pnlCb) pnlCb.checked = d.show_account_pnl !== false;
|
||||
if (fundsCb) fundsCb.checked = d.show_nav_funds !== false;
|
||||
if (dashCb) dashCb.checked = d.show_nav_dashboard !== false;
|
||||
if (planCb) planCb.checked = d.show_nav_plan !== false;
|
||||
if (archiveCb) archiveCb.checked = d.show_nav_archive !== false;
|
||||
if (quotesCb) quotesCb.checked = d.show_nav_quotes !== false;
|
||||
if (aiCb) aiCb.checked = d.show_nav_ai !== false;
|
||||
if (calcCb) calcCb.checked = d.show_nav_calculator !== false;
|
||||
if (compareCb) compareCb.checked = d.show_nav_compare !== false;
|
||||
if (strategyCb) strategyCb.checked = d.show_nav_strategy !== false;
|
||||
if (ampCb) ampCb.checked = d.show_nav_amp_stats !== false;
|
||||
if (helpCb) helpCb.checked = d.show_nav_help !== false;
|
||||
if (logsCb) logsCb.checked = d.show_nav_logs !== false;
|
||||
const setChk = (id, key) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.checked = d[key] !== false;
|
||||
};
|
||||
setChk("pref-show-account-pnl", "show_account_pnl");
|
||||
setChk("pref-show-nav-funds", "show_nav_funds");
|
||||
setChk("pref-show-nav-dashboard", "show_nav_dashboard");
|
||||
setChk("pref-show-nav-plan", "show_nav_plan");
|
||||
setChk("pref-show-nav-archive", "show_nav_archive");
|
||||
setChk("pref-show-nav-quotes", "show_nav_quotes");
|
||||
setChk("pref-show-nav-ai", "show_nav_ai");
|
||||
setChk("pref-show-nav-calculator", "show_nav_calculator");
|
||||
setChk("pref-show-nav-compare", "show_nav_compare");
|
||||
setChk("pref-show-nav-strategy", "show_nav_strategy");
|
||||
setChk("pref-show-nav-amp-stats", "show_nav_amp_stats");
|
||||
setChk("pref-show-nav-help", "show_nav_help");
|
||||
setChk("pref-show-nav-logs", "show_nav_logs");
|
||||
setChk("pref-show-monitor-binance", "show_monitor_binance");
|
||||
setChk("pref-show-monitor-okx-perp", "show_monitor_okx_perp");
|
||||
setChk("pref-show-monitor-okx-options", "show_monitor_okx_options");
|
||||
setChk("pref-show-monitor-gate", "show_monitor_gate");
|
||||
setChk("pref-show-strategy-playbook-v2", "show_strategy_playbook_v2");
|
||||
setChk("pref-show-strategy-playbook", "show_strategy_playbook");
|
||||
setChk("pref-show-strategy-behavior", "show_strategy_behavior");
|
||||
setChk("pref-show-strategy-binance", "show_strategy_binance");
|
||||
setChk("pref-show-strategy-okx", "show_strategy_okx");
|
||||
setChk("pref-show-strategy-gate", "show_strategy_gate");
|
||||
syncNavVisibility(data);
|
||||
}
|
||||
|
||||
@@ -1569,7 +1600,7 @@
|
||||
if (upd) upd.textContent = txt;
|
||||
if (updSum) updSum.textContent = txt;
|
||||
}
|
||||
updateMonitorAlertSummary(rows || []);
|
||||
updateMonitorAlertSummary(filterVisibleMonitorRows(rows || []));
|
||||
void refreshMacroRiskBanner(rows || []);
|
||||
renderMonitorGrid(rows || []);
|
||||
}
|
||||
@@ -2080,19 +2111,20 @@
|
||||
if (lastMonitorRows.length && nowMobile !== wasMobile) {
|
||||
wasMobile = nowMobile;
|
||||
renderMonitorGrid(lastMonitorRows);
|
||||
updateMonitorAlertSummary(lastMonitorRows);
|
||||
updateMonitorAlertSummary(filterVisibleMonitorRows(lastMonitorRows));
|
||||
syncHubMobileTabActive(currentPage());
|
||||
return;
|
||||
}
|
||||
wasMobile = nowMobile;
|
||||
const box = document.getElementById("monitor-grid");
|
||||
if (box && lastMonitorRows.length) {
|
||||
const split = monitorOptionsSplitActive(lastMonitorRows);
|
||||
syncMonitorGridColumns(box, lastMonitorRows.length + (lastMonitorTotals ? 1 : 0), {
|
||||
const visible = filterVisibleMonitorRows(lastMonitorRows);
|
||||
const split = monitorOptionsSplitActive(visible);
|
||||
syncMonitorGridColumns(box, visible.length + (lastMonitorTotals ? 1 : 0), {
|
||||
statsFirst: !!lastMonitorTotals && !split,
|
||||
optionsSplit: split,
|
||||
});
|
||||
updateMonitorAlertSummary(lastMonitorRows);
|
||||
updateMonitorAlertSummary(visible);
|
||||
}
|
||||
syncHubMobileTabActive(currentPage());
|
||||
}, 120);
|
||||
@@ -2486,11 +2518,12 @@
|
||||
const fs = document.getElementById("exchange-fullscreen");
|
||||
const fsInner = document.getElementById("exchange-fullscreen-inner");
|
||||
if (!box) return;
|
||||
if (expandedExchangeId && !rows.some((r) => String(r.id) === String(expandedExchangeId))) {
|
||||
const visibleSource = filterVisibleMonitorRows(rows);
|
||||
if (expandedExchangeId && !visibleSource.some((r) => String(r.id) === String(expandedExchangeId))) {
|
||||
closeExchangeFullscreen();
|
||||
}
|
||||
const mobileTiles = isMobileLayout() && !expandedExchangeId;
|
||||
const displayRows = mobileTiles ? sortRowsForMobileDashboard(rows) : rows;
|
||||
const displayRows = mobileTiles ? sortRowsForMobileDashboard(visibleSource) : visibleSource;
|
||||
const optionsSplit = monitorOptionsSplitActive(displayRows);
|
||||
monitorGridOptionsSplit = optionsSplit;
|
||||
const showStatsCard = !expandedExchangeId;
|
||||
@@ -2503,20 +2536,33 @@
|
||||
let cardsHtml = "";
|
||||
if (optionsSplit) {
|
||||
const okxRow = displayRows.find((r) => rowHasOptionsLayout(r));
|
||||
const otherRows = displayRows.filter((r) => !rowHasOptionsLayout(r));
|
||||
const ph =
|
||||
'<div class="card card-monitor-split-side card-monitor-placeholder" aria-hidden="true"></div>';
|
||||
/* 平铺 2×2 顺序:永续|币安 / 期权|Gate —— 同行左右同高,多仓时该行一起长高 */
|
||||
const cells = [
|
||||
okxRow ? renderMonitorCard(okxRow, { okxPart: "perp", splitSide: true }) : ph,
|
||||
otherRows[0] ? renderMonitorCard(otherRows[0], { splitSide: true }) : ph,
|
||||
okxRow ? renderMonitorCard(okxRow, { okxPart: "options", splitSide: true }) : ph,
|
||||
otherRows[1] ? renderMonitorCard(otherRows[1], { splitSide: true }) : ph,
|
||||
];
|
||||
for (let i = 2; i < otherRows.length; i++) {
|
||||
cells.push(renderMonitorCard(otherRows[i], { splitSide: true }));
|
||||
const otherByKey = {};
|
||||
displayRows
|
||||
.filter((r) => !rowHasOptionsLayout(r))
|
||||
.forEach((r) => {
|
||||
otherByKey[String(r.key || "").toLowerCase()] = r;
|
||||
});
|
||||
/* 平铺顺序尽量保持:永续|币安 / 期权|Gate;隐藏项不占位 */
|
||||
const cells = [];
|
||||
if (okxRow && showMonitorOkxPerpPref()) {
|
||||
cells.push(renderMonitorCard(okxRow, { okxPart: "perp", splitSide: true }));
|
||||
}
|
||||
cardsHtml = `<div class="monitor-split-body monitor-split-2x2">${cells.join("")}</div>`;
|
||||
if (otherByKey.binance && showMonitorBinancePref()) {
|
||||
cells.push(renderMonitorCard(otherByKey.binance, { splitSide: true }));
|
||||
}
|
||||
if (okxRow && showMonitorOkxOptionsPref()) {
|
||||
cells.push(renderMonitorCard(okxRow, { okxPart: "options", splitSide: true }));
|
||||
}
|
||||
if (otherByKey.gate && showMonitorGatePref()) {
|
||||
cells.push(renderMonitorCard(otherByKey.gate, { splitSide: true }));
|
||||
}
|
||||
Object.keys(otherByKey).forEach((k) => {
|
||||
if (k === "binance" || k === "gate") return;
|
||||
cells.push(renderMonitorCard(otherByKey[k], { splitSide: true }));
|
||||
});
|
||||
cardsHtml = cells.length
|
||||
? `<div class="monitor-split-body monitor-split-2x2">${cells.join("")}</div>`
|
||||
: "";
|
||||
} else {
|
||||
cardsHtml =
|
||||
displayRows
|
||||
@@ -2545,7 +2591,7 @@
|
||||
}
|
||||
|
||||
if (expandedExchangeId && fs && fsInner) {
|
||||
const row = rows.find((r) => String(r.id) === String(expandedExchangeId));
|
||||
const row = visibleSource.find((r) => String(r.id) === String(expandedExchangeId));
|
||||
if (row) {
|
||||
try {
|
||||
fsInner.innerHTML = renderFullscreenExchange(row);
|
||||
@@ -3900,7 +3946,8 @@
|
||||
|
||||
function monitorOptionsSplitActive(rows) {
|
||||
if (isMobileLayout() || expandedExchangeId) return false;
|
||||
return (rows || []).some((r) => rowHasOptionsLayout(r));
|
||||
if (!(rows || []).some((r) => rowHasOptionsLayout(r))) return false;
|
||||
return showMonitorOkxPerpPref() || showMonitorOkxOptionsPref();
|
||||
}
|
||||
|
||||
function renderPerpetualInnerCard(row, ag, pos, orders, trends, tickMap, intraday) {
|
||||
@@ -5112,19 +5159,10 @@
|
||||
|
||||
function collectSettingsFromUI() {
|
||||
const rows = [...document.querySelectorAll("#settings-list .settings-card")];
|
||||
const pnlCb = document.getElementById("pref-show-account-pnl");
|
||||
const fundsCb = document.getElementById("pref-show-nav-funds");
|
||||
const dashCb = document.getElementById("pref-show-nav-dashboard");
|
||||
const planCb = document.getElementById("pref-show-nav-plan");
|
||||
const archiveCb = document.getElementById("pref-show-nav-archive");
|
||||
const quotesCb = document.getElementById("pref-show-nav-quotes");
|
||||
const aiCb = document.getElementById("pref-show-nav-ai");
|
||||
const calcCb = document.getElementById("pref-show-nav-calculator");
|
||||
const compareCb = document.getElementById("pref-show-nav-compare");
|
||||
const strategyCb = document.getElementById("pref-show-nav-strategy");
|
||||
const ampCb = document.getElementById("pref-show-nav-amp-stats");
|
||||
const helpCb = document.getElementById("pref-show-nav-help");
|
||||
const logsCb = document.getElementById("pref-show-nav-logs");
|
||||
const chk = (id) => {
|
||||
const el = document.getElementById(id);
|
||||
return el ? !!el.checked : true;
|
||||
};
|
||||
const supEnabled = document.getElementById("supervisor-enabled");
|
||||
const supProg = document.getElementById("supervisor-wechat-program");
|
||||
const supWebhook = document.getElementById("supervisor-wechat-webhook");
|
||||
@@ -5137,19 +5175,29 @@
|
||||
return {
|
||||
version: 1,
|
||||
display: {
|
||||
show_account_pnl: pnlCb ? !!pnlCb.checked : true,
|
||||
show_nav_funds: fundsCb ? !!fundsCb.checked : true,
|
||||
show_nav_dashboard: dashCb ? !!dashCb.checked : true,
|
||||
show_nav_plan: planCb ? !!planCb.checked : true,
|
||||
show_nav_archive: archiveCb ? !!archiveCb.checked : true,
|
||||
show_nav_quotes: quotesCb ? !!quotesCb.checked : true,
|
||||
show_nav_ai: aiCb ? !!aiCb.checked : true,
|
||||
show_nav_calculator: calcCb ? !!calcCb.checked : true,
|
||||
show_nav_compare: compareCb ? !!compareCb.checked : true,
|
||||
show_nav_strategy: strategyCb ? !!strategyCb.checked : true,
|
||||
show_nav_amp_stats: ampCb ? !!ampCb.checked : true,
|
||||
show_nav_help: helpCb ? !!helpCb.checked : true,
|
||||
show_nav_logs: logsCb ? !!logsCb.checked : true,
|
||||
show_account_pnl: chk("pref-show-account-pnl"),
|
||||
show_nav_funds: chk("pref-show-nav-funds"),
|
||||
show_nav_dashboard: chk("pref-show-nav-dashboard"),
|
||||
show_nav_plan: chk("pref-show-nav-plan"),
|
||||
show_nav_archive: chk("pref-show-nav-archive"),
|
||||
show_nav_quotes: chk("pref-show-nav-quotes"),
|
||||
show_nav_ai: chk("pref-show-nav-ai"),
|
||||
show_nav_calculator: chk("pref-show-nav-calculator"),
|
||||
show_nav_compare: chk("pref-show-nav-compare"),
|
||||
show_nav_strategy: chk("pref-show-nav-strategy"),
|
||||
show_nav_amp_stats: chk("pref-show-nav-amp-stats"),
|
||||
show_nav_help: chk("pref-show-nav-help"),
|
||||
show_nav_logs: chk("pref-show-nav-logs"),
|
||||
show_monitor_binance: chk("pref-show-monitor-binance"),
|
||||
show_monitor_okx_perp: chk("pref-show-monitor-okx-perp"),
|
||||
show_monitor_okx_options: chk("pref-show-monitor-okx-options"),
|
||||
show_monitor_gate: chk("pref-show-monitor-gate"),
|
||||
show_strategy_playbook_v2: chk("pref-show-strategy-playbook-v2"),
|
||||
show_strategy_playbook: chk("pref-show-strategy-playbook"),
|
||||
show_strategy_behavior: chk("pref-show-strategy-behavior"),
|
||||
show_strategy_binance: chk("pref-show-strategy-binance"),
|
||||
show_strategy_okx: chk("pref-show-strategy-okx"),
|
||||
show_strategy_gate: chk("pref-show-strategy-gate"),
|
||||
},
|
||||
supervisor: {
|
||||
enabled: supEnabled ? !!supEnabled.checked : true,
|
||||
@@ -5210,6 +5258,9 @@
|
||||
if (window.hubDashboardPage && window.hubDashboardPage.refresh) {
|
||||
window.hubDashboardPage.refresh();
|
||||
}
|
||||
if (window.hubStrategyPage && typeof window.hubStrategyPage.reloadMeta === "function") {
|
||||
window.hubStrategyPage.reloadMeta();
|
||||
}
|
||||
if (!pageNavAllowed(currentPage())) {
|
||||
history.replaceState({}, "", "/monitor");
|
||||
setActiveNav();
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
const elQuoteContent = document.getElementById("archive-quote-content");
|
||||
const elQuoteSubmit = document.getElementById("archive-quote-submit");
|
||||
const elContentTabs = document.getElementById("archive-content-tabs");
|
||||
const elProductTabs = document.getElementById("archive-product-tabs");
|
||||
const elPanelViz = document.getElementById("archive-panel-viz");
|
||||
const elPanelCalendar = document.getElementById("archive-panel-calendar");
|
||||
const elPanelTrades = document.getElementById("archive-panel-trades");
|
||||
@@ -76,6 +77,7 @@
|
||||
let selectedQuoteId = null;
|
||||
let editingQuoteId = null;
|
||||
let archiveContentTab = "trades";
|
||||
let archiveProduct = "perp";
|
||||
let quoteDayTrades = [];
|
||||
let quoteDayTradesDay = "";
|
||||
let quoteDayTradesReq = 0;
|
||||
@@ -416,6 +418,44 @@
|
||||
syncPeriodUI();
|
||||
}
|
||||
|
||||
function isOptionsProduct() {
|
||||
return archiveProduct === "options";
|
||||
}
|
||||
|
||||
function syncProductUI() {
|
||||
document.body.classList.toggle("archive-product-options", isOptionsProduct());
|
||||
if (elProductTabs) {
|
||||
elProductTabs.querySelectorAll(".archive-product-tab").forEach(function (btn) {
|
||||
const on = btn.getAttribute("data-archive-product") === archiveProduct;
|
||||
btn.classList.toggle("is-active", on);
|
||||
btn.setAttribute("aria-selected", on ? "true" : "false");
|
||||
});
|
||||
}
|
||||
if (isOptionsProduct()) {
|
||||
setChartOpen(false);
|
||||
if (archiveContentTab === "viz") setArchiveContentTab("trades");
|
||||
}
|
||||
}
|
||||
|
||||
function setArchiveProduct(product) {
|
||||
const next = product === "options" ? "options" : "perp";
|
||||
if (next === archiveProduct) return;
|
||||
archiveProduct = next;
|
||||
selected = null;
|
||||
selectedTradeKey = null;
|
||||
syncProductUI();
|
||||
void loadDailyTrades();
|
||||
void loadCalendar();
|
||||
}
|
||||
|
||||
function dailyTradesApiPath() {
|
||||
return isOptionsProduct() ? "/api/archive/options/daily-trades" : "/api/archive/daily-trades";
|
||||
}
|
||||
|
||||
function calendarApiPath() {
|
||||
return isOptionsProduct() ? "/api/archive/options/calendar" : "/api/archive/calendar";
|
||||
}
|
||||
|
||||
function queryDailyParams() {
|
||||
const q = new URLSearchParams();
|
||||
q.set("period", periodMode);
|
||||
@@ -430,7 +470,7 @@
|
||||
if (ex) q.set("exchange_key", ex);
|
||||
if (elFilterProfit && elFilterProfit.checked) q.set("filter_profit", "1");
|
||||
if (elFilterLoss && elFilterLoss.checked) q.set("filter_loss", "1");
|
||||
if (elFilterSick && elFilterSick.checked) q.set("filter_sick", "1");
|
||||
if (!isOptionsProduct() && elFilterSick && elFilterSick.checked) q.set("filter_sick", "1");
|
||||
if (elSearch && elSearch.value.trim()) q.set("search", elSearch.value.trim());
|
||||
return q.toString();
|
||||
}
|
||||
@@ -554,7 +594,7 @@
|
||||
return q;
|
||||
},
|
||||
fetchFn: async function (q) {
|
||||
const r = await apiFetch("/api/archive/calendar?" + q.toString());
|
||||
const r = await apiFetch(calendarApiPath() + "?" + q.toString());
|
||||
return r.json();
|
||||
},
|
||||
parseResponse: function (data) {
|
||||
@@ -1089,7 +1129,7 @@
|
||||
elQuoteDayTradesBody.innerHTML = '<p class="archive-empty">加载当日已平仓…</p>';
|
||||
if (elQuoteDayTradesMeta) elQuoteDayTradesMeta.textContent = day;
|
||||
try {
|
||||
const r = await apiFetch("/api/archive/daily-trades?" + q.toString());
|
||||
const r = await apiFetch(dailyTradesApiPath() + "?" + q.toString());
|
||||
const j = await r.json();
|
||||
if (req !== quoteDayTradesReq) return;
|
||||
if (!r.ok) {
|
||||
@@ -1827,6 +1867,80 @@
|
||||
return;
|
||||
}
|
||||
const pageRows = pagedDailyTrades();
|
||||
if (isOptionsProduct()) {
|
||||
elTrades.innerHTML =
|
||||
'<table class="archive-trades-table"><thead><tr>' +
|
||||
"<th>交易所</th><th>标的</th><th>合约/来源</th><th>开仓时间</th><th>平仓时间</th><th>持仓</th>" +
|
||||
"<th>类型</th><th>策略</th><th>盈亏</th><th>权利金</th><th>复盘</th>" +
|
||||
"</tr></thead><tbody>" +
|
||||
pageRows
|
||||
.map(function (t) {
|
||||
const rowKey = tradeRowKey(t);
|
||||
const active = rowKey && rowKey === selectedTradeKey ? " is-active" : "";
|
||||
const holdMin =
|
||||
t.hold_minutes != null
|
||||
? t.hold_minutes
|
||||
: t.hold_seconds != null
|
||||
? Number(t.hold_seconds) / 60
|
||||
: null;
|
||||
const optLabel =
|
||||
t.source_label ||
|
||||
t.source_type ||
|
||||
(t.opt_type === "C" || t.opt_type === "CALL"
|
||||
? "Call"
|
||||
: t.opt_type === "P" || t.opt_type === "PUT"
|
||||
? "Put"
|
||||
: "—");
|
||||
const pnl = t.pnl_amount != null ? t.pnl_amount : t.realized_pnl_total;
|
||||
return (
|
||||
'<tr class="archive-trade-row' +
|
||||
active +
|
||||
'" data-key="' +
|
||||
esc(rowKey) +
|
||||
'">' +
|
||||
"<td>" +
|
||||
esc(tradeRowExchange(t)) +
|
||||
"</td>" +
|
||||
'<td class="archive-symbol">' +
|
||||
esc(t.underlying || "—") +
|
||||
"</td>" +
|
||||
"<td>" +
|
||||
esc(t.inst_id || t.source_label || "—") +
|
||||
"</td>" +
|
||||
'<td class="archive-dt">' +
|
||||
fmtDt(t.opened_at) +
|
||||
"</td>" +
|
||||
'<td class="archive-dt">' +
|
||||
fmtDt(t.closed_at) +
|
||||
"</td>" +
|
||||
'<td class="archive-hold">' +
|
||||
fmtDurationMinutes(holdMin) +
|
||||
"</td>" +
|
||||
"<td>" +
|
||||
esc(optLabel) +
|
||||
"</td>" +
|
||||
"<td>" +
|
||||
esc(t.strategy_tag || "—") +
|
||||
"</td>" +
|
||||
'<td class="' +
|
||||
pnlClass(pnl) +
|
||||
'">' +
|
||||
fmtPnl(pnl) +
|
||||
"</td>" +
|
||||
"<td>" +
|
||||
fmtVolStat(t.premium_total != null ? t.premium_total : t.premium_paid) +
|
||||
"</td>" +
|
||||
"<td>" +
|
||||
(t.reviewed ? "已复盘" : "—") +
|
||||
"</td>" +
|
||||
"</tr>"
|
||||
);
|
||||
})
|
||||
.join("") +
|
||||
"</tbody></table>";
|
||||
updateTradesPager();
|
||||
return;
|
||||
}
|
||||
elTrades.innerHTML =
|
||||
'<table class="archive-trades-table"><thead><tr>' +
|
||||
"<th>交易所</th><th>合约</th><th>开仓类型</th><th>开仓时间</th><th>平仓时间</th><th>持仓时长</th>" +
|
||||
@@ -2051,7 +2165,7 @@
|
||||
|
||||
async function loadDailyTrades() {
|
||||
setStatus("加载交易记录…");
|
||||
const r = await apiFetch("/api/archive/daily-trades?" + queryDailyParams());
|
||||
const r = await apiFetch(dailyTradesApiPath() + "?" + queryDailyParams());
|
||||
const j = await r.json();
|
||||
if (!r.ok) {
|
||||
setStatus(j.detail || "加载失败");
|
||||
@@ -2080,7 +2194,8 @@
|
||||
void loadCalendar();
|
||||
if (archiveContentTab === "quotes") void loadQuoteDayTrades();
|
||||
setStatus(
|
||||
(periodLabel || tradingDay || "当日") +
|
||||
(isOptionsProduct() ? "期权 · " : "永续 · ") +
|
||||
(periodLabel || tradingDay || "当日") +
|
||||
" · 列表 " +
|
||||
dailyTrades.length +
|
||||
" 笔 · " +
|
||||
@@ -2105,6 +2220,7 @@
|
||||
|
||||
function formatSyncSummary(j) {
|
||||
const results = j.results || [];
|
||||
const optResults = j.options_results || [];
|
||||
const okN = results.filter(function (x) {
|
||||
return x.ok !== false;
|
||||
}).length;
|
||||
@@ -2118,6 +2234,19 @@
|
||||
parts.push(line);
|
||||
}
|
||||
});
|
||||
optResults.forEach(function (row) {
|
||||
const label = (row.exchange_key || row.name || "?") + "期权";
|
||||
if (row.ok === false) parts.push(label + " 失败: " + (row.msg || "未知错误"));
|
||||
else {
|
||||
let line =
|
||||
label +
|
||||
" " +
|
||||
(row.trade_count != null ? row.trade_count : row.trades_upserted || 0) +
|
||||
" 笔";
|
||||
if (row.trades_removed > 0) line += " 清" + row.trades_removed;
|
||||
parts.push(line);
|
||||
}
|
||||
});
|
||||
return parts.join(" · ");
|
||||
}
|
||||
|
||||
@@ -2217,6 +2346,13 @@
|
||||
setArchiveContentTab(btn.getAttribute("data-archive-tab") || "trades");
|
||||
});
|
||||
}
|
||||
if (elProductTabs) {
|
||||
elProductTabs.addEventListener("click", function (ev) {
|
||||
const btn = ev.target.closest(".archive-product-tab");
|
||||
if (!btn) return;
|
||||
setArchiveProduct(btn.getAttribute("data-archive-product") || "perp");
|
||||
});
|
||||
}
|
||||
if (elTfTabs) {
|
||||
elTfTabs.addEventListener("click", function (ev) {
|
||||
const btn = ev.target.closest(".archive-tf-btn");
|
||||
@@ -2249,6 +2385,7 @@
|
||||
syncPeriodUI();
|
||||
syncTradesLayout();
|
||||
bindEvents();
|
||||
syncProductUI();
|
||||
setArchiveContentTab("trades");
|
||||
inited = true;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
|
||||
<noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript>
|
||||
<link rel="stylesheet" href="/assets/app.css?v=20260723-cmp-pad" />
|
||||
<link rel="stylesheet" href="/assets/app.css?v=20260724-opt-archive" />
|
||||
<link rel="stylesheet" href="/assets/trade_stats_calendar.css?v=4" />
|
||||
<link rel="stylesheet" href="/assets/account_risk_badge.css?v=4" />
|
||||
<script src="/assets/account_risk_badge.js?v=4"></script>
|
||||
@@ -451,7 +451,11 @@
|
||||
<div id="page-archive" class="page hidden">
|
||||
<div class="page-head">
|
||||
<h1><span class="head-tag">IN</span> 内照明心</h1>
|
||||
<p class="page-desc">交易记录 · 交易日历 · 图表概览 · 复盘语录</p>
|
||||
<p class="page-desc">永续 / 期权交易记录 · 交易日历 · 图表概览 · 复盘语录</p>
|
||||
</div>
|
||||
<div class="archive-product-tabs" id="archive-product-tabs" role="tablist" aria-label="品种">
|
||||
<button type="button" class="archive-product-tab is-active" role="tab" aria-selected="true" data-archive-product="perp">永续</button>
|
||||
<button type="button" class="archive-product-tab" role="tab" aria-selected="false" data-archive-product="options">期权</button>
|
||||
</div>
|
||||
<div class="archive-toolbar toolbar">
|
||||
<label class="chk-label archive-toolbar-desktop"><input type="checkbox" id="archive-filter-profit" /> 盈利单</label>
|
||||
@@ -1081,7 +1085,7 @@
|
||||
<div class="page-head strategy-page-head">
|
||||
<div>
|
||||
<h1><span class="head-tag">STR</span> 策略说明</h1>
|
||||
<p class="page-desc">执行手册 · 三所策略正文(带目录) · 执行清单(打印对照)</p>
|
||||
<p class="page-desc">执行手册v2(无对冲)· 行为准则 · 三所策略正文 · 执行清单</p>
|
||||
</div>
|
||||
<div class="strategy-page-actions no-print">
|
||||
<button type="button" id="strategy-btn-download" class="ghost">下载 HTML</button>
|
||||
@@ -1382,6 +1386,50 @@
|
||||
<input type="checkbox" id="pref-show-nav-logs" checked />
|
||||
顶栏显示「系统日志」
|
||||
</label>
|
||||
<p class="settings-display-subtitle">监控区卡片</p>
|
||||
<p class="settings-display-hint">仅隐藏监控区界面卡片,不关闭账户与后台拉取.例:只做 OKX 期权与 Gate 时可关掉币安与 OKX 永续.</p>
|
||||
<label class="chk-label settings-display-chk">
|
||||
<input type="checkbox" id="pref-show-monitor-binance" checked />
|
||||
监控区显示「币安」
|
||||
</label>
|
||||
<label class="chk-label settings-display-chk">
|
||||
<input type="checkbox" id="pref-show-monitor-okx-perp" checked />
|
||||
监控区显示「OKX 永续」
|
||||
</label>
|
||||
<label class="chk-label settings-display-chk">
|
||||
<input type="checkbox" id="pref-show-monitor-okx-options" checked />
|
||||
监控区显示「OKX 期权」
|
||||
</label>
|
||||
<label class="chk-label settings-display-chk">
|
||||
<input type="checkbox" id="pref-show-monitor-gate" checked />
|
||||
监控区显示「Gate」
|
||||
</label>
|
||||
<p class="settings-display-subtitle">策略说明页签</p>
|
||||
<p class="settings-display-hint">关闭后该页签从策略说明中消失;顶栏「策略说明」入口仍由上方导航开关控制.</p>
|
||||
<label class="chk-label settings-display-chk">
|
||||
<input type="checkbox" id="pref-show-strategy-playbook-v2" checked />
|
||||
策略说明显示「执行手册v2」
|
||||
</label>
|
||||
<label class="chk-label settings-display-chk">
|
||||
<input type="checkbox" id="pref-show-strategy-playbook" checked />
|
||||
策略说明显示「执行手册v1」
|
||||
</label>
|
||||
<label class="chk-label settings-display-chk">
|
||||
<input type="checkbox" id="pref-show-strategy-behavior" checked />
|
||||
策略说明显示「行为准则」
|
||||
</label>
|
||||
<label class="chk-label settings-display-chk">
|
||||
<input type="checkbox" id="pref-show-strategy-binance" checked />
|
||||
策略说明显示「币安」
|
||||
</label>
|
||||
<label class="chk-label settings-display-chk">
|
||||
<input type="checkbox" id="pref-show-strategy-okx" checked />
|
||||
策略说明显示「OKX」
|
||||
</label>
|
||||
<label class="chk-label settings-display-chk">
|
||||
<input type="checkbox" id="pref-show-strategy-gate" checked />
|
||||
策略说明显示「Gate」
|
||||
</label>
|
||||
<p class="settings-display-hint">保存至 hub_settings.json,换浏览器同样生效.关闭导航后对应页面将不可从顶栏进入,直接访问 URL 会跳回监控区.</p>
|
||||
</section>
|
||||
|
||||
@@ -1629,11 +1677,11 @@
|
||||
<script src="/assets/calculator.js?v=20260715-calc-tabs"></script>
|
||||
<script src="/assets/compare.js?v=20260723-compare"></script>
|
||||
<script src="/assets/trade_stats_calendar.js?v=3"></script>
|
||||
<script src="/assets/archive.js?v=20260717-archive-cal-chart"></script>
|
||||
<script src="/assets/archive.js?v=20260724-opt-archive"></script>
|
||||
<script src="/assets/quotes.js?v=20260717-quotes-feed"></script>
|
||||
<script src="/assets/funds.js?v=20260717-funds-scroll-fix"></script>
|
||||
<script src="/assets/dashboard.js?v=20260723-hide-pnl"></script>
|
||||
<script src="/assets/strategy.js?v=9"></script>
|
||||
<script src="/assets/strategy.js?v=11"></script>
|
||||
<script src="/assets/amp_stats.js?v=5"></script>
|
||||
<script src="/assets/help.js?v=1"></script>
|
||||
<script src="/assets/logs.js?v=1"></script>
|
||||
@@ -1642,6 +1690,6 @@
|
||||
<script src="/assets/options_expiry_countdown.js?v=1"></script>
|
||||
<script src="/assets/options_position_cards.js?v=3"></script>
|
||||
<script src="/assets/backup.js?v=1"></script>
|
||||
<script src="/assets/app.js?v=20260723-compare"></script>
|
||||
<script src="/assets/app.js?v=20260724-display-hide"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
const btnPrintChecklistInline = document.getElementById("strategy-btn-print-checklist-inline");
|
||||
const btnDownload = document.getElementById("strategy-btn-download");
|
||||
|
||||
let activeKey = "playbook";
|
||||
let activeKey = "playbook_v2";
|
||||
let activeView = "doc";
|
||||
let tabsMeta = [];
|
||||
let cache = {};
|
||||
@@ -74,6 +74,14 @@
|
||||
|
||||
function renderExchangeTabs() {
|
||||
if (!tabsEl) return;
|
||||
if (!tabsMeta.length) {
|
||||
tabsEl.innerHTML = "";
|
||||
if (statusEl) statusEl.textContent = "当前无可显示的策略页签(可在系统设置·显示与导航中开启)";
|
||||
if (docBody) docBody.innerHTML = "";
|
||||
if (docToc) docToc.innerHTML = "";
|
||||
if (checklistBody) checklistBody.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
tabsEl.innerHTML = tabsMeta
|
||||
.map(
|
||||
(t) =>
|
||||
@@ -274,6 +282,15 @@
|
||||
renderExchangeTabs();
|
||||
}
|
||||
|
||||
async function reloadMeta() {
|
||||
try {
|
||||
await loadMeta();
|
||||
if (tabsMeta.length) await loadExchange(activeKey);
|
||||
} catch (e) {
|
||||
if (statusEl) statusEl.textContent = String(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function printSection(mode) {
|
||||
const part = mode === "checklist" ? "checklist" : "doc";
|
||||
const url = `/api/strategy/${encodeURIComponent(activeKey)}/print?part=${encodeURIComponent(part)}`;
|
||||
@@ -334,7 +351,8 @@
|
||||
setView(activeView);
|
||||
try {
|
||||
await loadMeta();
|
||||
await loadExchange(activeKey);
|
||||
if (tabsMeta.length) await loadExchange(activeKey);
|
||||
else if (statusEl) statusEl.textContent = "当前无可显示的策略页签(可在系统设置·显示与导航中开启)";
|
||||
} catch (e) {
|
||||
if (statusEl) statusEl.textContent = String(e);
|
||||
}
|
||||
@@ -347,5 +365,5 @@
|
||||
}
|
||||
}
|
||||
|
||||
window.hubStrategyPage = { init, destroy };
|
||||
window.hubStrategyPage = { init, destroy, reloadMeta };
|
||||
})();
|
||||
|
||||
@@ -0,0 +1,674 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate business-style XMind (Zen/2020+) from playbook + behavior rules."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import uuid
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
OUT = Path(__file__).resolve().parents[1] / "docs" / "交易执行手册与行为准则.xmind"
|
||||
|
||||
# 商务配色:深蓝主调 + 灰蓝辅色 + 强调色
|
||||
C_ROOT = "#0F2942"
|
||||
C_L1 = "#1B4F72"
|
||||
C_L2 = "#2E86AB"
|
||||
C_PASS = "#1E8449"
|
||||
C_FAIL = "#922B21"
|
||||
C_WARN = "#B9770E"
|
||||
C_MUTED = "#566573"
|
||||
C_TEXT = "#FFFFFF"
|
||||
C_TEXT_DARK = "#1C2833"
|
||||
|
||||
|
||||
def tid() -> str:
|
||||
return uuid.uuid4().hex[:26]
|
||||
|
||||
|
||||
def style(
|
||||
*,
|
||||
fill: Optional[str] = None,
|
||||
color: Optional[str] = None,
|
||||
font_size: str = "12pt",
|
||||
bold: bool = False,
|
||||
shape: str = "org.xmind.topicShape.roundedRect",
|
||||
line: Optional[str] = None,
|
||||
) -> dict[str, Any]:
|
||||
props: dict[str, str] = {
|
||||
"shape-class": shape,
|
||||
"fo:font-family": "Microsoft YaHei",
|
||||
"fo:font-size": font_size,
|
||||
"fo:font-weight": "bold" if bold else "normal",
|
||||
"border-line-width": "0pt",
|
||||
"line-width": "1.5pt",
|
||||
"line-class": "org.xmind.branchConnection.roundedelbow",
|
||||
}
|
||||
if fill:
|
||||
props["svg:fill"] = fill
|
||||
if color:
|
||||
props["fo:color"] = color
|
||||
if line:
|
||||
props["line-color"] = line
|
||||
return {"id": tid(), "properties": props}
|
||||
|
||||
|
||||
def topic(
|
||||
title: str,
|
||||
children: list | None = None,
|
||||
*,
|
||||
markers: list[str] | None = None,
|
||||
labels: list[str] | None = None,
|
||||
notes: str | None = None,
|
||||
fill: Optional[str] = None,
|
||||
color: Optional[str] = None,
|
||||
font_size: str = "12pt",
|
||||
bold: bool = False,
|
||||
line: Optional[str] = None,
|
||||
) -> dict:
|
||||
node: dict[str, Any] = {
|
||||
"id": tid(),
|
||||
"class": "topic",
|
||||
"title": title,
|
||||
"style": style(
|
||||
fill=fill, color=color, font_size=font_size, bold=bold, line=line
|
||||
),
|
||||
}
|
||||
if markers:
|
||||
node["markers"] = [{"markerId": m} for m in markers]
|
||||
if labels:
|
||||
node["labels"] = labels
|
||||
if notes:
|
||||
node["notes"] = {"plain": {"content": notes}}
|
||||
if children:
|
||||
node["children"] = {"attached": children}
|
||||
return node
|
||||
|
||||
|
||||
def t1(title: str, children: list, markers: list[str], label: str) -> dict:
|
||||
return topic(
|
||||
title,
|
||||
children,
|
||||
markers=markers,
|
||||
labels=[label],
|
||||
fill=C_L1,
|
||||
color=C_TEXT,
|
||||
font_size="16pt",
|
||||
bold=True,
|
||||
line=C_L1,
|
||||
)
|
||||
|
||||
|
||||
def t2(title: str, children: list | None = None, markers: list[str] | None = None) -> dict:
|
||||
return topic(
|
||||
title,
|
||||
children,
|
||||
markers=markers or ["flag-dark-blue"],
|
||||
fill=C_L2,
|
||||
color=C_TEXT,
|
||||
font_size="13pt",
|
||||
bold=True,
|
||||
line=C_L2,
|
||||
)
|
||||
|
||||
|
||||
def leaf(
|
||||
title: str,
|
||||
*,
|
||||
markers: list[str] | None = None,
|
||||
fill: Optional[str] = None,
|
||||
color: Optional[str] = C_TEXT_DARK,
|
||||
) -> dict:
|
||||
return topic(
|
||||
title,
|
||||
markers=markers or ["symbol-right"],
|
||||
fill=fill or "#EBF5FB",
|
||||
color=color,
|
||||
font_size="11pt",
|
||||
line="#AED6F1",
|
||||
)
|
||||
|
||||
|
||||
def ok(title: str) -> dict:
|
||||
return leaf(title, markers=["other-yes", "symbol-right"], fill="#E8F8F5", color=C_PASS)
|
||||
|
||||
|
||||
def no(title: str) -> dict:
|
||||
return leaf(title, markers=["other-no", "flag-gray"], fill="#FDEDEC", color=C_FAIL)
|
||||
|
||||
|
||||
def warn(title: str) -> dict:
|
||||
return leaf(title, markers=["symbol-info"], fill="#FEF9E7", color=C_WARN)
|
||||
|
||||
|
||||
def build_content() -> list:
|
||||
root = topic(
|
||||
"交易执行体系\n手册 v2 · 开单三检",
|
||||
[
|
||||
t1(
|
||||
"① 设计理念",
|
||||
[
|
||||
t2(
|
||||
"核心主张",
|
||||
[
|
||||
leaf("少而精,珍惜机会,样本干净", markers=["star-dark-blue"]),
|
||||
leaf("不保证收益;过程可控,结果随缘", markers=["symbol-info"]),
|
||||
leaf("过滤比频率重要;日更不是目标", markers=["symbol-info"]),
|
||||
leaf("看不懂不做;不为开单找理由", markers=["symbol-info"]),
|
||||
warn("丢掉对冲:无「有保护就能多做」幻觉"),
|
||||
],
|
||||
markers=["other-lightbulb"],
|
||||
),
|
||||
t2(
|
||||
"工具边界",
|
||||
[
|
||||
leaf("OKX 期权:方向单(虚值等)", markers=["flag-blue"]),
|
||||
leaf("Gate 合约:结构清楚时的波段", markers=["flag-dark-blue"]),
|
||||
leaf("同一时段尽量只让一边说话", markers=["symbol-equality"]),
|
||||
no("不做期期对冲 / 偏置壳"),
|
||||
],
|
||||
markers=["symbol-info"],
|
||||
),
|
||||
t2(
|
||||
"文档分工",
|
||||
[
|
||||
leaf("行为准则:能不能动手(防火墙)", markers=["other-lock"]),
|
||||
leaf("执行手册:怎么做单(玩法/仓位/离场)", markers=["other-note"]),
|
||||
],
|
||||
markers=["other-businesscard"],
|
||||
),
|
||||
],
|
||||
markers=["priority-1", "other-lightbulb"],
|
||||
label="理念",
|
||||
),
|
||||
t1(
|
||||
"② 资金要求",
|
||||
[
|
||||
t2(
|
||||
"总盘约 800U",
|
||||
[
|
||||
leaf("单笔约 1.25% 量级", markers=["symbol-info"]),
|
||||
leaf("全错一天约 2.5% 量级——防守优先", markers=["symbol-info"]),
|
||||
],
|
||||
markers=["other-businesscard"],
|
||||
),
|
||||
t2(
|
||||
"单笔期权",
|
||||
[
|
||||
leaf("约 10U 权利金预算", markers=["priority-1"]),
|
||||
leaf("一次只持有一个期权仓位", markers=["symbol-info"]),
|
||||
leaf("打满 = min(余额, 单笔预算)", markers=["symbol-equality"]),
|
||||
],
|
||||
markers=["flag-blue"],
|
||||
),
|
||||
t2(
|
||||
"Gate 合约",
|
||||
[
|
||||
leaf("日内保证金约 50U · 约 10 倍", markers=["symbol-info"]),
|
||||
leaf("止损一般约 5U", markers=["symbol-info"]),
|
||||
leaf("单笔最大亏损不超过约 10U", markers=["flag-gray"]),
|
||||
leaf("有单才用保证金,无单为 0", markers=["task-done"]),
|
||||
],
|
||||
markers=["flag-dark-blue"],
|
||||
),
|
||||
t2(
|
||||
"日损失心理框",
|
||||
[
|
||||
warn("都错:合计大约 ≤20U"),
|
||||
ok("都对:期望可到 40U+(理想,非每日目标)"),
|
||||
no("不为「好像有保护」放大仓位"),
|
||||
warn("尽量少同向双开;双开按合计最坏约 20U"),
|
||||
],
|
||||
markers=["symbol-info"],
|
||||
),
|
||||
],
|
||||
markers=["priority-2", "other-businesscard"],
|
||||
label="资金",
|
||||
),
|
||||
t1(
|
||||
"③ 操盘思路",
|
||||
[
|
||||
t2(
|
||||
"行为准则 · 开单三检",
|
||||
[
|
||||
topic(
|
||||
"一句话防火墙",
|
||||
[
|
||||
leaf("信号够不够清晰?", markers=["symbol-question"]),
|
||||
leaf("流程有没有跑通?", markers=["symbol-question"]),
|
||||
leaf("情绪是不是在证明自己?", markers=["symbol-question"]),
|
||||
no("三检不过 → 不开"),
|
||||
],
|
||||
markers=["other-lock", "priority-1"],
|
||||
fill="#154360",
|
||||
color=C_TEXT,
|
||||
font_size="12pt",
|
||||
bold=True,
|
||||
labels=["防火墙"],
|
||||
),
|
||||
topic(
|
||||
"总循环",
|
||||
[
|
||||
leaf("信号判断 → 流程确认 → 情绪自检", markers=["arrow-right"]),
|
||||
leaf("全部通过 → 开仓", markers=["other-yes"]),
|
||||
leaf("等待系统结果(止盈/止损/到期)", markers=["other-clock"]),
|
||||
leaf("复盘整环 → 等待下一信号", markers=["arrow-refresh"]),
|
||||
no("任一步否决 → 空仓离开"),
|
||||
],
|
||||
markers=["arrow-right"],
|
||||
fill="#1A5276",
|
||||
color=C_TEXT,
|
||||
font_size="12pt",
|
||||
bold=True,
|
||||
),
|
||||
topic(
|
||||
"开单前三秒停顿",
|
||||
[
|
||||
leaf("核心信号是什么?", markers=["symbol-info"]),
|
||||
leaf("安全流程跑通了吗?", markers=["task-start"]),
|
||||
leaf("冷静执行,还是怕踏空/回本/证明自己?", markers=["symbol-info"]),
|
||||
],
|
||||
markers=["other-clock"],
|
||||
fill="#1A5276",
|
||||
color=C_TEXT,
|
||||
font_size="12pt",
|
||||
bold=True,
|
||||
),
|
||||
topic(
|
||||
"检1 · 信号判断",
|
||||
[
|
||||
ok("一句话说清唯一核心确认"),
|
||||
ok("点位/结构本身已够清楚"),
|
||||
no("说不清、靠宏观故事自圆"),
|
||||
no("「好像有戏」但确认模糊"),
|
||||
leaf("对照:1H→空间→结构→定损盈→工具", markers=["arrow-right"]),
|
||||
],
|
||||
markers=["priority-1", "symbol-info"],
|
||||
fill="#1A5276",
|
||||
color=C_TEXT,
|
||||
font_size="12pt",
|
||||
bold=True,
|
||||
labels=["Signal"],
|
||||
),
|
||||
topic(
|
||||
"检2 · 流程确认",
|
||||
[
|
||||
ok("资金与当日额度符合"),
|
||||
ok("单笔/组合敞口在预算内"),
|
||||
no("资金或次数已触限"),
|
||||
no("单笔或日最坏超限 → 暂停"),
|
||||
no("「先开了再说」跳步"),
|
||||
],
|
||||
markers=["priority-2", "task-start"],
|
||||
fill="#1A5276",
|
||||
color=C_TEXT,
|
||||
font_size="12pt",
|
||||
bold=True,
|
||||
labels=["Process"],
|
||||
),
|
||||
topic(
|
||||
"检3 · 情绪自检",
|
||||
[
|
||||
ok("符合系统 + 账户没问题 → 开"),
|
||||
ok("可接受空仓,旁观者视角"),
|
||||
no("怕踏空"),
|
||||
no("上回亏了要回本"),
|
||||
no("必须证明我是对的"),
|
||||
warn("红灯亮了,信号再好看也不开"),
|
||||
],
|
||||
markers=["priority-3", "smiley-smile"],
|
||||
fill="#1A5276",
|
||||
color=C_TEXT,
|
||||
font_size="12pt",
|
||||
bold=True,
|
||||
labels=["Emotion"],
|
||||
),
|
||||
topic(
|
||||
"复盘只记什么",
|
||||
[
|
||||
leaf("信号:是否做了?核心写了什么?", markers=["other-note"]),
|
||||
leaf("流程:资金/敞口是否过关?有无跳步?", markers=["other-note"]),
|
||||
leaf("情绪:当时是哪一类心态?", markers=["other-note"]),
|
||||
warn("结果不推翻「三检是否完成」评分"),
|
||||
],
|
||||
markers=["other-note"],
|
||||
fill="#1A5276",
|
||||
color=C_TEXT,
|
||||
font_size="12pt",
|
||||
bold=True,
|
||||
),
|
||||
],
|
||||
markers=["other-lock", "flag-purple"],
|
||||
),
|
||||
t2(
|
||||
"开仓逻辑",
|
||||
[
|
||||
topic(
|
||||
"主链条(强制)",
|
||||
[
|
||||
leaf("1H 方向:明显 N 字;跟 1H 波段", markers=["priority-1"]),
|
||||
leaf("空间:空看支撑、多看阻力;≥约 2%", markers=["priority-2"]),
|
||||
leaf("结构:15m/5m;量级约 8h+(约 48×15m)", markers=["priority-3"]),
|
||||
leaf("定损盈:外沿/针尖;RR 须接受", markers=["priority-4"]),
|
||||
leaf("选工具:期权 或 合约(不对冲)", markers=["priority-5"]),
|
||||
no("任一步不过 → 空仓等待"),
|
||||
],
|
||||
markers=["arrow-right", "symbol-info"],
|
||||
fill="#154360",
|
||||
color=C_TEXT,
|
||||
font_size="12pt",
|
||||
bold=True,
|
||||
labels=["主链"],
|
||||
),
|
||||
topic(
|
||||
"结构形态参考",
|
||||
[
|
||||
leaf("收敛", markers=["flag-blue"]),
|
||||
leaf("两段式回调", markers=["flag-dark-blue"]),
|
||||
leaf("箱体", markers=["flag-gray"]),
|
||||
leaf("假突破", markers=["flag-orange"]),
|
||||
],
|
||||
markers=["symbol-image"],
|
||||
fill="#1A5276",
|
||||
color=C_TEXT,
|
||||
font_size="12pt",
|
||||
bold=True,
|
||||
),
|
||||
topic(
|
||||
"期权入场",
|
||||
[
|
||||
ok("主链条全过;结构突破/假突破成立"),
|
||||
leaf("一天期方向单;空间够优先虚值", markers=["star-blue"]),
|
||||
leaf("默认先只开期权,不上合约", markers=["symbol-info"]),
|
||||
leaf("尽量 16:00 后开次日到期", markers=["other-clock"]),
|
||||
no("不做:期期对冲、偏置壳、为开而开"),
|
||||
],
|
||||
markers=["flag-blue", "symbol-plus"],
|
||||
fill="#1A5276",
|
||||
color=C_TEXT,
|
||||
font_size="12pt",
|
||||
bold=True,
|
||||
labels=["期权"],
|
||||
),
|
||||
topic(
|
||||
"合约入场(Gate)",
|
||||
[
|
||||
ok("主链条过关;位置极明确"),
|
||||
leaf("想清进场:假突破 / 结构突破", markers=["symbol-info"]),
|
||||
leaf("止损挂模型位(外沿/针尖)", markers=["symbol-info"]),
|
||||
warn("独立假突破:只做合约或空仓"),
|
||||
no("勿与「突破期权后再加仓」混仓"),
|
||||
],
|
||||
markers=["flag-dark-blue", "symbol-plus"],
|
||||
fill="#1A5276",
|
||||
color=C_TEXT,
|
||||
font_size="12pt",
|
||||
bold=True,
|
||||
labels=["合约"],
|
||||
),
|
||||
],
|
||||
markers=["symbol-plus", "arrow-up-right"],
|
||||
),
|
||||
t2(
|
||||
"平仓逻辑",
|
||||
[
|
||||
topic(
|
||||
"期权离场",
|
||||
[
|
||||
ok("只认:系统/规则止盈"),
|
||||
ok("只认:到期"),
|
||||
no("开仓后中间不手动平仓"),
|
||||
warn("紧急手平 → 标记非策略样本"),
|
||||
],
|
||||
markers=["flag-green", "symbol-minus"],
|
||||
fill="#145A32",
|
||||
color=C_TEXT,
|
||||
font_size="12pt",
|
||||
bold=True,
|
||||
labels=["期权"],
|
||||
),
|
||||
topic(
|
||||
"合约离场",
|
||||
[
|
||||
ok("结构止盈为准"),
|
||||
ok("结构止损为准(约 5U 量级)"),
|
||||
leaf("等待系统/挂单结果,不情绪手平", markers=["other-clock"]),
|
||||
],
|
||||
markers=["flag-dark-green", "symbol-minus"],
|
||||
fill="#145A32",
|
||||
color=C_TEXT,
|
||||
font_size="12pt",
|
||||
bold=True,
|
||||
labels=["合约"],
|
||||
),
|
||||
topic(
|
||||
"持仓期盯什么",
|
||||
[
|
||||
leaf("程序与纪律是否正常", markers=["task-done"]),
|
||||
no("不是浮盈浮亏数字本身"),
|
||||
leaf("无信号空档:空跑三检也是训练", markers=["other-lightbulb"]),
|
||||
],
|
||||
markers=["symbol-info"],
|
||||
fill="#1A5276",
|
||||
color=C_TEXT,
|
||||
font_size="12pt",
|
||||
bold=True,
|
||||
),
|
||||
],
|
||||
markers=["symbol-minus", "flag-green"],
|
||||
),
|
||||
],
|
||||
markers=["priority-3", "arrow-right"],
|
||||
label="操盘",
|
||||
),
|
||||
t1(
|
||||
"④ 纪律执行",
|
||||
[
|
||||
t2(
|
||||
"Gate 日纪律",
|
||||
[
|
||||
leaf("只做很明确的位置", markers=["symbol-info"]),
|
||||
leaf("同一位置最多两次机会(突破/假突破)", markers=["priority-2"]),
|
||||
no("两次都错 → 当日不再做单"),
|
||||
ok("离场以结构止盈/止损为准"),
|
||||
],
|
||||
markers=["flag-dark-blue", "task-done"],
|
||||
),
|
||||
t2(
|
||||
"期权日纪律",
|
||||
[
|
||||
no("不手平;等规则止盈或到期"),
|
||||
leaf("一次一仓;约 10U 权利金", markers=["symbol-info"]),
|
||||
no("不对冲;不做每天默认开期权"),
|
||||
leaf("损位跟模型:外沿/针尖", markers=["symbol-info"]),
|
||||
],
|
||||
markers=["flag-blue", "task-done"],
|
||||
),
|
||||
t2(
|
||||
"开仓前自检清单",
|
||||
[
|
||||
leaf("今日只动期权/合约?未开对冲?", markers=["task-start"]),
|
||||
leaf("1H 方向清楚(含 N 字)?", markers=["task-start"]),
|
||||
leaf("空间足够?结构量级够?", markers=["task-start"]),
|
||||
leaf("止损/止盈与 RR 定好?", markers=["task-start"]),
|
||||
leaf("工具选期权还是合约?理由写清?", markers=["task-start"]),
|
||||
leaf("合约:本位置第几次?今日两次用完?", markers=["task-start"]),
|
||||
],
|
||||
markers=["other-yes", "task-start"],
|
||||
),
|
||||
t2(
|
||||
"一句话版本",
|
||||
[
|
||||
leaf("1H→空间→结构→定损盈→期权/合约", markers=["arrow-right"]),
|
||||
leaf("不对冲;期权不手平", markers=["flag-gray"]),
|
||||
leaf("一位置两次,错完收工", markers=["priority-2"]),
|
||||
leaf("珍惜机会,日更不是目标", markers=["star-dark-blue"]),
|
||||
],
|
||||
markers=["star-dark-blue", "symbol-right"],
|
||||
),
|
||||
],
|
||||
markers=["priority-4", "task-done"],
|
||||
label="纪律",
|
||||
),
|
||||
],
|
||||
# 中心主题保持干净:不加图标/标签/备注,避免绿人、黄便签等杂乱标识
|
||||
markers=None,
|
||||
labels=None,
|
||||
fill=C_ROOT,
|
||||
color=C_TEXT,
|
||||
font_size="20pt",
|
||||
bold=True,
|
||||
line=C_ROOT,
|
||||
)
|
||||
root["structureClass"] = "org.xmind.ui.logic.right"
|
||||
|
||||
# XMind Zen 内置主题名;客户端可识别 business
|
||||
sheet = {
|
||||
"id": tid(),
|
||||
"class": "sheet",
|
||||
"title": "执行手册与行为准则 · 商务版",
|
||||
"rootTopic": root,
|
||||
"theme": {
|
||||
"id": tid(),
|
||||
"title": "business",
|
||||
"centralTopic": {
|
||||
"id": "centralTopic",
|
||||
"properties": {
|
||||
"svg:fill": C_ROOT,
|
||||
"fo:color": C_TEXT,
|
||||
"fo:font-family": "Microsoft YaHei",
|
||||
"fo:font-size": "20pt",
|
||||
"fo:font-weight": "bold",
|
||||
"shape-class": "org.xmind.topicShape.roundedRect",
|
||||
"line-color": C_L1,
|
||||
"line-width": "2pt",
|
||||
"line-class": "org.xmind.branchConnection.roundedelbow",
|
||||
},
|
||||
},
|
||||
"mainTopic": {
|
||||
"id": "mainTopic",
|
||||
"properties": {
|
||||
"svg:fill": C_L1,
|
||||
"fo:color": C_TEXT,
|
||||
"fo:font-family": "Microsoft YaHei",
|
||||
"fo:font-size": "15pt",
|
||||
"fo:font-weight": "bold",
|
||||
"shape-class": "org.xmind.topicShape.roundedRect",
|
||||
"line-color": C_L2,
|
||||
"line-width": "1.5pt",
|
||||
},
|
||||
},
|
||||
"subTopic": {
|
||||
"id": "subTopic",
|
||||
"properties": {
|
||||
"svg:fill": C_L2,
|
||||
"fo:color": C_TEXT,
|
||||
"fo:font-family": "Microsoft YaHei",
|
||||
"fo:font-size": "12pt",
|
||||
"shape-class": "org.xmind.topicShape.roundedRect",
|
||||
"line-color": "#85C1E9",
|
||||
},
|
||||
},
|
||||
"floatingTopic": {
|
||||
"id": "floatingTopic",
|
||||
"properties": {
|
||||
"svg:fill": C_MUTED,
|
||||
"fo:color": C_TEXT,
|
||||
"fo:font-family": "Microsoft YaHei",
|
||||
},
|
||||
},
|
||||
"importantTopic": {
|
||||
"id": "importantTopic",
|
||||
"properties": {
|
||||
"svg:fill": C_WARN,
|
||||
"fo:color": C_TEXT,
|
||||
},
|
||||
},
|
||||
"minorTopic": {
|
||||
"id": "minorTopic",
|
||||
"properties": {
|
||||
"svg:fill": "#EBF5FB",
|
||||
"fo:color": C_TEXT_DARK,
|
||||
},
|
||||
},
|
||||
"expiredTopic": {
|
||||
"id": "expiredTopic",
|
||||
"properties": {
|
||||
"svg:fill": "#D5D8DC",
|
||||
"fo:color": C_MUTED,
|
||||
},
|
||||
},
|
||||
"calloutTopic": {
|
||||
"id": "calloutTopic",
|
||||
"properties": {
|
||||
"svg:fill": "#FEF9E7",
|
||||
"fo:color": C_WARN,
|
||||
},
|
||||
},
|
||||
"summaryTopic": {
|
||||
"id": "summaryTopic",
|
||||
"properties": {
|
||||
"svg:fill": "#145A32",
|
||||
"fo:color": C_TEXT,
|
||||
},
|
||||
},
|
||||
"boundary": {
|
||||
"id": "boundary",
|
||||
"properties": {
|
||||
"svg:fill": "#D6EAF8",
|
||||
"fo:color": C_L1,
|
||||
"line-color": C_L2,
|
||||
},
|
||||
},
|
||||
"summary": {
|
||||
"id": "summary",
|
||||
"properties": {
|
||||
"line-color": C_L1,
|
||||
"line-width": "2pt",
|
||||
},
|
||||
},
|
||||
"relationship": {
|
||||
"id": "relationship",
|
||||
"properties": {
|
||||
"line-color": C_MUTED,
|
||||
"line-pattern": "dash",
|
||||
},
|
||||
},
|
||||
"map": {
|
||||
"id": "map",
|
||||
"properties": {
|
||||
"svg:fill": "#F4F6F7",
|
||||
"color-list": f"{C_L1} {C_L2} #2874A6 #1ABC9C #B9770E",
|
||||
"line-tapered": "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
return [sheet]
|
||||
|
||||
|
||||
def main() -> None:
|
||||
content = build_content()
|
||||
metadata = {
|
||||
"creator": {"name": "crypto_monitor", "version": "1.1"},
|
||||
"activeSheetId": content[0]["id"],
|
||||
}
|
||||
manifest = {
|
||||
"file-entries": {
|
||||
"content.json": {},
|
||||
"metadata.json": {},
|
||||
"manifest.json": {},
|
||||
}
|
||||
}
|
||||
OUT.parent.mkdir(parents=True, exist_ok=True)
|
||||
if OUT.exists():
|
||||
OUT.unlink()
|
||||
with zipfile.ZipFile(OUT, "w", compression=zipfile.ZIP_DEFLATED) as zf:
|
||||
zf.writestr("content.json", json.dumps(content, ensure_ascii=False, indent=2))
|
||||
zf.writestr("metadata.json", json.dumps(metadata, ensure_ascii=False, indent=2))
|
||||
zf.writestr("manifest.json", json.dumps(manifest, ensure_ascii=False, indent=2))
|
||||
print(f"wrote {OUT}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,69 @@
|
||||
"""期权档案缓存 upsert / 列表 / 日历."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from lib.hub.hub_options_archive_lib import (
|
||||
init_options_archive_db,
|
||||
list_archive_options_calendar,
|
||||
list_daily_options_trades,
|
||||
upsert_options_trades_cache,
|
||||
)
|
||||
|
||||
|
||||
class TestHubOptionsArchive(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self._td = tempfile.TemporaryDirectory()
|
||||
self.db = Path(self._td.name) / "hub_symbol_archive.db"
|
||||
init_options_archive_db(self.db)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self._td.cleanup()
|
||||
|
||||
def test_upsert_and_list_daily(self) -> None:
|
||||
trades = [
|
||||
{
|
||||
"history_key": "local_opt:1",
|
||||
"source_type": "option_spot",
|
||||
"source_label": "纯期权",
|
||||
"underlying": "ETH",
|
||||
"inst_id": "ETH-USD-250725-3200-C",
|
||||
"opt_type": "C",
|
||||
"opened_at": "2026-07-20 10:00:00",
|
||||
"closed_at": "2026-07-20 16:00:00",
|
||||
"hold_seconds": 21600,
|
||||
"realized_pnl_total": 12.5,
|
||||
"premium_paid": 8.0,
|
||||
"reviewed": True,
|
||||
"strategy_tag": "假突破",
|
||||
},
|
||||
{
|
||||
"history_key": "local_opt:2",
|
||||
"source_type": "option_spot",
|
||||
"underlying": "ETH",
|
||||
"opened_at": "2026-07-19 10:00:00",
|
||||
"closed_at": "2026-07-19 12:00:00",
|
||||
"realized_pnl_total": -3.0,
|
||||
"excluded_as_hedge_leg": 1,
|
||||
},
|
||||
]
|
||||
r = upsert_options_trades_cache("okx", trades, db_path=self.db)
|
||||
self.assertEqual(r["upserted"], 1)
|
||||
payload = list_daily_options_trades(
|
||||
"2026-07-20",
|
||||
period="today",
|
||||
db_path=self.db,
|
||||
)
|
||||
self.assertEqual(len(payload["trades"]), 1)
|
||||
self.assertEqual(payload["trades"][0]["history_key"], "local_opt:1")
|
||||
self.assertAlmostEqual(payload["stats"]["pnl_total"], 12.5)
|
||||
cal = list_archive_options_calendar(2026, 7, db_path=self.db)
|
||||
self.assertIn("2026-07-20", cal["days"])
|
||||
self.assertEqual(cal["days"]["2026-07-20"]["open_count"], 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -21,6 +21,19 @@ class TestInstanceDisplayPrefs(unittest.TestCase):
|
||||
prefs = normalize_display_prefs({"show_nav_stats": False})
|
||||
self.assertFalse(tab_allowed("stats", prefs))
|
||||
self.assertTrue(tab_allowed("trade", prefs))
|
||||
self.assertTrue(tab_allowed("key_monitor", prefs))
|
||||
|
||||
def test_key_monitor_and_trade_nav_can_hide(self):
|
||||
prefs = normalize_display_prefs(
|
||||
{"show_nav_key_monitor": False, "show_nav_trade": False}
|
||||
)
|
||||
self.assertFalse(tab_allowed("key_monitor", prefs))
|
||||
self.assertFalse(tab_allowed("trade", prefs))
|
||||
on = normalize_display_prefs({})
|
||||
self.assertTrue(on["show_nav_key_monitor"])
|
||||
self.assertTrue(on["show_nav_trade"])
|
||||
self.assertTrue(tab_allowed("key_monitor", on))
|
||||
self.assertTrue(tab_allowed("trade", on))
|
||||
|
||||
def test_dashboard_nav_default_off(self):
|
||||
prefs = normalize_display_prefs({})
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
"""按可用余额打满:min(余额, 单笔预算)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from lib.options.options_pricing_lib import resolve_budget_full_usdc
|
||||
|
||||
|
||||
def test_balance_above_budget_uses_budget():
|
||||
assert resolve_budget_full_usdc(100.0, 10.0) == 10.0
|
||||
|
||||
|
||||
def test_balance_below_budget_uses_balance():
|
||||
assert resolve_budget_full_usdc(5.0, 10.0) == 5.0
|
||||
|
||||
|
||||
def test_balance_equals_budget():
|
||||
assert resolve_budget_full_usdc(10.0, 10.0) == 10.0
|
||||
@@ -0,0 +1,48 @@
|
||||
"""期权开平仓微信文案."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from lib.options.options_notify_lib import (
|
||||
build_options_close_message,
|
||||
build_options_open_message,
|
||||
)
|
||||
|
||||
|
||||
class TestOptionsNotify(unittest.TestCase):
|
||||
def test_open_close_messages(self) -> None:
|
||||
open_msg = build_options_open_message(
|
||||
account_label="OKX期权",
|
||||
inst_id="ETH-USD-250725-3200-C",
|
||||
underlying="ETH",
|
||||
opt_type="C",
|
||||
sheets=2,
|
||||
premium_paid=8.5,
|
||||
open_quote=0.01,
|
||||
target_index=3400,
|
||||
signal_note="假突破",
|
||||
trade_id=12,
|
||||
)
|
||||
self.assertIn("【OKX期权·开仓】", open_msg)
|
||||
self.assertIn("ETH-USD-250725-3200-C", open_msg)
|
||||
self.assertIn("目标指数:3400", open_msg)
|
||||
|
||||
close_msg = build_options_close_message(
|
||||
account_label="OKX期权",
|
||||
inst_id="ETH-USD-250725-3200-C",
|
||||
reason="手动平仓",
|
||||
underlying="ETH",
|
||||
opt_type="C",
|
||||
sheets=2,
|
||||
premium_paid=8.5,
|
||||
premium_received=12.0,
|
||||
realized_pnl=3.5,
|
||||
)
|
||||
self.assertIn("【OKX期权·平仓】", close_msg)
|
||||
self.assertIn("手动平仓", close_msg)
|
||||
self.assertIn("3.5000", close_msg)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user