Compare commits
165 Commits
4547ec11b5
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 05d0b7524f | |||
| 8ed87a23e9 | |||
| c9a7dee7ff | |||
| 794ca61358 | |||
| 11aff71415 | |||
| e1dc4595a2 | |||
| fa763bd968 | |||
| 9bce381497 | |||
| 227d8b515f | |||
| c40b4b919c | |||
| 434acb0470 | |||
| 1a70a60dba | |||
| 5cacb62d4c | |||
| d5648e0b2c | |||
| c291f331fc | |||
| 1faeff4ee9 | |||
| eb096d915d | |||
| abc436514a | |||
| b67dde0427 | |||
| 4abec850a3 | |||
| 6c54a8ae95 | |||
| b421945b16 | |||
| 6eb312451b | |||
| 9c00e167f6 | |||
| 5ca5df72b6 | |||
| 06f7fc930a | |||
| 1e6589a39f | |||
| 7bc85b2528 | |||
| 9bab06c1e7 | |||
| fb5cc801ee | |||
| d87bb00208 | |||
| 0684f15859 | |||
| c2a6b99fbd | |||
| c05a1b6c7f | |||
| 8b5fa347cb | |||
| b22123bdfc | |||
| 8076cbbd51 | |||
| c8ed098bfc | |||
| 71ef310648 | |||
| e3f288083c | |||
| d44e93dd4f | |||
| 4db07bea59 | |||
| befcfcd282 | |||
| 85733e870c | |||
| 7a5795e131 | |||
| 46654a05b1 | |||
| 085652c2dd | |||
| fb4710b011 | |||
| 5dd58b8297 | |||
| 4724741fdd | |||
| 9b08bd8bfd | |||
| adec664c52 | |||
| e222a93204 | |||
| 51be04e5d2 | |||
| 58d99259e8 | |||
| 2065122777 | |||
| 5670e5138c | |||
| 5e9d9f00e8 | |||
| e155682b5f | |||
| 91840ca2ca | |||
| d000fce423 | |||
| ac4236b03f | |||
| 223001d8a2 | |||
| 3a32429a5f | |||
| 18575d1c55 | |||
| d62304b2c4 | |||
| 9fd0116dec | |||
| ab9b132c69 | |||
| 1b0f906640 | |||
| 012b220c60 | |||
| 19203c050c | |||
| 58327a9a66 | |||
| ca98967af0 | |||
| b74dccd805 | |||
| cd7f6668af | |||
| 5453ce1dcf | |||
| c0e338eb23 | |||
| 492ca9d82e | |||
| de62f0779b | |||
| 57f792fbe5 | |||
| 2854eb97bb | |||
| 1488fc42e1 | |||
| 557c7dbda0 | |||
| 2bcae30378 | |||
| 970bea5222 | |||
| 6718e5e2cc | |||
| 30900dc3ad | |||
| 0947b73a1a | |||
| aaf7d0afd1 | |||
| 77ffb150c6 | |||
| 73ff3f7089 | |||
| 8fb3949fac | |||
| dce017fbf2 | |||
| 58e3e98031 | |||
| e65661ef94 | |||
| 5e88a1ce24 | |||
| 67ae4fe038 | |||
| 561f3332b4 | |||
| c628e208ca | |||
| 273924a36a | |||
| ff53290a57 | |||
| 7589e942eb | |||
| a53b0fcc4c | |||
| 1ff2aa17e8 | |||
| d06ed06f01 | |||
| 971252ee7c | |||
| 7cd020fbd9 | |||
| 730326fb5b | |||
| d95aa8d215 | |||
| 4c3dd86c37 | |||
| 106eb73b22 | |||
| 36eebbc03c | |||
| bc6390ac90 | |||
| aab2a51c1c | |||
| b5eae63f4a | |||
| 95eaeb5ddc | |||
| cfd820cc1a | |||
| 65ae34ceb6 | |||
| 5e05a37c5d | |||
| b876b6f91d | |||
| 0d40f336f0 | |||
| eb51e8f670 | |||
| ddd9e71820 | |||
| 55c7bdf18d | |||
| c7534bd3df | |||
| 111d70209a | |||
| c8faab4dda | |||
| dd1833c68e | |||
| 2883402b96 | |||
| 3460396ac2 | |||
| 8452912577 | |||
| cc2bd519e7 | |||
| f6c54971e4 | |||
| 4b4469528f | |||
| f53b07fbec | |||
| ff9e463e0a | |||
| 09579fbc13 | |||
| c67360c124 | |||
| 559d2fcd83 | |||
| 6260806c3b | |||
| e513d0ceee | |||
| 0860b26510 | |||
| ebeb659d38 | |||
| e833457117 | |||
| 2fc822eba9 | |||
| 5eee96c3a1 | |||
| 4dc6e258bd | |||
| 19534b6e80 | |||
| eabeebd3f5 | |||
| 51488a6df4 | |||
| 8badfe12a3 | |||
| b2c00b9fc5 | |||
| 2a1e0bd2f6 | |||
| 297ce190be | |||
| 17f71e70f2 | |||
| f42096fc75 | |||
| c52ca752da | |||
| 8ed73697b8 | |||
| f24dd3fe9b | |||
| f7f023d77f | |||
| 341f941e20 | |||
| 284c89df94 | |||
| ceb6828cb7 | |||
| cf6d79c8f6 | |||
| b8991cb74e |
@@ -0,0 +1,88 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
AI 协助分析交易数据和回测的仓库。主要用来做策略回测、行情分析、期权定价、资产配置等量化相关操作。
|
||||||
|
|
||||||
|
## Python 执行环境
|
||||||
|
|
||||||
|
**仅允许使用 Docker 运行 Python**,严禁使用设备本地 Python:
|
||||||
|
|
||||||
|
- **⚠️ 严禁直接 `docker run python:3.11-slim`**,必须先执行 `docker images` 确认本地已有镜像,优先复用
|
||||||
|
- 无可用镜像时才允许拉取,默认拉取 `python:3.11-slim`
|
||||||
|
- 示例: `docker images | grep python` 查看已有镜像,再用已有镜像执行
|
||||||
|
|
||||||
|
## 环境搭建(首次使用)
|
||||||
|
|
||||||
|
### 1. 创建容器
|
||||||
|
|
||||||
|
检查是否已有 `finance-talk` 容器(名称固定,在终端中运行):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker ps -a --filter "name=finance-talk" --format "{{.Names}}"
|
||||||
|
```
|
||||||
|
|
||||||
|
若无输出,则按以下步骤创建:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看已有 Python 镜像,优先复用
|
||||||
|
docker images | grep python
|
||||||
|
|
||||||
|
# 无可用镜像时才拉取(默认 python:3.11-slim)
|
||||||
|
docker pull python:3.11-slim
|
||||||
|
|
||||||
|
# 创建并启动容器(映射当前目录到 /workspace)
|
||||||
|
docker run -d \
|
||||||
|
--name finance-talk \
|
||||||
|
-v $(pwd):/workspace \
|
||||||
|
-w /workspace \
|
||||||
|
python:3.13.11-slim \
|
||||||
|
sleep infinity
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 安装依赖包
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec finance-talk pip install tushare akshare
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec finance-talk python -c "import tushare; import akshare; print('OK')"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 日常使用
|
||||||
|
|
||||||
|
Python 脚本通过 `docker exec` 在容器中执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec finance-talk python your_script.py
|
||||||
|
```
|
||||||
|
|
||||||
|
若容器已存在但未运行,先启动:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker start finance-talk
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Sources
|
||||||
|
|
||||||
|
按优先级排序:
|
||||||
|
|
||||||
|
1. **Tushare Pro** — 优先使用,数据最全(股票、期货、指数、期权等)
|
||||||
|
- Token: `76efd8465f9f2591aa42a385268e06acf6b80b7a15be2267ad2281b7`
|
||||||
|
- 用法: `import tushare as ts; ts.set_token('...'); pro = ts.pro_api()`
|
||||||
|
2. **akshare** — 备选,无需 token,覆盖面广
|
||||||
|
- A股指数: `ak.stock_zh_index_daily(symbol="sh000300")`
|
||||||
|
- 全球指数: `ak.index_global_hist_em(symbol="纳斯达克")`
|
||||||
|
- 个股: `ak.stock_zh_a_hist(symbol="600030", adjust="qfq")`
|
||||||
|
- 期货主连: `ak.futures_main_sina(symbol="FG0")`
|
||||||
|
3. **yfinance** — 海外市场(注意频次限制,需加 sleep)
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- 期货主连代码格式: `FG0` (品种代码 + "0")
|
||||||
|
- A股个股复权方式: 前复权 (`adjust="qfq"`)
|
||||||
@@ -6,12 +6,67 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
AI 协助分析交易数据和回测的仓库。主要用来做策略回测、行情分析、期权定价、资产配置等量化相关操作。
|
AI 协助分析交易数据和回测的仓库。主要用来做策略回测、行情分析、期权定价、资产配置等量化相关操作。
|
||||||
|
|
||||||
## Python Environment
|
## Python 执行环境
|
||||||
|
|
||||||
- Python 3.13 (Homebrew at `/opt/homebrew/bin/python3`)
|
**仅允许使用 Docker 运行 Python**,严禁使用设备本地 Python:
|
||||||
- `pip` 与 `python3` 版本可能不对应(pip 可能指向 3.9),安装包时用 `/opt/homebrew/bin/python3 -m pip install <pkg>` 或创建 venv
|
|
||||||
- `.venv/` 已 gitignore
|
- **⚠️ 严禁直接 `docker run python:3.11-slim`**,必须先执行 `docker images` 确认本地已有镜像,优先复用
|
||||||
- 常用库: `pandas`, `numpy`, `akshare`, `yfinance`, `tushare`
|
- 无可用镜像时才允许拉取,默认拉取 `python:3.11-slim`
|
||||||
|
- 示例: `docker images | grep python` 查看已有镜像,再用已有镜像执行
|
||||||
|
|
||||||
|
## 环境搭建(首次使用)
|
||||||
|
|
||||||
|
### 1. 创建容器
|
||||||
|
|
||||||
|
检查是否已有 `finance-talk` 容器(名称固定,在终端中运行):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker ps -a --filter "name=finance-talk" --format "{{.Names}}"
|
||||||
|
```
|
||||||
|
|
||||||
|
若无输出,则按以下步骤创建:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看已有 Python 镜像,优先复用
|
||||||
|
docker images | grep python
|
||||||
|
|
||||||
|
# 无可用镜像时才拉取(默认 python:3.11-slim)
|
||||||
|
docker pull python:3.11-slim
|
||||||
|
|
||||||
|
# 创建并启动容器(映射当前目录到 /workspace)
|
||||||
|
docker run -d \
|
||||||
|
--name finance-talk \
|
||||||
|
-v $(pwd):/workspace \
|
||||||
|
-w /workspace \
|
||||||
|
python:3.13.11-slim \
|
||||||
|
sleep infinity
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 安装依赖包
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec finance-talk pip install tushare akshare
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec finance-talk python -c "import tushare; import akshare; print('OK')"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 日常使用
|
||||||
|
|
||||||
|
Python 脚本通过 `docker exec` 在容器中执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec finance-talk python your_script.py
|
||||||
|
```
|
||||||
|
|
||||||
|
若容器已存在但未运行,先启动:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker start finance-talk
|
||||||
|
```
|
||||||
|
|
||||||
## Data Sources
|
## Data Sources
|
||||||
|
|
||||||
@@ -31,4 +86,3 @@ AI 协助分析交易数据和回测的仓库。主要用来做策略回测、
|
|||||||
|
|
||||||
- 期货主连代码格式: `FG0` (品种代码 + "0")
|
- 期货主连代码格式: `FG0` (品种代码 + "0")
|
||||||
- A股个股复权方式: 前复权 (`adjust="qfq"`)
|
- A股个股复权方式: 前复权 (`adjust="qfq"`)
|
||||||
- 图表输出到 `/tmp/` 临时目录,不回写仓库
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
data/*
|
||||||
|
!data/ft.db
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
FROM python:3.13.11-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY app/ ./app/
|
||||||
|
|
||||||
|
RUN mkdir -p /app/data
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
# FG2609 席位资金分析报告(2026-07-20 → 2026-07-24)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 席位分类
|
||||||
|
|
||||||
|
| 分类 | 席位 |
|
||||||
|
|------|------|
|
||||||
|
| 家人(散户) | 东方财富、徽商期货、方正中期、中信建投、平安期货 |
|
||||||
|
| 大户(产业/套保) | 永安期货、中信期货、国泰君安、海通期货、华泰期货、高盛期货、乾坤期货、摩根大通、中粮期货 |
|
||||||
|
| 噪音(忽略) | 东证期货 |
|
||||||
|
| 其他 | 未归入以上三类的席位 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、家人席位动向(散户情绪)
|
||||||
|
|
||||||
|
| 日期 | 多单增减 | 空单增减 | 当日净多 |
|
||||||
|
|------|---------|---------|----------|
|
||||||
|
| 07-20 | -0.66万 | +0.75万 | -1.41万 |
|
||||||
|
| 07-21 | -3.38万 | -0.04万 | -3.34万 |
|
||||||
|
| 07-22 | -1.60万 | +0.50万 | -2.10万 |
|
||||||
|
| 07-23 | +0.83万 | -0.44万 | +1.27万 |
|
||||||
|
| 07-24 | -1.89万 | -0.30万 | -1.59万 |
|
||||||
|
|
||||||
|
**核心结论:散户持续看空,减多为主。**
|
||||||
|
|
||||||
|
- 5 日累计减多 **-6.69 万**,累计加空 **+0.46 万**,净多变化 **-7.15 万**。
|
||||||
|
- 唯一一天净多(07-23)量级很小(+1.27 万),第二天立刻转回净空,说明散户整体偏空且缺乏持续性信心。
|
||||||
|
- 07-21 单日减多 **-3.38 万**,是当周家人席位减多最大的一天。这一天跳空低开、放量下杀,散户在恐慌中止损多单——典型的追跌杀多行为。
|
||||||
|
- 家人席位里,方正中期、中信建投、徽商期货、东方财富、平安期货五个席位多单在 5 日内全部下降,空单方向仅中信建投在 07-20 和 07-22 有加空动作。
|
||||||
|
|
||||||
|
**定性:散户情绪整体偏空,在下跌中减多出逃,属于典型的情绪化交易。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、大户席位动向(产业/套保资金)
|
||||||
|
|
||||||
|
| 日期 | 多单增减 | 空单增减 | 当日净多 |
|
||||||
|
|------|---------|---------|----------|
|
||||||
|
| 07-20 | +0.13万 | -1.26万 | +1.39万 |
|
||||||
|
| 07-21 | +2.14万 | -1.93万 | +4.07万 |
|
||||||
|
| 07-22 | -2.10万 | -6.67万 | +4.57万 |
|
||||||
|
| 07-23 | -0.52万 | -1.56万 | +1.04万 |
|
||||||
|
| 07-24 | -0.29万 | -1.50万 | +1.21万 |
|
||||||
|
|
||||||
|
**核心结论:大户持续大规模减空,是本周最重要的资金信号。**
|
||||||
|
|
||||||
|
- 5 日累计减空 **-12.93 万**,减多仅 **-0.65 万**,净多变化 **+12.28 万**。
|
||||||
|
- 每天都是净多方向(大户每一天的空单减持量都大于多单减持量),全周无一天例外。
|
||||||
|
- **07-22(周三)是标志性节点**:大户单日减空 **-6.67 万**,是当周最大单日空头撤离。当天高盛减空 -1.49 万、中信减空 -2.20 万、国泰君安减空 -0.93 万,三家头部大户同步砍空单。
|
||||||
|
|
||||||
|
### 关键大户席位拆解
|
||||||
|
|
||||||
|
**空头三巨头(中信、高盛、国泰君安):**
|
||||||
|
|
||||||
|
| 席位 | 07-20 空单 | 07-24 空单 | 5日减空 |
|
||||||
|
|------|-----------|-----------|---------|
|
||||||
|
| 中信期货 | 21.61万 | 18.07万 | **-3.54万** |
|
||||||
|
| 高盛期货 | 21.93万 | 17.81万 | **-4.12万** |
|
||||||
|
| 国泰君安 | 18.81万 | 17.47万 | **-1.34万** |
|
||||||
|
|
||||||
|
三家合计减空 **9.00 万**,占了全部大户减空量的 70%。这不是散户式的小打小闹,是主力资金在系统性地撤退空头头寸。
|
||||||
|
|
||||||
|
**定性:大户资金明确偏多。产业/套保盘的空头大幅回补,说明产业端认为当前价位已不具备继续做空的安全边际,或是交割月临近主动平空离场。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、多空力量对比
|
||||||
|
|
||||||
|
| 分类 | 5日多单累计 | 5日空单累计 | 净多(多-空)| 方向 |
|
||||||
|
|------|------------|------------|-------------|------|
|
||||||
|
| 家人 | -6.69万 | +0.46万 | **-7.15万** | 偏空 |
|
||||||
|
| 大户 | -0.65万 | -12.93万 | **+12.28万** | 强烈偏多 |
|
||||||
|
| 其他 | -4.24万 | -3.63万 | **-0.62万** | 略偏空 |
|
||||||
|
| **合计** | -11.58万 | -16.10万 | **+4.52万** | 偏多 |
|
||||||
|
|
||||||
|
**核心结论:大户的净多力量(+12.28 万)远超家人(-7.15 万)和其他席位(-0.62 万)的净空力量之和。**
|
||||||
|
|
||||||
|
- 全市场合计净多 **+4.52 万**,由大户独力支撑。
|
||||||
|
- 家人与其他席位合计净空 **-7.77 万**,与大户形成明确对手盘——大户在接散户抛出的多单、同时平掉自己的空单,典型的"聪明钱吃傻钱"格局。
|
||||||
|
- 东证期货被定义为噪音席位已排除,不影响结论。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、异常信号
|
||||||
|
|
||||||
|
### 1. 07-22(周三)空头集体溃退
|
||||||
|
|
||||||
|
大户单日减空 **-6.67 万**,是 5 日均值(-2.59 万)的 2.6 倍。中信 -2.20 万、高盛 -1.49 万、国泰 -0.93 万同时大幅减空。当天成交量 Top 1 的国泰君安成交达 38.13 万手(本周最高),放量明显。这一天出现了明确的空头踩踏信号。
|
||||||
|
|
||||||
|
### 2. 07-21(周二)散户与大户严重分歧
|
||||||
|
|
||||||
|
家人减多 **-3.38 万**(全周最大单日减多),大户加多 **+2.14 万**(全周最大单日加多)。一方恐慌出逃,一方逆势加仓。这是本周最典型的"对手盘"交易日。如果当天价格下跌,则大户在低位吸筹;如果价格上涨,则散户在反弹中解套离场——无论哪种,都是大户占优。
|
||||||
|
|
||||||
|
### 3. 冠通期货(其他席位)异动
|
||||||
|
|
||||||
|
07-22 成交 +4.23 万(从名不见经传到跻身 Top 13),07-23 再度 +4.78 万成交并进入 Top 6。这个席位两天成交暴增近 10 万手,背后可能有大资金借道入场,值得后续关注。
|
||||||
|
|
||||||
|
### 4. 永安期货多单逆势增加
|
||||||
|
|
||||||
|
永安 5 日多单从 4.38 万 → 4.53 万 → 4.56 万 → 4.99 万 → 5.19 万,累计加多 **+0.81 万**。同时空单从 6.41 万减至 4.62 万(-1.79 万)。永安的"多增空减"在大户中最为突出,是本周方向最明确的偏多席位。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、行情预判
|
||||||
|
|
||||||
|
### 判断:偏多,短期看反弹
|
||||||
|
|
||||||
|
**理由:**
|
||||||
|
|
||||||
|
1. **大户资金面强烈偏多**:5 日净多 +12.28 万,头部空头(中信、高盛、国泰)合计减空 9 万手,空头回补动能远未耗尽。如果这些席位还有空头敞口,下周可能继续平空,形成持续买盘。
|
||||||
|
|
||||||
|
2. **散户是反向指标**:家人席位 5 日净空 -7.15 万,在下跌中恐慌减多,符合散户在底部区域的行为特征。散户出清往往是阶段性底部的必要条件。
|
||||||
|
|
||||||
|
3. **对手盘格局明显**:大户接散户的盘,且大户是主动方(减空 + 稳多),散户是被动方(减多),这是典型的筑底资金结构。
|
||||||
|
|
||||||
|
4. **永安、国泰君安等头部席位方向一致**:多家大户多单稳定、空单持续下降,资金面没有分歧。
|
||||||
|
|
||||||
|
### 风险提示
|
||||||
|
|
||||||
|
- 全周减多总量(-11.58 万)也很大,说明多头信心整体不足。大户更多是"不空了"而非"大力做多",反弹力度取决于后续是否有新增多单入场。
|
||||||
|
- 冠通期货的异常放量可能代表新的空头力量进场,需要观察其后续持仓方向。
|
||||||
|
- 如果下一交易日价格反弹后大户开始减多,则说明这只是一轮空头回补行情,而非趋势反转。
|
||||||
|
|
||||||
|
**综合判断:偏多(概率 65%),预计 FG2609 下一交易日震荡偏强,关注空头回补持续性和是否有增量多头入场。**
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
# FG2611 席位资金分析报告(2026-07-20 → 2026-07-24)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 席位分类
|
||||||
|
|
||||||
|
| 分类 | 席位 |
|
||||||
|
|------|------|
|
||||||
|
| 家人(散户) | 东方财富、徽商期货、方正中期、中信建投、平安期货 |
|
||||||
|
| 大户(产业/套保) | 永安期货、中信期货、国泰君安、海通期货、华泰期货、高盛期货、乾坤期货、摩根大通、中粮期货 |
|
||||||
|
| 噪音(忽略) | 东证期货 |
|
||||||
|
| 其他 | 未归入以上三类的席位 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、家人席位动向(散户情绪)
|
||||||
|
|
||||||
|
| 日期 | 多单增减 | 空单增减 | 当日净多 |
|
||||||
|
|------|---------|---------|----------|
|
||||||
|
| 07-20 | -0.06万 | -0.15万 | +0.09万 |
|
||||||
|
| 07-21 | -0.00万 | +0.01万 | -0.01万 |
|
||||||
|
| 07-22 | +0.07万 | -0.04万 | +0.11万 |
|
||||||
|
| 07-23 | +0.02万 | -0.06万 | +0.08万 |
|
||||||
|
| 07-24 | -0.13万 | -0.06万 | -0.07万 |
|
||||||
|
|
||||||
|
**核心结论:家人席位参与度极低,无明确方向性。**
|
||||||
|
|
||||||
|
- 5 日累计减多 **-0.10 万**,累计减空 **-0.30 万**,净多变化 **+0.20 万**。
|
||||||
|
- 每日变动量级在数百手级别,与 FG2609 主连合约上万手的波动完全不同。FG2611 是远月合约,散户参与度天然较低。
|
||||||
|
- 无追涨杀跌迹象——单日最大变动仅 -0.13 万(07-24 减多),远不足以构成情绪判断。
|
||||||
|
- 家人持仓绝对量也极小:中信建投是唯一多空持仓均超 2 万的家人席位,东方财富多单仅 0.32 万、空单未进 Top 20。
|
||||||
|
|
||||||
|
**定性:远月合约散户参与度极低,家人席位资金信号不具备参考价值。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、大户席位动向(产业/套保资金)
|
||||||
|
|
||||||
|
| 日期 | 多单增减 | 空单增减 | 当日净多 |
|
||||||
|
|------|---------|---------|----------|
|
||||||
|
| 07-20 | -0.68万 | -0.31万 | -0.37万 |
|
||||||
|
| 07-21 | -0.10万 | +0.14万 | -0.24万 |
|
||||||
|
| 07-22 | -0.28万 | +0.20万 | -0.48万 |
|
||||||
|
| 07-23 | +0.98万 | +0.42万 | +0.56万 |
|
||||||
|
| 07-24 | +0.41万 | +0.30万 | +0.11万 |
|
||||||
|
|
||||||
|
**核心结论:前三天减多偏空,后两天显著反转加多,方向切换明显。**
|
||||||
|
|
||||||
|
- 5 日累计加多 **+0.33 万**,加空 **+0.76 万**,净多变化 **-0.43 万**。总体略偏空,但主要是前三天贡献的。
|
||||||
|
- **07-23(周四)是转折点**:大户加多 +0.98 万,是 5 日最大单日加多。华泰期货加多 +0.74 万(从 0.18 万跳升至 0.92 万,增量惊人)、中粮期货加多 +0.32 万。
|
||||||
|
- 空头方面,高盛期货持续加空:从 07-20 的 1.05 万 → 07-21 的 1.20 万 → 07-22 的 1.76 万 → 07-23 的 1.91 万 → 07-24 的 2.01 万,5 日累计加空 **+0.96 万**,是大户中唯一持续单向加空的席位。
|
||||||
|
- 国泰君安 07-24 多单 +0.26 万、空单 +0.32 万,双边大幅加仓,方向不明确但参与度在提升。
|
||||||
|
|
||||||
|
### 关键大户席位拆解
|
||||||
|
|
||||||
|
**中粮期货——本周最活跃的大户席位:**
|
||||||
|
|
||||||
|
| 指标 | 07-20 | 07-21 | 07-22 | 07-23 | 07-24 |
|
||||||
|
|------|-------|-------|-------|-------|-------|
|
||||||
|
| 成交量 | 5.05万 | 2.81万 | 5.62万 | 7.48万 | 1.65万 |
|
||||||
|
| 多单 | 1.48万 | 1.16万 | 1.43万 | 1.75万 | 1.79万 |
|
||||||
|
| 空单 | 1.46万 | 1.14万 | 1.39万 | 1.73万 | 1.73万 |
|
||||||
|
|
||||||
|
中粮在 FG2611 上多空双向加仓,多单从 1.48 万增至 1.79 万(+0.31 万),空单从 1.46 万增至 1.73 万(+0.27 万)。**这是典型的套利/套保盘口——多空同步增加,而非单纯做方向。**
|
||||||
|
|
||||||
|
**华泰期货 07-23 异动:**
|
||||||
|
|
||||||
|
华泰多单从 0.18 万(07-22)暴增至 0.92 万(07-23),单日加多 **+0.74 万**,增幅超过 400%。同时成交量从 0.42 万放大至 1.22 万。这是一个非常突兀的增仓,值得重点关注后续是否持续。
|
||||||
|
|
||||||
|
**定性:大户整体略偏空(净多 -0.43 万),但后半周出现方向切换迹象。高盛是唯一坚定的空头加仓者,中粮以套利盘为主,华泰异动值得追踪。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、多空力量对比
|
||||||
|
|
||||||
|
| 分类 | 5日多单累计 | 5日空单累计 | 净多(多-空)| 方向 |
|
||||||
|
|------|------------|------------|-------------|------|
|
||||||
|
| 家人 | -0.10万 | -0.30万 | **+0.20万** | 微偏多 |
|
||||||
|
| 大户 | +0.33万 | +0.76万 | **-0.43万** | 略偏空 |
|
||||||
|
| 其他 | -0.72万 | -1.20万 | **+0.48万** | 偏多 |
|
||||||
|
| **合计** | -0.49万 | -0.74万 | **+0.25万** | 微偏多 |
|
||||||
|
|
||||||
|
**核心结论:FG2611 整体资金量级较小,市场方向不明确。**
|
||||||
|
|
||||||
|
- 全市场净多仅 **+0.25 万**,与 FG2609 的 +4.52 万形成鲜明对比。远月合约的资金博弈强度远低于主连。
|
||||||
|
- 三大分类之间无明显对手盘关系——家人偏多(+0.20 万)、大户偏空(-0.43 万)、其他偏多(+0.48 万),但量级都太小,无法形成有效信号。
|
||||||
|
- 其他席位(+0.48 万)贡献了最大的净多力量,但这些席位分散、方向一致性弱。
|
||||||
|
|
||||||
|
**定性:远月合约流动性差,各类资金均未展现出明确的战略方向,不宜据此做出方向判断。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、异常信号
|
||||||
|
|
||||||
|
### 1. 07-23(周四)大户方向逆转
|
||||||
|
|
||||||
|
前半周大户减多为主(三日累计 -1.06 万),07-23 突然加多 +0.98 万。华泰期货一天加多 +0.74 万是主要推手。这种突然的方向切换值得警惕——可能是某个产业户判断价格到位后集中建仓多单。
|
||||||
|
|
||||||
|
### 2. 高盛期货持续单向加空
|
||||||
|
|
||||||
|
高盛空单每日递增:1.05 → 1.20 → 1.76 → 1.91 → 2.01 万,5 日累计 +0.96 万,且多单从不在 Top 20。在所有大户席位中,高盛是唯一一个方向完全一致的席位——**只做空、不加多**。这在 FG2609 高盛大规模减空的背景下显得尤为突兀:主连上高盛在撤退,远月上高盛在加码。需要关注这是跨期套利(空 11 多 09)还是纯粹的远月看空。
|
||||||
|
|
||||||
|
### 3. 07-24(周五)中粮成交量骤降
|
||||||
|
|
||||||
|
中粮成交量从 07-23 的 7.48 万骤降至 07-24 的 1.65 万(-5.82 万),降幅居全席位之首。而当天中粮多空持仓几乎未变(多 +0.04 万、空 +0.00 万)。这说明中粮在周四大幅建仓后,周五选择锁仓不动,符合套保盘的交易特征。
|
||||||
|
|
||||||
|
### 4. 华泰期货多单暴增
|
||||||
|
|
||||||
|
07-23 华泰多单从 0.18 万跳升至 0.92 万(+0.74 万),而 07-22 华泰多单甚至不在 Top 20(意味持仓低于 0.27 万)。也就是说在两天内华泰从几乎零多单变成了近万手多单。这是一个需要持续追踪的信号——如果后续继续加仓,可能预示产业资金在远月布局。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、行情预判
|
||||||
|
|
||||||
|
### 判断:震荡,方向不明
|
||||||
|
|
||||||
|
**理由:**
|
||||||
|
|
||||||
|
1. **资金量级太小**:全周净多仅 +0.25 万,各项资金变动均不足以支撑方向判断。远月合约天然流动性差,席位分析的有效性大幅下降。
|
||||||
|
|
||||||
|
2. **大户内部存在分歧**:高盛持续单向加空(+0.96 万),但华泰(+0.74 万)和中粮(+0.31 万)在加多。同为产业/套保大户,方向截然相反,说明产业端对远月定价存在明显分歧。
|
||||||
|
|
||||||
|
3. **与 FG2609 的方向背离**:FG2609 大户强烈偏多(净多 +12.28 万),而 FG2611 大户略偏空(净多 -0.43 万)。高盛在 2609 上减空 -4.12 万,在 2611 上加空 +0.96 万——这可能是跨期套利,而非单纯看多或看空。
|
||||||
|
|
||||||
|
4. **缺乏家人席位验证**:远月合约散户参与度极低,无法通过"家人—大户对手盘"框架来验证信号。缺少了一个重要的判断维度。
|
||||||
|
|
||||||
|
### 参考建议
|
||||||
|
|
||||||
|
- **以 FG2609 为主、FG2611 为辅**:主连合约资金博弈充分,信号更可靠。FG2611 当前阶段更适合作为跨期价差分析的参考,而非独立的方向判断依据。
|
||||||
|
- **重点关注华泰期货后续动向**:如果华泰在 FG2611 上继续加多,同时 FG2609 方向一致,则强化偏多判断。
|
||||||
|
- **关注高盛的跨期行为**:高盛在 2609 减空、2611 加空,如果这一模式持续,可能是"近强远弱"的跨期结构信号。
|
||||||
|
|
||||||
|
**综合判断:FG2611 自身资金面无法给出有效方向,建议以 FG2609 的分析结论为主(偏多),FG2611 跟随主连波动。远月合约暂以震荡看待。**
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
# FG2701 席位资金分析报告(2026-07-20 → 2026-07-24)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 席位分类
|
||||||
|
|
||||||
|
| 分类 | 席位 |
|
||||||
|
|------|------|
|
||||||
|
| 家人(散户) | 东方财富、徽商期货、方正中期、中信建投、平安期货 |
|
||||||
|
| 大户(产业/套保) | 永安期货、中信期货、国泰君安、海通期货、华泰期货、高盛期货、乾坤期货、摩根大通、中粮期货 |
|
||||||
|
| 噪音(忽略) | 东证期货 |
|
||||||
|
| 其他 | 未归入以上三类的席位 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、家人席位动向(散户情绪)
|
||||||
|
|
||||||
|
| 日期 | 多单增减 | 空单增减 | 当日净多 |
|
||||||
|
|------|---------|---------|----------|
|
||||||
|
| 07-20 | +0.08万 | +0.61万 | -0.53万 |
|
||||||
|
| 07-21 | +0.06万 | 0.00万 | +0.06万 |
|
||||||
|
| 07-22 | +0.20万 | +0.45万 | -0.25万 |
|
||||||
|
| 07-23 | +0.10万 | -0.22万 | +0.32万 |
|
||||||
|
| 07-24 | -0.02万 | -0.18万 | +0.16万 |
|
||||||
|
|
||||||
|
**核心结论:家人席位多空双增,但空单增幅更大,整体略偏空。**
|
||||||
|
|
||||||
|
- 5 日累计加多 **+0.43 万**,加空 **+0.65 万**,净多变化 **-0.23 万**。
|
||||||
|
- 前半周(07-20、07-22)偏空,后半周(07-23、07-24)偏多,方向摇摆不定。
|
||||||
|
- **07-20(周一)是家人加空最多的一天**:+0.61 万,全部来自中信建投单日加空 **+0.56 万**。中信建投空单从 0.80 万跳升至 1.50 万(07-22 最高点),后半周又回落到 1.07 万。这不是散户行为,更像是中信建投自营或大户借道这个席位做空了远月,因为散户不太可能一天加 5600 手空单。
|
||||||
|
- 方正中期是家人中多单最大的席位(1.03→1.03→1.08→1.03→1.06 万),5 日几乎未变,方向中性。
|
||||||
|
- 东方财富和平安期货持仓量极小(0.3 万级别),纯散户参与度极低。
|
||||||
|
|
||||||
|
**定性:FG2701 远月合约家人席位参与度低,且中信建投的"家人"分类在此合约上可能失真——其 07-20 单日加空 0.56 万的行为更像是大户级的操作。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、大户席位动向(产业/套保资金)
|
||||||
|
|
||||||
|
| 日期 | 多单增减 | 空单增减 | 当日净多 |
|
||||||
|
|------|---------|---------|----------|
|
||||||
|
| 07-20 | +0.15万 | +0.27万 | -0.12万 |
|
||||||
|
| 07-21 | -0.76万 | 0.00万 | -0.76万 |
|
||||||
|
| 07-22 | -0.25万 | -0.28万 | +0.03万 |
|
||||||
|
| 07-23 | -0.24万 | +0.68万 | -0.92万 |
|
||||||
|
| 07-24 | +0.12万 | -0.16万 | +0.28万 |
|
||||||
|
|
||||||
|
**核心结论:大户显著偏空,是三个合约中大户方向最明确的空头。**
|
||||||
|
|
||||||
|
- 5 日累计减多 **-0.97 万**,加空 **+0.51 万**,净多变化 **-1.48 万**。
|
||||||
|
- 5 天中有 3 天净空,且 07-21(-0.76 万)和 07-23(-0.92 万)两天的净空量级明显大于另外三天的波动幅度。
|
||||||
|
- 与 FG2609(大户净多 +12.28 万)和 FG2611(大户净多 -0.43 万)形成渐进式对比:**越远的合约,大户越偏空**。
|
||||||
|
|
||||||
|
### 关键大户席位拆解
|
||||||
|
|
||||||
|
**国泰君安——FG2701 的"超级空头":**
|
||||||
|
|
||||||
|
| 指标 | 07-20 | 07-21 | 07-22 | 07-23 | 07-24 |
|
||||||
|
|------|-------|-------|-------|-------|-------|
|
||||||
|
| 多单 | 1.40万 | 1.43万 | 1.50万 | 1.32万 | 1.39万 |
|
||||||
|
| 空单 | **4.42万** | — | 4.15万 | **4.46万** | **4.54万** |
|
||||||
|
|
||||||
|
国泰君安的空单(4.54 万)是它多单(1.39 万)的 **3.3 倍**,占大户空单总量的近 50%,占全市场空单的约 30%。在 FG2701 这个流动性远不如主连的远月合约上,单一席位持有如此庞大的空头敞口,非常不寻常。
|
||||||
|
|
||||||
|
这个席位在三个合约上的行为完全不同:
|
||||||
|
- FG2609:空单 18.81 → 17.47 万(减空 -1.34 万),**偏多**
|
||||||
|
- FG2611:多空双向,方向模糊
|
||||||
|
- FG2701:空单 4.42 → 4.54 万(加空 +0.12 万),**坚定偏空**
|
||||||
|
|
||||||
|
**这很可能不是同一类资金在操作三个合约**。FG2701 的国泰君安空头大概率是产业套保盘——玻璃企业锁定明年 1 月的卖出价格。
|
||||||
|
|
||||||
|
**高盛期货——锁仓不动:**
|
||||||
|
|
||||||
|
高盛空单全周维持在 1.70-1.71 万,几乎零变动。对比 FG2609 上高盛大幅减空 4.12 万、FG2611 上加空 0.96 万,FG2701 上的高盛选择了按兵不动。
|
||||||
|
|
||||||
|
**华泰期货多单持续缩水:**
|
||||||
|
|
||||||
|
华泰多单从 07-20 的 1.63 万 → 07-21 的 0.92 万(-0.71 万)→ 07-22/23/24 维持 0.87-0.91 万。华泰曾是 FG2701 上最大的多头,但 07-21 一天就砍掉了近一半的多单。这是一个明确的减多信号。
|
||||||
|
|
||||||
|
**永安期货多减空增:**
|
||||||
|
|
||||||
|
永安多单从 0.89 万 → 0.85 → 0.41 → 0.42 → 0.46 万(减多 -0.43 万),空单从 0.88 → 无 → 1.02 → 1.32 → 1.06 万(加空 +0.18 万)。方向明确偏空,与国泰君安形成呼应。
|
||||||
|
|
||||||
|
**定性:大户在 FG2701 上呈现一致的偏空布局。国泰君安的 4.54 万天量空单是核心信号,永安、华泰(减多方面)也指向同一方向。高盛虽然锁仓不动,但其在 FG2611 上的加空行为暗示整体偏空倾向。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、多空力量对比
|
||||||
|
|
||||||
|
| 分类 | 5日多单累计 | 5日空单累计 | 净多(多-空)| 方向 |
|
||||||
|
|------|------------|------------|-------------|------|
|
||||||
|
| 家人 | +0.43万 | +0.65万 | **-0.23万** | 略偏空 |
|
||||||
|
| 大户 | -0.97万 | +0.51万 | **-1.48万** | 明确偏空 |
|
||||||
|
| 其他 | +1.01万 | +1.77万 | **-0.75万** | 偏空 |
|
||||||
|
| **合计** | +0.47万 | +2.93万 | **-2.46万** | 明确偏空 |
|
||||||
|
|
||||||
|
**核心结论:FG2701 全市场三大分类一致偏空,是本周三个合约中方向最统一的空头合约。**
|
||||||
|
|
||||||
|
- 全市场净空 **-2.46 万**,与 FG2609 的 +4.52 万净多形成 180 度背离。
|
||||||
|
- 其他席位是最大的空头增量来源(+1.77 万空单),但分散在多个席位上,方向一致性不如大户。
|
||||||
|
- 没有人做多:大户减多(-0.97 万)而非加多,其他席位加多(+1.01 万)但加空更多(+1.77 万),家人加多(+0.43 万)但加空同样更多(+0.65 万)。
|
||||||
|
|
||||||
|
### 三大合约方向对比
|
||||||
|
|
||||||
|
| 合约 | 大户净多 | 总净多 | 方向 |
|
||||||
|
|------|---------|--------|------|
|
||||||
|
| FG2609(近月) | +12.28万 | +4.52万 | **强烈偏多** |
|
||||||
|
| FG2611(次月) | -0.43万 | +0.25万 | 震荡 |
|
||||||
|
| FG2701(远月) | -1.48万 | -2.46万 | **明确偏空** |
|
||||||
|
|
||||||
|
**这构成了一条清晰的期限结构:近强远弱,越远期越偏空。** 这符合商品期货在 contango/backwardation 结构下的常见特征——近月受现货支撑偏强,远月因产业套保盘(玻璃厂卖出远期产量)而承压。
|
||||||
|
|
||||||
|
**定性:FG2701 全市场一致偏空,核心驱动力是大户套保空头(国泰君安 4.54 万)和其他席位的分散加空。这并非市场情绪的"看空",更像是产业资金利用远月合约进行卖出套保的正常操作。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、异常信号
|
||||||
|
|
||||||
|
### 1. 国泰君安的"超级空头"是 FG2701 的定价锚
|
||||||
|
|
||||||
|
一个席位在远月合约上持有 4.54 万空单、多单仅 1.39 万,净空 3.15 万。这个量级在整个 FG2701 市场中占比极大。**如果国泰君安这个空头开始平仓(买回),会对价格形成巨大上推力。** 这是 FG2701 最大的潜在多空催化剂。
|
||||||
|
|
||||||
|
### 2. 07-24(周五)其他席位空单集中暴增
|
||||||
|
|
||||||
|
其他席位空单 07-24 单日 +1.33 万,是 5 日最大单日增幅。主要贡献者:
|
||||||
|
- 国投期货空单 +0.54 万(从 0.39 万跳至 0.93 万)
|
||||||
|
- 广发期货空单 +0.46 万(从 0.38 万跳至 0.84 万)
|
||||||
|
- 瑞达期货空单 +0.23 万(从 0.68 万增至 0.91 万)
|
||||||
|
|
||||||
|
三家合计加空 1.23 万。周五通常是减仓日(规避周末风险),但这三家反而在周五尾盘加空,值得关注。
|
||||||
|
|
||||||
|
### 3. 07-21(周二)华泰多单崩盘式减仓
|
||||||
|
|
||||||
|
华泰期货多单从 1.63 万暴跌至 0.92 万(-0.71 万),单日减幅 44%。当天华泰成交量 1.62 万(较前一日的 0.58 万放大近 3 倍),但多单大减、空单未变——说明是单向平多出逃。作为 FG2701 原本的第一大多头,华泰的突然撤退是重要的偏空信号。
|
||||||
|
|
||||||
|
### 4. 中信建投的异常空单操作(07-20 和 07-22)
|
||||||
|
|
||||||
|
中信建投空单:07-20 为 0.80 万,07-20 当天 +0.56 万 → 07-22 继续增至 1.50 万 → 07-23 降至 1.32 万 → 07-24 再降至 1.07 万。先猛加后缓减,这种操作模式更像是机构行为而非散户行为。将其归入"家人"分类在 FG2701 上可能不准确。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、行情预判
|
||||||
|
|
||||||
|
### 判断:偏空,但不宜追空(空头已高度拥挤)
|
||||||
|
|
||||||
|
**理由:**
|
||||||
|
|
||||||
|
1. **方向一致但过于拥挤**:全市场三大分类全部偏空,总净空 -2.46 万。国泰君安一家就净空 3.15 万。当一个方向上所有人都已入场,反向的边际力量反而更大——任何风吹草动都可能触发空头回补。
|
||||||
|
|
||||||
|
2. **产业套保逻辑清晰**:FG2701 对应明年 1 月交割,是玻璃消费淡季。玻璃厂在远月合约做卖出套保(锁定销售价格)是合理的产业行为。国泰君安的 4.54 万空单大概率是产业客户的套保盘,这类资金通常持有到期,不会频繁交易。所以这 4.54 万更像是"压舱石"而非"投机空头"。
|
||||||
|
|
||||||
|
3. **跨期结构已定价**:近强远弱(FG2609 强多 → FG2701 强空)的期限结构已经非常清晰。如果你已经做空 FG2701,这个结构已经 price in。真正的边际变化要看:
|
||||||
|
- 近月 FG2609 的空头回补能否带动远月跟涨
|
||||||
|
- 国泰君安是否会在 FG2701 上继续加空
|
||||||
|
|
||||||
|
4. **周五空头集中加仓可能是尾部信号**:07-24 其他席位集中加空 +1.33 万,如果这是新空头的"最后一波"入场,那么空头动能可能即将衰竭。
|
||||||
|
|
||||||
|
### 情景推演
|
||||||
|
|
||||||
|
| 情景 | 触发条件 | FG2701 走势 |
|
||||||
|
|------|---------|------------|
|
||||||
|
| 基准(60%) | FG2609 继续反弹,国泰君安维持空单 | 震荡偏弱,跟随主连但涨幅受限 |
|
||||||
|
| 空头回补(25%) | FG2609 大幅反弹,国泰君安开始平 FG2701 空单 | 涨幅可能超过 FG2609(空头踩踏) |
|
||||||
|
| 继续加空(15%) | 市场情绪恶化,产业端加大套保力度 | 独立走弱,跌幅大于 FG2609 |
|
||||||
|
|
||||||
|
**综合判断:偏空(概率 60%),但风险收益比已不适合追空。FG2701 的空头过于拥挤,一旦触发回补行情,反弹速度和幅度可能超预期。建议以观望为主,若持有空单则设好止盈,若空仓则等待更明确的信号。FG2609 的偏多判断和 FG2701 的偏空判断并不矛盾——它们反映的是同一品种不同到期月份的合理价差结构。**
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
# 玻璃期货综合持仓与成交量分析(2026-07-27 盘前)
|
||||||
|
|
||||||
|
基于 FG2609/FG2611/FG2701 三份席位分析报告,对下个交易日(07-27 周一)的行情推演。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、三合约核心数据总览
|
||||||
|
|
||||||
|
| 指标 | FG2609(主连) | FG2611(次月) | FG2701(远月) |
|
||||||
|
|------|:------------:|:------------:|:------------:|
|
||||||
|
| 大户 5 日净多 | **+12.28 万** | -0.43 万 | -1.48 万 |
|
||||||
|
| 家人 5 日净多 | -7.15 万 | +0.20 万 | -0.23 万 |
|
||||||
|
| 全市场净多 | **+4.52 万** | +0.25 万 | **-2.46 万** |
|
||||||
|
| 方向 | **强烈偏多** | 震荡 | 明确偏空 |
|
||||||
|
| 主力信号 | 空头踩踏回补 | 流动性不足 | 产业套保压盘 |
|
||||||
|
|
||||||
|
**期限结构清晰:近强远弱,逐月递减。FG2609 是行情发动机,FG2611 跟随,FG2701 独立承压。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、交易量分析:放量集中在近月,空头回补是主要成交来源
|
||||||
|
|
||||||
|
**FG2609 成交量是核心。** 07-22(周三)国泰君安单日成交 38.13 万手,为全周最高,当天大户减空 -6.67 万手,中信 -2.20 万、高盛 -1.49 万、国泰 -0.93 万同步砍空。这是典型的**空头踩踏式放量**——主要成交不是新增多头在买,而是老空头在抢着平仓。
|
||||||
|
|
||||||
|
FG2611 的成交也佐证了"量在近月":中粮期货 07-23 成交 7.48 万手(周最高),07-24 骤降至 1.65 万手后锁仓不动,套保盘集中建仓后收手。
|
||||||
|
|
||||||
|
**FG2701 周五出现反常放量**——其他席位(国投 +0.54 万、广发 +0.46 万、瑞达 +0.23 万)合计加空 1.23 万手。周五通常是避险减仓日,此时加空说明有空头在押注周一继续下跌,这构成周一潜在的**空头拥挤风险**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、持仓结构分析:三合约构成跨期对手盘矩阵
|
||||||
|
|
||||||
|
**最核心的逻辑不在单个合约内部,而在三个合约之间的资金联动:**
|
||||||
|
|
||||||
|
### 高盛期货——跨期套利布局
|
||||||
|
|
||||||
|
| 合约 | 行为 | 5 日变动 |
|
||||||
|
|------|------|---------|
|
||||||
|
| FG2609 | **大幅减空** | -4.12 万 |
|
||||||
|
| FG2611 | **持续加空** | +0.96 万 |
|
||||||
|
| FG2701 | 锁仓不动 | ~0 |
|
||||||
|
|
||||||
|
高盛在做 **"多近月、空远月"的跨期套利**,不是单边看多或看空。它认为近月(09)被低估、远月(11)相对高估,近强远弱的价差会继续扩大。
|
||||||
|
|
||||||
|
### 国泰君安——三合约三个角色
|
||||||
|
|
||||||
|
| 合约 | 角色 | 持仓特征 |
|
||||||
|
|------|------|---------|
|
||||||
|
| FG2609 | 空头回补者 | 空单 18.81→17.47 万,减空 -1.34 万 |
|
||||||
|
| FG2611 | 双边加仓 | 多空同步增加,方向模糊 |
|
||||||
|
| FG2701 | **超级空头** | 空单 4.54 万,是多单的 3.3 倍 |
|
||||||
|
|
||||||
|
国泰君安在 FG2701 上 4.54 万空单占全市场约 30%,这大概率是产业套保盘(玻璃厂锁定明年 1 月卖出价格),持有到期、不频繁交易,是 FG2701 的长期压舱石。
|
||||||
|
|
||||||
|
### 永安期货——唯一的多空同向偏多席位
|
||||||
|
|
||||||
|
永安在 FG2609 上:多单 5 日递增(4.38→4.53→4.56→4.99→5.19 万,+0.81 万),空单递减(6.41→4.62 万,-1.79 万)。**多增空减,在所有大户中方向最干净、最统一。**
|
||||||
|
|
||||||
|
### 空头三巨头剩余敞口
|
||||||
|
|
||||||
|
| 席位 | 07-24 空单 | 5 日减空 | 剩余空头敞口 |
|
||||||
|
|------|-----------|---------|------------|
|
||||||
|
| 中信期货 | 18.07 万 | -3.54 万 | 仍很大 |
|
||||||
|
| 高盛期货 | 17.81 万 | -4.12 万 | 仍很大 |
|
||||||
|
| 国泰君安 | 17.47 万 | -1.34 万 | 仍很大 |
|
||||||
|
| **合计** | **53.35 万** | **-9.00 万** | — |
|
||||||
|
|
||||||
|
三家合计仍持有 53.35 万空单,上周只减了 9 万。**如果反弹延续,空头回补的买盘还远未耗尽。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、下个交易日(07-27)行情推演
|
||||||
|
|
||||||
|
### FG2609(主连):偏多(概率 65%),看反弹延续
|
||||||
|
|
||||||
|
**做多力量来源:**
|
||||||
|
|
||||||
|
1. **空头回补动能远未耗尽**:三大空头仍有 53.35 万敞口。反弹延续 → 空头继续砍仓 → 形成持续买盘的逻辑链条仍在。
|
||||||
|
2. **永安在持续加多**:唯一在价格低位连续 5 日递增多单的大户,方向最明确。
|
||||||
|
3. **散户是反向指标**:家人 5 日净空 -7.15 万,在下跌中止损多单。散户出清 + 大户接盘 = 典型的筑底资金结构。
|
||||||
|
|
||||||
|
**抑制上涨的因素:**
|
||||||
|
|
||||||
|
- 大户更多是"不空了"而非"大力做多"(减空 -12.93 万 vs 减多仅 -0.65 万)。空头回补驱动的反弹,一旦空头平完、没有新增多头接力,反弹就会熄火。
|
||||||
|
- 冠通期货连续两天放量(+9 万手),持仓方向未知,可能是新的空头力量入场。
|
||||||
|
|
||||||
|
**情景判断:**
|
||||||
|
|
||||||
|
| 情景 | 概率 | 条件 | FG2609 走势 |
|
||||||
|
|------|:---:|------|------------|
|
||||||
|
| 基准 | 65% | 空头回补延续,成交量维持 | 震荡偏强,小幅上行 |
|
||||||
|
| 回调 | 25% | 反弹到位,空头停止回补 | 冲高回落,转震荡 |
|
||||||
|
| 加速上涨 | 10% | 增量多头入场 + 空头踩踏 | 放量大涨 |
|
||||||
|
|
||||||
|
### FG2611(次月):跟随主连震荡,无独立行情
|
||||||
|
|
||||||
|
流动性太差,全周净多仅 +0.25 万。唯一关注点:**华泰期货 07-23 多单暴增 +0.74 万**——如果周一华泰继续加多,是偏多信号;如果减仓,说明只是一日游。
|
||||||
|
|
||||||
|
### FG2701(远月):偏空但有回补风险,不建议追空
|
||||||
|
|
||||||
|
- 三类资金一致偏空,总净空 -2.46 万,方向最统一。
|
||||||
|
- **但空头已高度拥挤**:国泰一家净空 3.15 万 + 周五新空头集中入场(+1.23 万)。当一个方向所有人都已上船,反向的边际冲击最大。
|
||||||
|
- **核心变量是 FG2609**:如果主连大幅反弹触发远月空头回补,FG2701 涨幅可能超 FG2609(空头踩踏)。国泰的 4.54 万套保盘不会动,但投机空头会跑得很快。
|
||||||
|
|
||||||
|
| 情景 | 概率 | 触发条件 | FG2701 走势 |
|
||||||
|
|------|:---:|---------|------------|
|
||||||
|
| 基准 | 60% | FG2609 小幅反弹,国泰维持空单 | 震荡偏弱,涨幅受限 |
|
||||||
|
| 空头回补 | 25% | FG2609 大幅反弹,投机空头平仓 | 涨幅可能超 FG2609 |
|
||||||
|
| 继续加空 | 15% | 市场情绪恶化,产业加大套保 | 独立走弱 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、综合结论
|
||||||
|
|
||||||
|
> **下个交易日整体判断:偏多震荡。FG2609 是核心战场,空头回补是主要驱动。FG2701 的空头拥挤是最大的潜在爆发点——如果主连大幅上涨触发远月空头回补,可能出现近远月同步拉升的极端行情。反之,如果主连反弹乏力、成交量萎缩,则空头回补行情接近尾声,谨慎追多。**
|
||||||
|
|
||||||
|
### 周一盘中关键观察指标
|
||||||
|
|
||||||
|
1. **FG2609 成交量**:放量 = 空头继续砍仓(偏多),缩量 = 回补动能衰竭(转震荡)
|
||||||
|
2. **中信/高盛/国泰空单变化**(盘后数据):继续下降则反弹延续
|
||||||
|
3. **FG2701 是否跟涨**:跟涨说明空头回补扩散;不跟涨是正常期限结构
|
||||||
|
4. **华泰 FG2611 多单**:继续增加则强化远月翻多信号
|
||||||
|
5. **冠通期货持仓方向**:如果其方向为多,则强化偏多;如果为空,则是新的压力来源
|
||||||
@@ -0,0 +1,395 @@
|
|||||||
|
"""Data collector — fetch OHLCV from akshare and upsert into daily_bars."""
|
||||||
|
import socket
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
socket.setdefaulttimeout(15)
|
||||||
|
from app.database import SessionLocal
|
||||||
|
from app.models import DailyBar, Contract
|
||||||
|
from app.engine.lock_strategy import compute_amp_5d
|
||||||
|
|
||||||
|
_progress = {"running": False, "label": "", "done": 0, "total": 0, "result": 0, "finished": False, "error": "", "started_at": 0}
|
||||||
|
_lock = threading.Lock()
|
||||||
|
|
||||||
|
STALE_SECONDS = 120
|
||||||
|
|
||||||
|
|
||||||
|
def get_progress() -> dict:
|
||||||
|
with _lock:
|
||||||
|
return dict(_progress)
|
||||||
|
|
||||||
|
|
||||||
|
def _start_bg(target, label):
|
||||||
|
with _lock:
|
||||||
|
now = time.time()
|
||||||
|
if _progress["running"] and (now - _progress["started_at"]) < STALE_SECONDS:
|
||||||
|
return False
|
||||||
|
_progress.update(running=True, finished=False, label=label, done=0, total=0, error="", started_at=now)
|
||||||
|
|
||||||
|
def _run():
|
||||||
|
try:
|
||||||
|
target()
|
||||||
|
except Exception as e:
|
||||||
|
with _lock:
|
||||||
|
_progress["error"] = str(e)
|
||||||
|
finally:
|
||||||
|
with _lock:
|
||||||
|
_progress["running"] = False
|
||||||
|
_progress["finished"] = True
|
||||||
|
_progress["done"] = _progress["total"]
|
||||||
|
|
||||||
|
threading.Thread(target=_run, daemon=True).start()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def sync_contract_bars_bg(contract_code: str) -> bool:
|
||||||
|
"""Sync bars for one contract in background. Returns True if started."""
|
||||||
|
code = contract_code.upper()
|
||||||
|
|
||||||
|
def _run():
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
latest = (
|
||||||
|
db.query(DailyBar.date)
|
||||||
|
.filter(DailyBar.contract == code)
|
||||||
|
.order_by(DailyBar.date.desc())
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
start_date = latest[0].isoformat() if latest else None
|
||||||
|
bars = fetch_contract_bars(code, start_date)
|
||||||
|
|
||||||
|
with _lock:
|
||||||
|
_progress["total"] = len(bars)
|
||||||
|
|
||||||
|
total_bars = len(bars)
|
||||||
|
inserted = 0
|
||||||
|
min_date = None
|
||||||
|
for i, bar in enumerate(bars):
|
||||||
|
existing = (
|
||||||
|
db.query(DailyBar)
|
||||||
|
.filter(DailyBar.contract == code, DailyBar.date == bar["date"])
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if not existing:
|
||||||
|
db.add(DailyBar(
|
||||||
|
contract=code, date=bar["date"],
|
||||||
|
open=bar["open"], close=bar["close"],
|
||||||
|
high=bar["high"], low=bar["low"],
|
||||||
|
))
|
||||||
|
inserted += 1
|
||||||
|
if min_date is None or bar["date"] < min_date:
|
||||||
|
min_date = bar["date"]
|
||||||
|
with _lock:
|
||||||
|
_progress["done"] = i + 1
|
||||||
|
_progress["label"] = f"同步行情 {code} · {i + 1}/{total_bars} 条"
|
||||||
|
|
||||||
|
if inserted > 0:
|
||||||
|
db.flush()
|
||||||
|
_recompute_amp(db, code, from_date=min_date)
|
||||||
|
db.commit()
|
||||||
|
with _lock:
|
||||||
|
_progress["result"] = inserted
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
return _start_bg(_run, f"同步行情 {code}")
|
||||||
|
|
||||||
|
|
||||||
|
def sync_positions_bg(contract_code: str) -> bool:
|
||||||
|
"""Sync position rankings for one contract in background. Returns True if started."""
|
||||||
|
from app.models import PositionRanking
|
||||||
|
code = contract_code.upper()
|
||||||
|
|
||||||
|
def _run():
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
existing_dates = {
|
||||||
|
r[0] for r in
|
||||||
|
db.query(PositionRanking.date)
|
||||||
|
.filter(PositionRanking.contract_code == code)
|
||||||
|
.distinct().all()
|
||||||
|
}
|
||||||
|
bar_dates = [
|
||||||
|
r[0] for r in
|
||||||
|
db.query(DailyBar.date)
|
||||||
|
.filter(DailyBar.contract == code)
|
||||||
|
.order_by(DailyBar.date.desc())
|
||||||
|
.limit(20)
|
||||||
|
.all()
|
||||||
|
]
|
||||||
|
missing = [d for d in bar_dates if d not in existing_dates]
|
||||||
|
total_missing = len(missing)
|
||||||
|
|
||||||
|
with _lock:
|
||||||
|
_progress["total"] = total_missing
|
||||||
|
if total_missing == 0:
|
||||||
|
_progress["label"] = f"同步持仓 {code} · 已是最新"
|
||||||
|
|
||||||
|
inserted = 0
|
||||||
|
for i, d in enumerate(missing):
|
||||||
|
rankings = fetch_position_rankings(code, d.strftime("%Y%m%d"))
|
||||||
|
for r in rankings:
|
||||||
|
db.add(PositionRanking(
|
||||||
|
contract_code=code, institution=r["institution"],
|
||||||
|
data_type=r["data_type"], date=d,
|
||||||
|
rank=r["rank"], value=r["value"], change=r["change"],
|
||||||
|
))
|
||||||
|
inserted += 1
|
||||||
|
db.flush()
|
||||||
|
with _lock:
|
||||||
|
_progress["done"] = i + 1
|
||||||
|
_progress["label"] = f"同步持仓 {code} · {i + 1}/{total_missing} 天"
|
||||||
|
db.commit()
|
||||||
|
with _lock:
|
||||||
|
_progress["result"] = inserted
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
return _start_bg(_run, f"同步持仓 {code}")
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_contract_bars(contract_code: str, start_date: str | None = None) -> list[dict]:
|
||||||
|
"""Fetch daily OHLCV for a single contract from akshare.
|
||||||
|
|
||||||
|
Returns list of {date, open, close, high, low} dicts.
|
||||||
|
Only returns bars on or after start_date when provided.
|
||||||
|
"""
|
||||||
|
import akshare as ak
|
||||||
|
|
||||||
|
try:
|
||||||
|
df = ak.futures_zh_daily_sina(symbol=contract_code.upper())
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[collector] akshare error for {contract_code}: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
if df is None or df.empty:
|
||||||
|
print(f"[collector] No data returned for {contract_code}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
filter_date = date.fromisoformat(start_date) if start_date else None
|
||||||
|
|
||||||
|
bars = []
|
||||||
|
for _, row in df.iterrows():
|
||||||
|
try:
|
||||||
|
val = row["date"]
|
||||||
|
if hasattr(val, "strftime"):
|
||||||
|
d = val.date() if hasattr(val, "date") else val
|
||||||
|
else:
|
||||||
|
d = date.fromisoformat(str(val)[:10])
|
||||||
|
|
||||||
|
if filter_date and d < filter_date:
|
||||||
|
continue
|
||||||
|
|
||||||
|
bars.append({
|
||||||
|
"date": d,
|
||||||
|
"open": float(row["open"]),
|
||||||
|
"close": float(row["close"]),
|
||||||
|
"high": float(row["high"]),
|
||||||
|
"low": float(row["low"]),
|
||||||
|
})
|
||||||
|
except (KeyError, ValueError, TypeError) as e:
|
||||||
|
print(f"[collector] skip row: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
return bars
|
||||||
|
|
||||||
|
|
||||||
|
def sync_active_contracts() -> dict:
|
||||||
|
"""Sync all active contracts. Returns {contract_code: new_bars_count}."""
|
||||||
|
db = SessionLocal()
|
||||||
|
results = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
active_contracts = (
|
||||||
|
db.query(Contract).filter(Contract.is_active == True).all()
|
||||||
|
)
|
||||||
|
|
||||||
|
for c in active_contracts:
|
||||||
|
count = _sync_one(db, c.code)
|
||||||
|
results[c.code] = count
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def sync_one_contract(contract_code: str) -> int:
|
||||||
|
"""Sync a single contract. Returns number of new bars inserted."""
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
count = _sync_one(db, contract_code.upper())
|
||||||
|
db.commit()
|
||||||
|
return count
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
def _sync_one(db, contract_code: str) -> int:
|
||||||
|
"""Internal: sync one contract using existing session."""
|
||||||
|
latest = (
|
||||||
|
db.query(DailyBar.date)
|
||||||
|
.filter(DailyBar.contract == contract_code)
|
||||||
|
.order_by(DailyBar.date.desc())
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
start_date = latest[0].isoformat() if latest else None
|
||||||
|
|
||||||
|
bars = fetch_contract_bars(contract_code, start_date)
|
||||||
|
if not bars:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
inserted = 0
|
||||||
|
min_date = None
|
||||||
|
for bar in bars:
|
||||||
|
existing = (
|
||||||
|
db.query(DailyBar)
|
||||||
|
.filter(
|
||||||
|
DailyBar.contract == contract_code,
|
||||||
|
DailyBar.date == bar["date"],
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if not existing:
|
||||||
|
db.add(DailyBar(
|
||||||
|
contract=contract_code,
|
||||||
|
date=bar["date"],
|
||||||
|
open=bar["open"],
|
||||||
|
close=bar["close"],
|
||||||
|
high=bar["high"],
|
||||||
|
low=bar["low"],
|
||||||
|
))
|
||||||
|
inserted += 1
|
||||||
|
if min_date is None or bar["date"] < min_date:
|
||||||
|
min_date = bar["date"]
|
||||||
|
|
||||||
|
if inserted > 0:
|
||||||
|
db.flush()
|
||||||
|
_recompute_amp(db, contract_code, from_date=min_date)
|
||||||
|
|
||||||
|
return inserted
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_position_rankings(contract_code: str, trade_date: str) -> list[dict]:
|
||||||
|
"""Fetch top-20 position rankings for a contract on a given date from akshare.
|
||||||
|
|
||||||
|
Calls the API 3 times (volume, long, short) and returns a unified list of dicts:
|
||||||
|
{data_type, rank, institution, value, change}
|
||||||
|
"""
|
||||||
|
import akshare as ak
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for sym, dtype in [("成交量", "volume"), ("多单持仓", "long"), ("空单持仓", "short")]:
|
||||||
|
try:
|
||||||
|
df = ak.futures_hold_pos_sina(symbol=sym, contract=contract_code.upper(), date=trade_date)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[collector] position akshare error for {contract_code} {dtype}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if df is None or df.empty:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for _, row in df.iterrows():
|
||||||
|
try:
|
||||||
|
inst = str(row["会员简称"]).strip()
|
||||||
|
rank = int(row["名次"])
|
||||||
|
if inst in ("-", ""):
|
||||||
|
inst = f"-{rank}"
|
||||||
|
results.append({
|
||||||
|
"data_type": dtype,
|
||||||
|
"rank": rank,
|
||||||
|
"institution": inst,
|
||||||
|
"value": int(row.iloc[2]),
|
||||||
|
"change": int(row["比上交易增减"]),
|
||||||
|
})
|
||||||
|
except (KeyError, ValueError, TypeError) as e:
|
||||||
|
print(f"[collector] position skip row: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def sync_position_rankings() -> dict:
|
||||||
|
"""Sync position rankings for all active contracts. Returns {contract_code: new_rows}."""
|
||||||
|
db = SessionLocal()
|
||||||
|
results = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
active_contracts = (
|
||||||
|
db.query(Contract).filter(Contract.is_active == True).all()
|
||||||
|
)
|
||||||
|
|
||||||
|
for c in active_contracts:
|
||||||
|
count = _sync_positions_for_contract(db, c.code)
|
||||||
|
results[c.code] = count
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def _sync_positions_for_contract(db, contract_code: str) -> int:
|
||||||
|
"""Sync position rankings for all dates that have bars but no position data."""
|
||||||
|
from app.models import PositionRanking
|
||||||
|
|
||||||
|
code = contract_code.upper()
|
||||||
|
|
||||||
|
existing_dates = {
|
||||||
|
r[0] for r in
|
||||||
|
db.query(PositionRanking.date)
|
||||||
|
.filter(PositionRanking.contract_code == code)
|
||||||
|
.distinct()
|
||||||
|
.all()
|
||||||
|
}
|
||||||
|
|
||||||
|
bar_dates = [
|
||||||
|
r[0] for r in
|
||||||
|
db.query(DailyBar.date)
|
||||||
|
.filter(DailyBar.contract == code)
|
||||||
|
.order_by(DailyBar.date.desc())
|
||||||
|
.limit(20)
|
||||||
|
.all()
|
||||||
|
]
|
||||||
|
inserted = 0
|
||||||
|
for d in bar_dates:
|
||||||
|
if d in existing_dates:
|
||||||
|
continue
|
||||||
|
date_str = d.strftime("%Y%m%d")
|
||||||
|
rankings = fetch_position_rankings(code, date_str)
|
||||||
|
for r in rankings:
|
||||||
|
db.add(PositionRanking(
|
||||||
|
contract_code=code,
|
||||||
|
institution=r["institution"],
|
||||||
|
data_type=r["data_type"],
|
||||||
|
date=d,
|
||||||
|
rank=r["rank"],
|
||||||
|
value=r["value"],
|
||||||
|
change=r["change"],
|
||||||
|
))
|
||||||
|
inserted += 1
|
||||||
|
db.flush()
|
||||||
|
|
||||||
|
return inserted
|
||||||
|
|
||||||
|
|
||||||
|
def _recompute_amp(db, contract_code: str, from_date: date | None = None):
|
||||||
|
"""Recompute amp_5d for bars of a contract from from_date onwards."""
|
||||||
|
bars = (
|
||||||
|
db.query(DailyBar)
|
||||||
|
.filter(DailyBar.contract == contract_code)
|
||||||
|
.order_by(DailyBar.date)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
start_idx = 0
|
||||||
|
if from_date:
|
||||||
|
for i, bar in enumerate(bars):
|
||||||
|
if bar.date >= from_date:
|
||||||
|
start_idx = i
|
||||||
|
break
|
||||||
|
for i, bar in enumerate(bars):
|
||||||
|
if i >= 5 and i >= start_idx:
|
||||||
|
bar.amp_5d = compute_amp_5d([b.diff for b in bars[i - 5 : i]])
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker, DeclarativeBase
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
DATA_DIR = Path(__file__).parent.parent / "data"
|
||||||
|
DATA_DIR.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
DATABASE_URL = f"sqlite:///{DATA_DIR / 'ft.db'}"
|
||||||
|
|
||||||
|
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
||||||
|
SessionLocal = sessionmaker(bind=engine)
|
||||||
|
|
||||||
|
|
||||||
|
class Base(DeclarativeBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
"""振幅锁仓策略计算引擎"""
|
||||||
|
|
||||||
|
|
||||||
|
def compute_amp_5d(diffs: list[float]) -> float:
|
||||||
|
"""近5日均振幅 = round(mean of 5 diffs)"""
|
||||||
|
if len(diffs) < 5:
|
||||||
|
return 0.0
|
||||||
|
return round(sum(diffs[-5:]) / 5)
|
||||||
|
|
||||||
|
|
||||||
|
def check_stop_profit(
|
||||||
|
active_profits: list[float],
|
||||||
|
locked_losses: list[float],
|
||||||
|
daily_hands: int,
|
||||||
|
amp_threshold: float,
|
||||||
|
) -> bool:
|
||||||
|
"""止盈条件: total_floating_profit >= N * A"""
|
||||||
|
total = sum(active_profits) + sum(locked_losses)
|
||||||
|
return total >= daily_hands * amp_threshold
|
||||||
|
|
||||||
|
|
||||||
|
def check_lock(
|
||||||
|
open_price: float,
|
||||||
|
current_price: float,
|
||||||
|
lock_threshold: float,
|
||||||
|
) -> bool:
|
||||||
|
"""锁仓条件: 空单亏损 >= 开仓时的振幅阈值"""
|
||||||
|
return (current_price - open_price) >= lock_threshold
|
||||||
|
|
||||||
|
|
||||||
|
def should_meltdown(lock_count: int, max_locks: int = 3) -> bool:
|
||||||
|
"""锁仓次数达到上限时熔断"""
|
||||||
|
return lock_count >= max_locks
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
from contextlib import asynccontextmanager
|
||||||
|
from pathlib import Path
|
||||||
|
from fastapi import FastAPI, Request
|
||||||
|
from fastapi.responses import RedirectResponse
|
||||||
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
|
from app.database import engine, Base, SessionLocal
|
||||||
|
from app.models import User
|
||||||
|
from app.seed import seed
|
||||||
|
from app.routers import contracts, admin, auth, positions, trades, option_trades
|
||||||
|
|
||||||
|
TEMPLATES_DIR = Path(__file__).parent / "templates"
|
||||||
|
|
||||||
|
|
||||||
|
def setup_jinja(app: FastAPI):
|
||||||
|
env = Environment(loader=FileSystemLoader(str(TEMPLATES_DIR)))
|
||||||
|
app.state.templates = env
|
||||||
|
|
||||||
|
|
||||||
|
class AuthMiddleware(BaseHTTPMiddleware):
|
||||||
|
async def dispatch(self, request: Request, call_next):
|
||||||
|
# Public paths
|
||||||
|
if request.url.path in ("/auth/login", "/auth/logout") or request.url.path.startswith("/auth/"):
|
||||||
|
return await call_next(request)
|
||||||
|
|
||||||
|
# Check session
|
||||||
|
user_id = request.cookies.get("ft_session")
|
||||||
|
if user_id:
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
user = db.query(User).filter(User.id == int(user_id)).first()
|
||||||
|
if user:
|
||||||
|
request.state.user = user
|
||||||
|
return await call_next(request)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
# Redirect to login
|
||||||
|
return RedirectResponse(f"/auth/login?next={request.url.path}", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
setup_jinja(app)
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
seed()
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
app = FastAPI(title="期货量化系统", lifespan=lifespan)
|
||||||
|
|
||||||
|
app.add_middleware(AuthMiddleware)
|
||||||
|
|
||||||
|
app.include_router(auth.router)
|
||||||
|
app.include_router(contracts.router)
|
||||||
|
app.include_router(admin.router)
|
||||||
|
app.include_router(positions.router)
|
||||||
|
app.include_router(trades.router)
|
||||||
|
app.include_router(option_trades.router)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
def root():
|
||||||
|
return RedirectResponse("/contracts")
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
from datetime import date
|
||||||
|
from sqlalchemy import String, Integer, Float, Date, ForeignKey, Boolean, UniqueConstraint
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
from app.database import Base
|
||||||
|
|
||||||
|
|
||||||
|
class Product(Base):
|
||||||
|
__tablename__ = "products"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
code: Mapped[str] = mapped_column(String(10), unique=True, index=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(20))
|
||||||
|
exchange: Mapped[str] = mapped_column(String(10), default="CZCE")
|
||||||
|
|
||||||
|
contracts: Mapped[list["Contract"]] = relationship(
|
||||||
|
back_populates="product", cascade="all, delete-orphan"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Contract(Base):
|
||||||
|
__tablename__ = "contracts"
|
||||||
|
__table_args__ = (UniqueConstraint("code"),)
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
product_id: Mapped[int] = mapped_column(ForeignKey("products.id"), index=True)
|
||||||
|
code: Mapped[str] = mapped_column(String(10), unique=True, index=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(30))
|
||||||
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||||
|
|
||||||
|
product: Mapped["Product"] = relationship(back_populates="contracts")
|
||||||
|
|
||||||
|
|
||||||
|
class DailyBar(Base):
|
||||||
|
__tablename__ = "daily_bars"
|
||||||
|
__table_args__ = (UniqueConstraint("contract", "date"),)
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
contract: Mapped[str] = mapped_column(String(10), index=True)
|
||||||
|
date: Mapped[date] = mapped_column(Date, index=True)
|
||||||
|
open: Mapped[float] = mapped_column(Float)
|
||||||
|
close: Mapped[float] = mapped_column(Float)
|
||||||
|
high: Mapped[float] = mapped_column(Float)
|
||||||
|
low: Mapped[float] = mapped_column(Float)
|
||||||
|
amp_5d: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def diff(self) -> float:
|
||||||
|
"""最高-最低价差"""
|
||||||
|
return self.high - self.low
|
||||||
|
|
||||||
|
|
||||||
|
class PositionRanking(Base):
|
||||||
|
__tablename__ = "position_rankings"
|
||||||
|
__table_args__ = (UniqueConstraint("contract_code", "institution", "data_type", "date"),)
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
contract_code: Mapped[str] = mapped_column(String(10), index=True)
|
||||||
|
institution: Mapped[str] = mapped_column(String(30), index=True)
|
||||||
|
data_type: Mapped[str] = mapped_column(String(10), index=True)
|
||||||
|
date: Mapped[date] = mapped_column(Date, index=True)
|
||||||
|
rank: Mapped[int] = mapped_column(Integer)
|
||||||
|
value: Mapped[int] = mapped_column(Integer)
|
||||||
|
change: Mapped[int] = mapped_column(Integer)
|
||||||
|
|
||||||
|
|
||||||
|
class User(Base):
|
||||||
|
__tablename__ = "users"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
username: Mapped[str] = mapped_column(String(50), unique=True)
|
||||||
|
password_hash: Mapped[str] = mapped_column(String(128))
|
||||||
|
role: Mapped[str] = mapped_column(String(20), default="user")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def hash_password(password: str) -> str:
|
||||||
|
salt = os.urandom(16)
|
||||||
|
dk = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, 100000)
|
||||||
|
return salt.hex() + ":" + dk.hex()
|
||||||
|
|
||||||
|
def check_password(self, password: str) -> bool:
|
||||||
|
try:
|
||||||
|
salt_hex, dk_hex = self.password_hash.split(":")
|
||||||
|
salt = bytes.fromhex(salt_hex)
|
||||||
|
dk = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, 100000)
|
||||||
|
return dk.hex() == dk_hex
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class Round(Base):
|
||||||
|
__tablename__ = "rounds"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
contract_code: Mapped[str] = mapped_column(String(10), index=True)
|
||||||
|
daily_hands: Mapped[int] = mapped_column(Integer, default=1)
|
||||||
|
max_locks: Mapped[int] = mapped_column(Integer, default=3)
|
||||||
|
status: Mapped[str] = mapped_column(String(20), default="active")
|
||||||
|
started_at: Mapped[date] = mapped_column(Date)
|
||||||
|
|
||||||
|
positions: Mapped[list["Position"]] = relationship(
|
||||||
|
back_populates="round", cascade="all, delete-orphan"
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total_locks(self) -> int:
|
||||||
|
return sum(p.locked_count for p in self.positions)
|
||||||
|
|
||||||
|
|
||||||
|
class Position(Base):
|
||||||
|
__tablename__ = "positions"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
round_id: Mapped[int] = mapped_column(ForeignKey("rounds.id"), index=True)
|
||||||
|
open_price: Mapped[int] = mapped_column(Integer)
|
||||||
|
amp_threshold: Mapped[int] = mapped_column(Integer)
|
||||||
|
lock_price: Mapped[int] = mapped_column(Integer)
|
||||||
|
hands: Mapped[int] = mapped_column(Integer, default=1)
|
||||||
|
locked_count: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
|
opened_at: Mapped[date] = mapped_column(Date)
|
||||||
|
|
||||||
|
round: Mapped["Round"] = relationship(back_populates="positions")
|
||||||
|
|
||||||
|
|
||||||
|
class Trade(Base):
|
||||||
|
__tablename__ = "trades"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
product_code: Mapped[str] = mapped_column(String(10))
|
||||||
|
contract_code: Mapped[str] = mapped_column(String(10), index=True)
|
||||||
|
direction: Mapped[str] = mapped_column(String(4))
|
||||||
|
open_date: Mapped[date] = mapped_column(Date)
|
||||||
|
open_price: Mapped[float] = mapped_column(Float)
|
||||||
|
open_fee: Mapped[float | None] = mapped_column(Float, nullable=True, default=0)
|
||||||
|
close_date: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||||||
|
close_price: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||||
|
close_fee: Mapped[float | None] = mapped_column(Float, nullable=True, default=0)
|
||||||
|
status: Mapped[str] = mapped_column(String(10), default="open")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pnl(self) -> float | None:
|
||||||
|
if self.close_price is None:
|
||||||
|
return None
|
||||||
|
mul = 20 # glass futures point value
|
||||||
|
if self.direction == "long":
|
||||||
|
result = (self.close_price - self.open_price) * mul - (self.open_fee or 0) - (self.close_fee or 0)
|
||||||
|
else:
|
||||||
|
result = (self.open_price - self.close_price) * mul - (self.open_fee or 0) - (self.close_fee or 0)
|
||||||
|
return round(result, 2)
|
||||||
|
|
||||||
|
|
||||||
|
class OptionTrade(Base):
|
||||||
|
__tablename__ = "option_trades"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
product_code: Mapped[str] = mapped_column(String(10))
|
||||||
|
contract_code: Mapped[str] = mapped_column(String(10), index=True)
|
||||||
|
option_type: Mapped[str] = mapped_column(String(4))
|
||||||
|
direction: Mapped[str] = mapped_column(String(4))
|
||||||
|
strike_price: Mapped[float] = mapped_column(Float)
|
||||||
|
open_date: Mapped[date] = mapped_column(Date)
|
||||||
|
open_price: Mapped[float] = mapped_column(Float)
|
||||||
|
open_fee: Mapped[float | None] = mapped_column(Float, nullable=True, default=0)
|
||||||
|
close_date: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||||||
|
close_price: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||||
|
close_fee: Mapped[float | None] = mapped_column(Float, nullable=True, default=0)
|
||||||
|
status: Mapped[str] = mapped_column(String(10), default="open")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pnl(self) -> float | None:
|
||||||
|
if self.close_price is None:
|
||||||
|
return None
|
||||||
|
mul = 20 # glass futures point value
|
||||||
|
if self.direction == "sell":
|
||||||
|
result = (self.open_price - self.close_price) * mul - (self.open_fee or 0) - (self.close_fee or 0)
|
||||||
|
else:
|
||||||
|
result = (self.close_price - self.open_price) * mul - (self.open_fee or 0) - (self.close_fee or 0)
|
||||||
|
return round(result, 2)
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
"""持仓分析 prompt 生成器。
|
||||||
|
|
||||||
|
根据最近 5 个交易日的主力持仓排名生成结构化分析 prompt,
|
||||||
|
按席位属性(家人/大户/其他/噪音)分类汇总,聚焦资金风向变化。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
|
from datetime import date, timedelta
|
||||||
|
|
||||||
|
from app.database import SessionLocal
|
||||||
|
from app.models import PositionRanking, Contract
|
||||||
|
|
||||||
|
|
||||||
|
# ── 席位分类 ──
|
||||||
|
FAMILY_NAMES = [
|
||||||
|
"东方财富", "徽商期货", "方正中期", "中信建投", "平安期货",
|
||||||
|
]
|
||||||
|
WHALE_NAMES = [
|
||||||
|
"永安期货", "中信期货", "国泰君安", "海通期货", "华泰期货",
|
||||||
|
"高盛期货", "乾坤期货", "摩根大通", "中粮期货",
|
||||||
|
]
|
||||||
|
NOISE_NAMES = ["东证期货"]
|
||||||
|
|
||||||
|
TYPE_LABEL = {"volume": "成交量", "long": "多单持仓", "short": "空单持仓"}
|
||||||
|
|
||||||
|
|
||||||
|
def _classify(inst: str) -> str:
|
||||||
|
"""匹配席位分类,返回 家人/大户/噪音/其他。"""
|
||||||
|
for kw in FAMILY_NAMES:
|
||||||
|
if kw in inst:
|
||||||
|
return "家人"
|
||||||
|
for kw in WHALE_NAMES:
|
||||||
|
if kw in inst:
|
||||||
|
return "大户"
|
||||||
|
for kw in NOISE_NAMES:
|
||||||
|
if kw in inst:
|
||||||
|
return "噪音"
|
||||||
|
return "其他"
|
||||||
|
|
||||||
|
|
||||||
|
def _build_legend() -> str:
|
||||||
|
"""生成席位分类图例。"""
|
||||||
|
lines = ["## 席位分类", ""]
|
||||||
|
lines.append(f"- **家人席位(散户)**: {'、'.join(FAMILY_NAMES)}")
|
||||||
|
lines.append(f"- **大户席位(产业/套保)**: {'、'.join(WHALE_NAMES)}")
|
||||||
|
lines.append(f"- **噪音席位(忽略)**: {'、'.join(NOISE_NAMES)}")
|
||||||
|
lines.append("- **其他席位**: 未归入以上三类的席位")
|
||||||
|
lines.append("")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def _format_number(n: int) -> str:
|
||||||
|
"""数字格式化为万手。"""
|
||||||
|
v = n / 10000
|
||||||
|
sign = "+" if n > 0 else ""
|
||||||
|
return f"{sign}{v:.2f}万"
|
||||||
|
|
||||||
|
|
||||||
|
def build_prompt(contract_code: str | None = None) -> str:
|
||||||
|
"""生成单个或全部活跃合约的持仓分析 prompt。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
contract_code: 合约代码(如 FG2609),为 None 时分析所有活跃合约。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
可直接发送给 AI 进行分析的 prompt 文本。
|
||||||
|
"""
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
if contract_code:
|
||||||
|
codes = [contract_code.upper()]
|
||||||
|
else:
|
||||||
|
codes = [
|
||||||
|
r[0] for r in
|
||||||
|
db.query(Contract.code)
|
||||||
|
.filter(Contract.is_active == True)
|
||||||
|
.order_by(Contract.code)
|
||||||
|
.all()
|
||||||
|
]
|
||||||
|
|
||||||
|
# 取最近 5 个有持仓数据的交易日
|
||||||
|
all_dates = sorted({
|
||||||
|
r[0] for r in
|
||||||
|
db.query(PositionRanking.date)
|
||||||
|
.filter(PositionRanking.contract_code.in_(codes))
|
||||||
|
.distinct()
|
||||||
|
.all()
|
||||||
|
}, reverse=True)[:5]
|
||||||
|
all_dates.reverse() # 从旧到新
|
||||||
|
|
||||||
|
if not all_dates:
|
||||||
|
return "无持仓数据"
|
||||||
|
|
||||||
|
# 拉取全部数据
|
||||||
|
rankings = (
|
||||||
|
db.query(PositionRanking)
|
||||||
|
.filter(
|
||||||
|
PositionRanking.contract_code.in_(codes),
|
||||||
|
PositionRanking.date.in_(all_dates),
|
||||||
|
)
|
||||||
|
.order_by(PositionRanking.contract_code, PositionRanking.date, PositionRanking.data_type, PositionRanking.rank)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
# 按 contract → date → data_type 组织
|
||||||
|
data: dict[str, dict] = defaultdict(lambda: defaultdict(lambda: defaultdict(list)))
|
||||||
|
for r in rankings:
|
||||||
|
data[r.contract_code][r.date][r.data_type].append(r)
|
||||||
|
|
||||||
|
parts = [_build_legend()]
|
||||||
|
|
||||||
|
for code in codes:
|
||||||
|
if code not in data:
|
||||||
|
continue
|
||||||
|
cd = data[code]
|
||||||
|
parts.append(f"## {code}")
|
||||||
|
parts.append("")
|
||||||
|
parts.append(f"分析区间: {min(all_dates)} → {max(all_dates)}(共 {len(all_dates)} 个交易日)")
|
||||||
|
parts.append("")
|
||||||
|
|
||||||
|
# ── 每日明细 ──
|
||||||
|
for d in all_dates:
|
||||||
|
if d not in cd:
|
||||||
|
continue
|
||||||
|
parts.append(f"### {d} {_weekday_zh(d)}")
|
||||||
|
parts.append("")
|
||||||
|
|
||||||
|
for dtype in ["volume", "long", "short"]:
|
||||||
|
rows = cd[d].get(dtype, [])
|
||||||
|
if not rows:
|
||||||
|
continue
|
||||||
|
parts.append(f"**{TYPE_LABEL[dtype]} Top 20**")
|
||||||
|
parts.append("")
|
||||||
|
parts.append("| 排名 | 席位 | 分类 | 持仓量 | 增减 |")
|
||||||
|
parts.append("|------|------|------|--------|------|")
|
||||||
|
for r in rows[:20]:
|
||||||
|
tag = _classify(r.institution)
|
||||||
|
if tag == "噪音":
|
||||||
|
continue # 跳过东证
|
||||||
|
parts.append(
|
||||||
|
f"| {r.rank} | {r.institution} | {tag} | "
|
||||||
|
f"{_format_number(r.value)} | {_format_number(r.change)} |"
|
||||||
|
)
|
||||||
|
parts.append("")
|
||||||
|
|
||||||
|
# ── 每日家人 vs 大户汇总 ──
|
||||||
|
parts.append(f"### {code} 分类资金变化汇总")
|
||||||
|
parts.append("")
|
||||||
|
parts.append("| 日期 | 家人多单增减 | 家人空单增减 | 大户多单增减 | 大户空单增减 | 其他多单增减 | 其他空单增减 |")
|
||||||
|
parts.append("|------|-------------|-------------|-------------|-------------|-------------|-------------|")
|
||||||
|
|
||||||
|
for d in all_dates:
|
||||||
|
if d not in cd:
|
||||||
|
continue
|
||||||
|
cat_changes = defaultdict(lambda: defaultdict(int))
|
||||||
|
for dtype in ["long", "short"]:
|
||||||
|
for r in cd[d].get(dtype, []):
|
||||||
|
tag = _classify(r.institution)
|
||||||
|
if tag == "噪音":
|
||||||
|
continue
|
||||||
|
cat_changes[tag][dtype] += r.change
|
||||||
|
|
||||||
|
parts.append(
|
||||||
|
f"| {d} "
|
||||||
|
f"| {_format_number(cat_changes['家人']['long'])} "
|
||||||
|
f"| {_format_number(cat_changes['家人']['short'])} "
|
||||||
|
f"| {_format_number(cat_changes['大户']['long'])} "
|
||||||
|
f"| {_format_number(cat_changes['大户']['short'])} "
|
||||||
|
f"| {_format_number(cat_changes['其他']['long'])} "
|
||||||
|
f"| {_format_number(cat_changes['其他']['short'])} |"
|
||||||
|
)
|
||||||
|
parts.append("")
|
||||||
|
|
||||||
|
# ── 5日累计 ──
|
||||||
|
parts.append(f"### {code} 5日累计资金变化")
|
||||||
|
parts.append("")
|
||||||
|
total_cat = defaultdict(lambda: defaultdict(int))
|
||||||
|
for d in all_dates:
|
||||||
|
if d not in cd:
|
||||||
|
continue
|
||||||
|
for dtype in ["long", "short"]:
|
||||||
|
for r in cd[d].get(dtype, []):
|
||||||
|
tag = _classify(r.institution)
|
||||||
|
if tag == "噪音":
|
||||||
|
continue
|
||||||
|
total_cat[tag][dtype] += r.change
|
||||||
|
|
||||||
|
parts.append("| 分类 | 5日多单累计 | 5日空单累计 | 净多(多-空) |")
|
||||||
|
parts.append("|------|------------|------------|--------------|")
|
||||||
|
for tag in ["家人", "大户", "其他"]:
|
||||||
|
net = total_cat[tag]["long"] - total_cat[tag]["short"]
|
||||||
|
net_str = _format_number(net)
|
||||||
|
parts.append(
|
||||||
|
f"| {tag} | {_format_number(total_cat[tag]['long'])} "
|
||||||
|
f"| {_format_number(total_cat[tag]['short'])} | {net_str} |"
|
||||||
|
)
|
||||||
|
parts.append("")
|
||||||
|
|
||||||
|
# ── 分析引导 ──
|
||||||
|
parts.append("---")
|
||||||
|
parts.append("")
|
||||||
|
parts.append("请基于以上数据进行分析,重点关注:")
|
||||||
|
parts.append("")
|
||||||
|
parts.append("1. **家人席位动向**: 5日内多空增减趋势,散户情绪偏多还是偏空,"
|
||||||
|
"是否有明显的追涨杀跌行为。")
|
||||||
|
parts.append("2. **大户席位动向**: 产业资金和套保资金的布局方向,"
|
||||||
|
"多空增减是否与家人席位形成对手盘。")
|
||||||
|
parts.append("3. **多空力量对比**: 各分类的净多/净空方向,"
|
||||||
|
"整体市场情绪偏向。")
|
||||||
|
parts.append("4. **异常信号**: 单日大幅增减的席位,"
|
||||||
|
"分类资金出现明显分歧的交易日。")
|
||||||
|
parts.append("5. **行情预判**: 结合席位资金流向,"
|
||||||
|
"对下一交易日走势给出偏多/偏空/震荡的判断及理由。")
|
||||||
|
|
||||||
|
return "\n".join(parts)
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
def _weekday_zh(d: date) -> str:
|
||||||
|
return ["周一", "周二", "周三", "周四", "周五", "周六", "周日"][d.weekday()]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, "/app")
|
||||||
|
from app.prompt_builder import build_prompt
|
||||||
|
prompt = build_prompt()
|
||||||
|
print(prompt)
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
from fastapi import APIRouter, Depends, Form, Request
|
||||||
|
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.database import get_db
|
||||||
|
from app.models import Product, Contract, DailyBar, PositionRanking
|
||||||
|
from app.collector import (
|
||||||
|
sync_one_contract, sync_contract_bars_bg, sync_positions_bg, get_progress,
|
||||||
|
)
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/admin", tags=["admin"])
|
||||||
|
|
||||||
|
EXCHANGES = ["CZCE", "DCE", "SHFE", "CFFEX", "INE"]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_class=HTMLResponse)
|
||||||
|
def admin_page(request: Request, db: Session = Depends(get_db)):
|
||||||
|
products = db.query(Product).order_by(Product.code).all()
|
||||||
|
|
||||||
|
product_data = []
|
||||||
|
for p in products:
|
||||||
|
contracts = (
|
||||||
|
db.query(Contract)
|
||||||
|
.filter(Contract.product_id == p.id)
|
||||||
|
.order_by(Contract.code)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
contract_list = []
|
||||||
|
for c in contracts:
|
||||||
|
bar_count = (
|
||||||
|
db.query(DailyBar)
|
||||||
|
.filter(DailyBar.contract == c.code)
|
||||||
|
.count()
|
||||||
|
)
|
||||||
|
pos_days = len({
|
||||||
|
r[0] for r in
|
||||||
|
db.query(PositionRanking.date)
|
||||||
|
.filter(PositionRanking.contract_code == c.code)
|
||||||
|
.distinct().all()
|
||||||
|
})
|
||||||
|
contract_list.append({
|
||||||
|
"id": c.id, "code": c.code, "name": c.name,
|
||||||
|
"is_active": c.is_active, "bar_count": bar_count,
|
||||||
|
"pos_days": pos_days,
|
||||||
|
})
|
||||||
|
|
||||||
|
product_data.append({
|
||||||
|
"id": p.id,
|
||||||
|
"code": p.code,
|
||||||
|
"name": p.name,
|
||||||
|
"exchange": p.exchange,
|
||||||
|
"contracts": contract_list,
|
||||||
|
})
|
||||||
|
|
||||||
|
total_contracts = sum(len(pd["contracts"]) for pd in product_data)
|
||||||
|
|
||||||
|
template = request.app.state.templates.get_template("admin.html")
|
||||||
|
return HTMLResponse(
|
||||||
|
template.render(
|
||||||
|
request=request,
|
||||||
|
active_nav="admin",
|
||||||
|
products=product_data,
|
||||||
|
total_contracts=total_contracts,
|
||||||
|
exchanges=EXCHANGES,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/product")
|
||||||
|
def create_product(
|
||||||
|
request: Request,
|
||||||
|
code: str = Form(...),
|
||||||
|
name: str = Form(...),
|
||||||
|
exchange: str = Form(...),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
existing = db.query(Product).filter(Product.code == code.upper()).first()
|
||||||
|
if not existing:
|
||||||
|
p = Product(code=code.upper(), name=name, exchange=exchange)
|
||||||
|
db.add(p)
|
||||||
|
db.commit()
|
||||||
|
return RedirectResponse("/admin/?tab=product", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/contract")
|
||||||
|
def create_contract(
|
||||||
|
request: Request,
|
||||||
|
product_id: int = Form(...),
|
||||||
|
month: str = Form(...),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
product = db.query(Product).filter(Product.id == product_id).first()
|
||||||
|
if not product:
|
||||||
|
return RedirectResponse("/admin/?tab=contract", status_code=303)
|
||||||
|
|
||||||
|
code = (product.code + month).upper()
|
||||||
|
name = month.upper()
|
||||||
|
|
||||||
|
existing = db.query(Contract).filter(Contract.code == code).first()
|
||||||
|
if not existing:
|
||||||
|
c = Contract(
|
||||||
|
product_id=product_id,
|
||||||
|
code=code,
|
||||||
|
name=name,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
db.add(c)
|
||||||
|
db.commit()
|
||||||
|
return RedirectResponse("/admin/?tab=contract", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/contract/{contract_id}/toggle")
|
||||||
|
def toggle_contract(contract_id: int, db: Session = Depends(get_db)):
|
||||||
|
c = db.query(Contract).filter(Contract.id == contract_id).first()
|
||||||
|
if c:
|
||||||
|
c.is_active = not c.is_active
|
||||||
|
db.commit()
|
||||||
|
return RedirectResponse("/admin/?tab=contract", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/contract/{contract_id}/delete")
|
||||||
|
def delete_contract(contract_id: int, db: Session = Depends(get_db)):
|
||||||
|
c = db.query(Contract).filter(Contract.id == contract_id).first()
|
||||||
|
if c:
|
||||||
|
db.query(DailyBar).filter(DailyBar.contract == c.code).delete()
|
||||||
|
db.query(PositionRanking).filter(PositionRanking.contract_code == c.code).delete()
|
||||||
|
db.delete(c)
|
||||||
|
db.commit()
|
||||||
|
return RedirectResponse("/admin/?tab=contract", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/product/{product_id}/delete")
|
||||||
|
def delete_product(product_id: int, db: Session = Depends(get_db)):
|
||||||
|
p = db.query(Product).filter(Product.id == product_id).first()
|
||||||
|
if p:
|
||||||
|
for c in p.contracts:
|
||||||
|
db.query(DailyBar).filter(DailyBar.contract == c.code).delete()
|
||||||
|
db.query(PositionRanking).filter(PositionRanking.contract_code == c.code).delete()
|
||||||
|
db.delete(p)
|
||||||
|
db.commit()
|
||||||
|
return RedirectResponse("/admin/?tab=product", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/sync/{contract_code}")
|
||||||
|
def sync_single(contract_code: str):
|
||||||
|
count = sync_one_contract(contract_code.upper())
|
||||||
|
return RedirectResponse(f"/admin/?tab=sync&synced={count}", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/sync/{contract_code}/bg")
|
||||||
|
def sync_single_bg(contract_code: str):
|
||||||
|
ok = sync_contract_bars_bg(contract_code.upper())
|
||||||
|
return JSONResponse({"ok": ok})
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/sync-positions/{contract_code}/bg")
|
||||||
|
def sync_positions_bg_ep(contract_code: str):
|
||||||
|
ok = sync_positions_bg(contract_code.upper())
|
||||||
|
return JSONResponse({"ok": ok})
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/sync-status")
|
||||||
|
def sync_status():
|
||||||
|
return JSONResponse(get_progress())
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/sync/product/{product_id}")
|
||||||
|
def sync_product(product_id: int, request: Request, db: Session = Depends(get_db)):
|
||||||
|
contracts = db.query(Contract).filter(
|
||||||
|
Contract.product_id == product_id, Contract.is_active == True
|
||||||
|
).all()
|
||||||
|
total = 0
|
||||||
|
for c in contracts:
|
||||||
|
total += sync_one_contract(c.code)
|
||||||
|
return RedirectResponse(f"/admin/?tab=sync&synced={total}", status_code=303)
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
from fastapi import APIRouter, Depends, Form, Request
|
||||||
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.database import get_db
|
||||||
|
from app.models import User
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/auth", tags=["auth"])
|
||||||
|
|
||||||
|
SESSION_COOKIE = "ft_session"
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/login", response_class=HTMLResponse)
|
||||||
|
def login_page(request: Request):
|
||||||
|
template = request.app.state.templates.get_template("login.html")
|
||||||
|
return HTMLResponse(template.render(request=request))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/login")
|
||||||
|
def login(
|
||||||
|
request: Request,
|
||||||
|
username: str = Form(...),
|
||||||
|
password: str = Form(...),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
user = db.query(User).filter(User.username == username).first()
|
||||||
|
if not user or not user.check_password(password):
|
||||||
|
template = request.app.state.templates.get_template("login.html")
|
||||||
|
return HTMLResponse(
|
||||||
|
template.render(request=request, error="用户名或密码错误"),
|
||||||
|
status_code=401,
|
||||||
|
)
|
||||||
|
|
||||||
|
resp = RedirectResponse("/contracts/", status_code=303)
|
||||||
|
resp.set_cookie(SESSION_COOKIE, str(user.id), httponly=True)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/logout")
|
||||||
|
def logout():
|
||||||
|
resp = RedirectResponse("/auth/login", status_code=303)
|
||||||
|
resp.delete_cookie(SESSION_COOKIE)
|
||||||
|
return resp
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
import math
|
||||||
|
from datetime import date, timedelta
|
||||||
|
from fastapi import APIRouter, Depends, Request, Query
|
||||||
|
from fastapi.responses import HTMLResponse, PlainTextResponse
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.database import get_db
|
||||||
|
from app.models import DailyBar, Contract, PositionRanking
|
||||||
|
from app.prompt_builder import build_prompt
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/contracts", tags=["contracts"])
|
||||||
|
|
||||||
|
WEEKDAY_ZH = {0: "周一", 1: "周二", 2: "周三", 3: "周四", 4: "周五", 5: "周六", 6: "周日"}
|
||||||
|
PAGE_SIZE = 7
|
||||||
|
|
||||||
|
|
||||||
|
def get_active_contracts(db: Session) -> list[str]:
|
||||||
|
contracts = (
|
||||||
|
db.query(Contract.code)
|
||||||
|
.filter(Contract.is_active == True)
|
||||||
|
.order_by(Contract.code)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
return [c[0] for c in contracts]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_class=HTMLResponse)
|
||||||
|
def contract_index(request: Request, db: Session = Depends(get_db)):
|
||||||
|
from app.models import Product
|
||||||
|
|
||||||
|
# Build product → contracts mapping, ordered by product code
|
||||||
|
products = (
|
||||||
|
db.query(Product)
|
||||||
|
.order_by(Product.code)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
product_groups = []
|
||||||
|
all_codes = []
|
||||||
|
for prod in products:
|
||||||
|
codes = [
|
||||||
|
c.code for c in prod.contracts if c.is_active
|
||||||
|
]
|
||||||
|
if codes:
|
||||||
|
product_groups.append({
|
||||||
|
"code": prod.code,
|
||||||
|
"name": prod.name,
|
||||||
|
"exchange": prod.exchange,
|
||||||
|
"contracts": codes,
|
||||||
|
})
|
||||||
|
all_codes.extend(codes)
|
||||||
|
|
||||||
|
latest = (
|
||||||
|
db.query(DailyBar)
|
||||||
|
.filter(DailyBar.contract.in_(all_codes))
|
||||||
|
.order_by(DailyBar.date.desc())
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
contract_bars = {}
|
||||||
|
for bar in latest:
|
||||||
|
if bar.contract not in contract_bars:
|
||||||
|
contract_bars[bar.contract] = bar
|
||||||
|
|
||||||
|
# Next-day amplitude: mean of latest 5 diffs per contract
|
||||||
|
contract_next_amp = {}
|
||||||
|
for code in all_codes:
|
||||||
|
diffs = [
|
||||||
|
r[0] for r in
|
||||||
|
db.query(DailyBar.high - DailyBar.low)
|
||||||
|
.filter(DailyBar.contract == code)
|
||||||
|
.order_by(DailyBar.date.desc())
|
||||||
|
.limit(5)
|
||||||
|
.all()
|
||||||
|
]
|
||||||
|
if len(diffs) >= 5:
|
||||||
|
contract_next_amp[code] = round(sum(diffs) / 5)
|
||||||
|
|
||||||
|
template = request.app.state.templates.get_template("index.html")
|
||||||
|
return HTMLResponse(
|
||||||
|
template.render(
|
||||||
|
request=request,
|
||||||
|
active_nav="contracts",
|
||||||
|
product_groups=product_groups,
|
||||||
|
contract_bars=contract_bars,
|
||||||
|
contract_next_amp=contract_next_amp,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{contract}", response_class=HTMLResponse)
|
||||||
|
def contract_detail(
|
||||||
|
request: Request,
|
||||||
|
contract: str,
|
||||||
|
pos_date: str | None = None,
|
||||||
|
page: int = Query(1, ge=1),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
active_contracts = get_active_contracts(db)
|
||||||
|
bars = (
|
||||||
|
db.query(DailyBar)
|
||||||
|
.filter(DailyBar.contract == contract.upper())
|
||||||
|
.order_by(DailyBar.date.desc())
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
total = len(bars)
|
||||||
|
total_pages = max(1, math.ceil(total / PAGE_SIZE))
|
||||||
|
page = min(page, total_pages)
|
||||||
|
start = (page - 1) * PAGE_SIZE
|
||||||
|
page_bars = bars[start:start + PAGE_SIZE]
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
for i, bar in enumerate(page_bars):
|
||||||
|
global_idx = start + i
|
||||||
|
rows.append({
|
||||||
|
"global_idx": global_idx,
|
||||||
|
"date": bar.date.strftime("%Y/%-m/%-d"),
|
||||||
|
"weekday": WEEKDAY_ZH.get(bar.date.weekday(), ""),
|
||||||
|
"open": int(bar.open) if bar.open else "-",
|
||||||
|
"close": int(bar.close) if bar.close else "-",
|
||||||
|
"high": int(bar.high) if bar.high else "-",
|
||||||
|
"low": int(bar.low) if bar.low else "-",
|
||||||
|
"diff": int(bar.diff) if bar.diff else 0,
|
||||||
|
"amp_5d": int(bar.amp_5d) if bar.amp_5d is not None else None,
|
||||||
|
"has_amp": bar.amp_5d is not None,
|
||||||
|
})
|
||||||
|
|
||||||
|
latest = bars[0] if bars else None
|
||||||
|
|
||||||
|
# Dates that have position data (for date picker)
|
||||||
|
pos_dates = [
|
||||||
|
r[0] for r in
|
||||||
|
db.query(PositionRanking.date)
|
||||||
|
.filter(PositionRanking.contract_code == contract.upper())
|
||||||
|
.distinct()
|
||||||
|
.order_by(PositionRanking.date.desc())
|
||||||
|
.all()
|
||||||
|
]
|
||||||
|
|
||||||
|
# Determine which date to show position rankings for
|
||||||
|
if pos_date:
|
||||||
|
try:
|
||||||
|
selected_date = date.fromisoformat(pos_date)
|
||||||
|
except ValueError:
|
||||||
|
selected_date = pos_dates[0] if pos_dates else None
|
||||||
|
else:
|
||||||
|
selected_date = pos_dates[0] if pos_dates else None
|
||||||
|
|
||||||
|
# Position rankings for the selected date
|
||||||
|
pos_data = {"volume": [], "long": [], "short": []}
|
||||||
|
if selected_date:
|
||||||
|
rankings = (
|
||||||
|
db.query(PositionRanking)
|
||||||
|
.filter(
|
||||||
|
PositionRanking.contract_code == contract.upper(),
|
||||||
|
PositionRanking.date == selected_date,
|
||||||
|
)
|
||||||
|
.order_by(PositionRanking.data_type, PositionRanking.rank)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
for r in rankings:
|
||||||
|
pos_data[r.data_type].append({
|
||||||
|
"rank": r.rank,
|
||||||
|
"institution": r.institution,
|
||||||
|
"value": r.value,
|
||||||
|
"change": r.change,
|
||||||
|
})
|
||||||
|
|
||||||
|
pos_totals = {
|
||||||
|
"long": sum(r.change for r in rankings if r.data_type == "long"),
|
||||||
|
"short": sum(r.change for r in rankings if r.data_type == "short"),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
pos_totals = {"long": 0, "short": 0}
|
||||||
|
|
||||||
|
# Predict next trading day amplitude: mean of latest 5 diffs
|
||||||
|
# Compute next trading date
|
||||||
|
next_date = latest.date + timedelta(days=1) if latest else None
|
||||||
|
if next_date and next_date.weekday() >= 5:
|
||||||
|
next_date += timedelta(days=7 - next_date.weekday())
|
||||||
|
|
||||||
|
# Predict next trading day amplitude
|
||||||
|
next_amp = None
|
||||||
|
if len(bars) >= 5:
|
||||||
|
next_amp = round(sum(b.diff for b in bars[:5]) / 5)
|
||||||
|
template = request.app.state.templates.get_template("contract.html")
|
||||||
|
return HTMLResponse(
|
||||||
|
template.render(
|
||||||
|
request=request,
|
||||||
|
active_nav="contracts",
|
||||||
|
contract=contract.upper(),
|
||||||
|
contracts=active_contracts,
|
||||||
|
rows=rows,
|
||||||
|
latest=latest,
|
||||||
|
pos_dates=pos_dates,
|
||||||
|
selected_pos_date=selected_date,
|
||||||
|
pos_data=pos_data,
|
||||||
|
pos_totals=pos_totals,
|
||||||
|
next_amp=next_amp,
|
||||||
|
next_date=next_date.strftime("%Y/%-m/%-d") if next_date else None,
|
||||||
|
next_weekday=WEEKDAY_ZH.get(next_date.weekday(), "") if next_date else "",
|
||||||
|
row_count=len(rows),
|
||||||
|
total_rows=total,
|
||||||
|
page=page,
|
||||||
|
total_pages=total_pages,
|
||||||
|
pos_date=pos_date or "",
|
||||||
|
WEEKDAY_ZH=WEEKDAY_ZH,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{contract}/analysis-prompt", response_class=PlainTextResponse)
|
||||||
|
def contract_analysis_prompt(contract: str):
|
||||||
|
"""返回合约的持仓分析 prompt 文本。"""
|
||||||
|
return build_prompt(contract.upper())
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
from datetime import date
|
||||||
|
from fastapi import APIRouter, Depends, Form, Request
|
||||||
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.database import get_db
|
||||||
|
from app.models import OptionTrade, Contract, Product
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/options", tags=["options"])
|
||||||
|
|
||||||
|
OPTION_TYPES = [("C", "C 看涨"), ("P", "P 看跌")]
|
||||||
|
OPTION_DIRECTIONS = [("buy", "买"), ("sell", "卖")]
|
||||||
|
|
||||||
|
|
||||||
|
def get_product_contracts(db: Session) -> dict:
|
||||||
|
result: dict[str, list[str]] = {}
|
||||||
|
products = db.query(Product).order_by(Product.code).all()
|
||||||
|
for p in products:
|
||||||
|
codes = [c.code for c in p.contracts if c.is_active]
|
||||||
|
if codes:
|
||||||
|
result[p.code] = codes
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def today_str() -> str:
|
||||||
|
return date.today().isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_class=HTMLResponse)
|
||||||
|
def option_page(request: Request, db: Session = Depends(get_db)):
|
||||||
|
view = request.query_params.get("view", "")
|
||||||
|
open_trades = (
|
||||||
|
db.query(OptionTrade).filter(OptionTrade.status == "open")
|
||||||
|
.order_by(OptionTrade.open_date.desc()).all()
|
||||||
|
)
|
||||||
|
closed_trades = (
|
||||||
|
db.query(OptionTrade).filter(OptionTrade.status == "closed")
|
||||||
|
.order_by(OptionTrade.close_date.desc()).limit(50).all()
|
||||||
|
)
|
||||||
|
|
||||||
|
template = request.app.state.templates.get_template("options.html")
|
||||||
|
return HTMLResponse(
|
||||||
|
template.render(
|
||||||
|
request=request,
|
||||||
|
active_nav="options",
|
||||||
|
product_contracts=get_product_contracts(db),
|
||||||
|
option_types=OPTION_TYPES,
|
||||||
|
option_directions=OPTION_DIRECTIONS,
|
||||||
|
open_trades=open_trades,
|
||||||
|
closed_trades=closed_trades,
|
||||||
|
today=today_str(),
|
||||||
|
weekdays=["周一","周二","周三","周四","周五","周六","周日"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/open")
|
||||||
|
def open_trade(
|
||||||
|
request: Request,
|
||||||
|
contract_code: str = Form(...),
|
||||||
|
option_type: str = Form(...),
|
||||||
|
direction: str = Form(...),
|
||||||
|
strike_price: float = Form(...),
|
||||||
|
open_date: str = Form(...),
|
||||||
|
open_price: float = Form(...),
|
||||||
|
open_fee: float = Form(0.0),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
code = contract_code.upper()
|
||||||
|
contract = db.query(Contract).filter(Contract.code == code).first()
|
||||||
|
product_code = contract.product.code if contract else code[:2]
|
||||||
|
|
||||||
|
t = OptionTrade(
|
||||||
|
product_code=product_code,
|
||||||
|
contract_code=code,
|
||||||
|
option_type=option_type,
|
||||||
|
direction=direction,
|
||||||
|
strike_price=strike_price,
|
||||||
|
open_date=date.fromisoformat(open_date),
|
||||||
|
open_price=open_price,
|
||||||
|
open_fee=open_fee,
|
||||||
|
status="open",
|
||||||
|
)
|
||||||
|
db.add(t)
|
||||||
|
db.commit()
|
||||||
|
return RedirectResponse("/options/", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{trade_id}/close")
|
||||||
|
def close_trade(
|
||||||
|
request: Request,
|
||||||
|
trade_id: int,
|
||||||
|
close_date: str = Form(...),
|
||||||
|
close_price: float = Form(...),
|
||||||
|
close_fee: float = Form(0.0),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
t = db.query(OptionTrade).filter(OptionTrade.id == trade_id).first()
|
||||||
|
if t and t.status == "open":
|
||||||
|
t.close_date = date.fromisoformat(close_date)
|
||||||
|
t.close_price = close_price
|
||||||
|
t.close_fee = close_fee
|
||||||
|
t.status = "closed"
|
||||||
|
db.commit()
|
||||||
|
return RedirectResponse("/options/", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{trade_id}/edit")
|
||||||
|
def edit_trade(
|
||||||
|
request: Request,
|
||||||
|
trade_id: int,
|
||||||
|
contract_code: str = Form(...),
|
||||||
|
option_type: str = Form(...),
|
||||||
|
direction: str = Form(...),
|
||||||
|
strike_price: float = Form(...),
|
||||||
|
open_date: str = Form(...),
|
||||||
|
open_price: float = Form(...),
|
||||||
|
open_fee: float = Form(0.0),
|
||||||
|
close_date: str = Form(""),
|
||||||
|
close_price: str = Form(""),
|
||||||
|
close_fee: str = Form(""),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
t = db.query(OptionTrade).filter(OptionTrade.id == trade_id).first()
|
||||||
|
if t:
|
||||||
|
code = contract_code.upper()
|
||||||
|
contract = db.query(Contract).filter(Contract.code == code).first()
|
||||||
|
t.product_code = contract.product.code if contract else code[:2]
|
||||||
|
t.contract_code = code
|
||||||
|
t.option_type = option_type
|
||||||
|
t.direction = direction
|
||||||
|
t.strike_price = strike_price
|
||||||
|
t.open_date = date.fromisoformat(open_date)
|
||||||
|
t.open_price = open_price
|
||||||
|
t.open_fee = open_fee
|
||||||
|
if close_date and close_price:
|
||||||
|
t.close_date = date.fromisoformat(close_date)
|
||||||
|
t.close_price = float(close_price)
|
||||||
|
t.close_fee = float(close_fee) if close_fee else 0.0
|
||||||
|
db.commit()
|
||||||
|
return RedirectResponse("/options/", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{trade_id}/delete")
|
||||||
|
def delete_trade(trade_id: int, db: Session = Depends(get_db)):
|
||||||
|
t = db.query(OptionTrade).filter(OptionTrade.id == trade_id).first()
|
||||||
|
if t:
|
||||||
|
db.delete(t)
|
||||||
|
db.commit()
|
||||||
|
return RedirectResponse("/options/", status_code=303)
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
from datetime import date
|
||||||
|
from fastapi import APIRouter, Depends, Form, Request
|
||||||
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.database import get_db
|
||||||
|
from app.models import Round, Position, Contract, Product
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/positions", tags=["positions"])
|
||||||
|
|
||||||
|
|
||||||
|
def get_active_contracts(db: Session) -> list[str]:
|
||||||
|
contracts = (
|
||||||
|
db.query(Contract.code)
|
||||||
|
.filter(Contract.is_active == True)
|
||||||
|
.order_by(Contract.code)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
return [c[0] for c in contracts]
|
||||||
|
|
||||||
|
|
||||||
|
def get_product_contracts(db: Session) -> dict:
|
||||||
|
result: dict[str, list[str]] = {}
|
||||||
|
products = db.query(Product).order_by(Product.code).all()
|
||||||
|
for p in products:
|
||||||
|
codes = [c.code for c in p.contracts if c.is_active]
|
||||||
|
if codes:
|
||||||
|
result[p.code] = codes
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_class=HTMLResponse)
|
||||||
|
def positions_page(request: Request, db: Session = Depends(get_db)):
|
||||||
|
active_rounds = (
|
||||||
|
db.query(Round)
|
||||||
|
.filter(Round.status == "active")
|
||||||
|
.order_by(Round.started_at.desc())
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
template = request.app.state.templates.get_template("positions.html")
|
||||||
|
return HTMLResponse(
|
||||||
|
template.render(
|
||||||
|
request=request,
|
||||||
|
active_nav="positions",
|
||||||
|
contracts=get_active_contracts(db),
|
||||||
|
product_contracts=get_product_contracts(db),
|
||||||
|
active_rounds=active_rounds,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/round")
|
||||||
|
def create_round(
|
||||||
|
request: Request,
|
||||||
|
contract_code: str = Form(...),
|
||||||
|
daily_hands: int = Form(1),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
code = contract_code.upper()
|
||||||
|
existing = db.query(Round).filter(
|
||||||
|
Round.status == "active", Round.contract_code == code
|
||||||
|
).first()
|
||||||
|
if existing:
|
||||||
|
return RedirectResponse(f"/positions/?error={code} 已有活跃轮次", status_code=303)
|
||||||
|
|
||||||
|
r = Round(
|
||||||
|
contract_code=code,
|
||||||
|
daily_hands=daily_hands,
|
||||||
|
status="active",
|
||||||
|
started_at=date.today(),
|
||||||
|
)
|
||||||
|
db.add(r)
|
||||||
|
db.commit()
|
||||||
|
return RedirectResponse("/positions/", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{round_id}/add")
|
||||||
|
def add_position(
|
||||||
|
request: Request,
|
||||||
|
round_id: int,
|
||||||
|
open_price: int = Form(...),
|
||||||
|
amp_threshold: int = Form(...),
|
||||||
|
lock_price: str = Form(""),
|
||||||
|
hands: int = Form(1),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
r = db.query(Round).filter(Round.id == round_id).first()
|
||||||
|
if not r or r.status != "active":
|
||||||
|
return RedirectResponse("/positions/?error=轮次不存在或已结束", status_code=303)
|
||||||
|
|
||||||
|
final_lock = int(lock_price) if lock_price else open_price + amp_threshold
|
||||||
|
p = Position(
|
||||||
|
round_id=r.id,
|
||||||
|
open_price=open_price,
|
||||||
|
amp_threshold=amp_threshold,
|
||||||
|
lock_price=final_lock,
|
||||||
|
hands=hands,
|
||||||
|
opened_at=date.today(),
|
||||||
|
)
|
||||||
|
db.add(p)
|
||||||
|
db.commit()
|
||||||
|
return RedirectResponse("/positions/", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{position_id}/lock")
|
||||||
|
def lock_position(
|
||||||
|
position_id: int,
|
||||||
|
lock_price: str = Form(""),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
p = db.query(Position).filter(Position.id == position_id).first()
|
||||||
|
if p and p.locked_count < p.hands:
|
||||||
|
if lock_price:
|
||||||
|
p.lock_price = int(lock_price)
|
||||||
|
p.locked_count += 1
|
||||||
|
db.commit()
|
||||||
|
return RedirectResponse("/positions/", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{position_id}/unlock")
|
||||||
|
def unlock_position(position_id: int, db: Session = Depends(get_db)):
|
||||||
|
p = db.query(Position).filter(Position.id == position_id).first()
|
||||||
|
if p and p.locked_count > 0:
|
||||||
|
p.locked_count -= 1
|
||||||
|
db.commit()
|
||||||
|
return RedirectResponse("/positions/", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{position_id}/update-lock-price")
|
||||||
|
def update_lock_price(
|
||||||
|
position_id: int,
|
||||||
|
lock_price: int = Form(...),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
p = db.query(Position).filter(Position.id == position_id).first()
|
||||||
|
if p and p.locked_count == 0:
|
||||||
|
p.lock_price = lock_price
|
||||||
|
db.commit()
|
||||||
|
return RedirectResponse("/positions/", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{round_id}/max-locks")
|
||||||
|
def update_max_locks(
|
||||||
|
round_id: int,
|
||||||
|
max_locks: int = Form(...),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
r = db.query(Round).filter(Round.id == round_id).first()
|
||||||
|
if r and r.status == "active" and 1 <= max_locks <= 10:
|
||||||
|
r.max_locks = max_locks
|
||||||
|
db.commit()
|
||||||
|
return RedirectResponse("/positions/", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{round_id}/close")
|
||||||
|
def close_round(
|
||||||
|
request: Request,
|
||||||
|
round_id: int,
|
||||||
|
result: str = Form(...),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
r = db.query(Round).filter(Round.id == round_id).first()
|
||||||
|
if r and r.status == "active":
|
||||||
|
r.status = result
|
||||||
|
db.commit()
|
||||||
|
return RedirectResponse("/positions/", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{round_id}/delete")
|
||||||
|
def delete_round(round_id: int, db: Session = Depends(get_db)):
|
||||||
|
r = db.query(Round).filter(Round.id == round_id).first()
|
||||||
|
if r:
|
||||||
|
db.delete(r)
|
||||||
|
db.commit()
|
||||||
|
return RedirectResponse("/positions/", status_code=303)
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
from datetime import date
|
||||||
|
from fastapi import APIRouter, Depends, Form, Request
|
||||||
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.database import get_db
|
||||||
|
from app.models import Trade, Contract, Product
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/trades", tags=["trades"])
|
||||||
|
|
||||||
|
DIRECTIONS = [("short", "空"), ("long", "多")]
|
||||||
|
|
||||||
|
|
||||||
|
def get_active_contracts(db: Session) -> list[str]:
|
||||||
|
contracts = (
|
||||||
|
db.query(Contract.code)
|
||||||
|
.filter(Contract.is_active == True)
|
||||||
|
.order_by(Contract.code)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
return [c[0] for c in contracts]
|
||||||
|
|
||||||
|
|
||||||
|
def get_product_contracts(db: Session) -> dict:
|
||||||
|
"""Return {product_code: [contract_codes]} for active contracts."""
|
||||||
|
result: dict[str, list[str]] = {}
|
||||||
|
products = db.query(Product).order_by(Product.code).all()
|
||||||
|
for p in products:
|
||||||
|
codes = [c.code for c in p.contracts if c.is_active]
|
||||||
|
if codes:
|
||||||
|
result[p.code] = codes
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def today_str() -> str:
|
||||||
|
return date.today().isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_class=HTMLResponse)
|
||||||
|
def trades_page(request: Request, db: Session = Depends(get_db)):
|
||||||
|
open_trades = (
|
||||||
|
db.query(Trade).filter(Trade.status == "open")
|
||||||
|
.order_by(Trade.open_date.desc()).all()
|
||||||
|
)
|
||||||
|
closed_trades = (
|
||||||
|
db.query(Trade).filter(Trade.status == "closed")
|
||||||
|
.order_by(Trade.close_date.desc()).limit(50).all()
|
||||||
|
)
|
||||||
|
|
||||||
|
template = request.app.state.templates.get_template("trades.html")
|
||||||
|
return HTMLResponse(
|
||||||
|
template.render(
|
||||||
|
request=request,
|
||||||
|
active_nav="trades",
|
||||||
|
contracts=get_active_contracts(db),
|
||||||
|
product_contracts=get_product_contracts(db),
|
||||||
|
directions=DIRECTIONS,
|
||||||
|
open_trades=open_trades,
|
||||||
|
closed_trades=closed_trades,
|
||||||
|
today=today_str(),
|
||||||
|
weekdays=["周一","周二","周三","周四","周五","周六","周日"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/open")
|
||||||
|
def open_trade(
|
||||||
|
request: Request,
|
||||||
|
contract_code: str = Form(...),
|
||||||
|
direction: str = Form(...),
|
||||||
|
open_date: str = Form(...),
|
||||||
|
open_price: float = Form(...),
|
||||||
|
open_fee: float = Form(0.0),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
code = contract_code.upper()
|
||||||
|
contract = db.query(Contract).filter(Contract.code == code).first()
|
||||||
|
product_code = contract.product.code if contract else code[:2]
|
||||||
|
|
||||||
|
t = Trade(
|
||||||
|
product_code=product_code,
|
||||||
|
contract_code=code,
|
||||||
|
direction=direction,
|
||||||
|
open_date=date.fromisoformat(open_date),
|
||||||
|
open_price=open_price,
|
||||||
|
open_fee=open_fee,
|
||||||
|
status="open",
|
||||||
|
)
|
||||||
|
db.add(t)
|
||||||
|
db.commit()
|
||||||
|
return RedirectResponse("/trades/", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{trade_id}/close")
|
||||||
|
def close_trade(
|
||||||
|
request: Request,
|
||||||
|
trade_id: int,
|
||||||
|
close_date: str = Form(...),
|
||||||
|
close_price: float = Form(...),
|
||||||
|
close_fee: float = Form(0.0),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
t = db.query(Trade).filter(Trade.id == trade_id).first()
|
||||||
|
if t and t.status == "open":
|
||||||
|
t.close_date = date.fromisoformat(close_date)
|
||||||
|
t.close_price = close_price
|
||||||
|
t.close_fee = close_fee
|
||||||
|
t.status = "closed"
|
||||||
|
db.commit()
|
||||||
|
return RedirectResponse("/trades/", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{trade_id}/edit")
|
||||||
|
def edit_trade(
|
||||||
|
request: Request,
|
||||||
|
trade_id: int,
|
||||||
|
contract_code: str = Form(...),
|
||||||
|
direction: str = Form(...),
|
||||||
|
open_date: str = Form(...),
|
||||||
|
open_price: float = Form(...),
|
||||||
|
open_fee: float = Form(0.0),
|
||||||
|
close_date: str = Form(""),
|
||||||
|
close_price: str = Form(""),
|
||||||
|
close_fee: str = Form(""),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
t = db.query(Trade).filter(Trade.id == trade_id).first()
|
||||||
|
if t:
|
||||||
|
code = contract_code.upper()
|
||||||
|
contract = db.query(Contract).filter(Contract.code == code).first()
|
||||||
|
t.product_code = contract.product.code if contract else code[:2]
|
||||||
|
t.contract_code = code
|
||||||
|
t.direction = direction
|
||||||
|
t.open_date = date.fromisoformat(open_date)
|
||||||
|
t.open_price = open_price
|
||||||
|
t.open_fee = open_fee
|
||||||
|
if close_date and close_price:
|
||||||
|
t.close_date = date.fromisoformat(close_date)
|
||||||
|
t.close_price = float(close_price)
|
||||||
|
t.close_fee = float(close_fee) if close_fee else 0.0
|
||||||
|
db.commit()
|
||||||
|
return RedirectResponse("/trades/", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{trade_id}/delete")
|
||||||
|
def delete_trade(trade_id: int, db: Session = Depends(get_db)):
|
||||||
|
t = db.query(Trade).filter(Trade.id == trade_id).first()
|
||||||
|
if t:
|
||||||
|
db.delete(t)
|
||||||
|
db.commit()
|
||||||
|
return RedirectResponse("/trades/", status_code=303)
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
"""Seed database from existing data files. Run once manually or on first start."""
|
||||||
|
from datetime import date
|
||||||
|
from app.database import engine, Base, SessionLocal
|
||||||
|
from app.models import DailyBar, Product, Contract, User
|
||||||
|
from app.engine.lock_strategy import compute_amp_5d
|
||||||
|
|
||||||
|
# --- Seed OHLCV data ---
|
||||||
|
SEED_CONTRACT = "FG2609"
|
||||||
|
SEED_BARS: list[dict] = [
|
||||||
|
{"date": "2026-07-01", "open": 972, "close": 961, "high": 972, "low": 956},
|
||||||
|
{"date": "2026-07-02", "open": 960, "close": 966, "high": 972, "low": 957},
|
||||||
|
{"date": "2026-07-03", "open": 965, "close": 973, "high": 979, "low": 962},
|
||||||
|
{"date": "2026-07-06", "open": 977, "close": 967, "high": 982, "low": 964},
|
||||||
|
{"date": "2026-07-07", "open": 966, "close": 952, "high": 966, "low": 951},
|
||||||
|
{"date": "2026-07-08", "open": 954, "close": 962, "high": 966, "low": 951},
|
||||||
|
{"date": "2026-07-09", "open": 963, "close": 957, "high": 964, "low": 949},
|
||||||
|
{"date": "2026-07-10", "open": 956, "close": 965, "high": 974, "low": 952},
|
||||||
|
{"date": "2026-07-13", "open": 965, "close": 952, "high": 967, "low": 950},
|
||||||
|
{"date": "2026-07-14", "open": 954, "close": 951, "high": 956, "low": 944},
|
||||||
|
{"date": "2026-07-15", "open": 950, "close": 949, "high": 958, "low": 944},
|
||||||
|
{"date": "2026-07-16", "open": 949, "close": 929, "high": 952, "low": 927},
|
||||||
|
{"date": "2026-07-17", "open": 931, "close": 900, "high": 931, "low": 899},
|
||||||
|
{"date": "2026-07-20", "open": 902, "close": 890, "high": 908, "low": 889},
|
||||||
|
{"date": "2026-07-21", "open": 891, "close": 904, "high": 910, "low": 891},
|
||||||
|
{"date": "2026-07-22", "open": 908, "close": 913, "high": 918, "low": 900},
|
||||||
|
{"date": "2026-07-23", "open": 914, "close": 899, "high": 917, "low": 892},
|
||||||
|
{"date": "2026-07-24", "open": 900, "close": 908, "high": 912, "low": 891},
|
||||||
|
]
|
||||||
|
|
||||||
|
def _migrate(engine):
|
||||||
|
"""Add missing columns to existing tables (SQLite doesn't auto-migrate)."""
|
||||||
|
import sqlite3
|
||||||
|
conn = engine.raw_connection()
|
||||||
|
try:
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("PRAGMA table_info(rounds)")
|
||||||
|
cols = {row[1] for row in cur.fetchall()}
|
||||||
|
if "max_locks" not in cols:
|
||||||
|
cur.execute("ALTER TABLE rounds ADD COLUMN max_locks INTEGER DEFAULT 3")
|
||||||
|
conn.commit()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def seed():
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
_migrate(engine)
|
||||||
|
db = SessionLocal()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# --- Seed default user ---
|
||||||
|
if not db.query(User).first():
|
||||||
|
admin = User(
|
||||||
|
username="admin",
|
||||||
|
password_hash=User.hash_password("admin123"),
|
||||||
|
role="admin",
|
||||||
|
)
|
||||||
|
db.add(admin)
|
||||||
|
db.flush()
|
||||||
|
|
||||||
|
# --- Seed products and contracts ---
|
||||||
|
if not db.query(Product).first():
|
||||||
|
fg = Product(code="FG", name="玻璃", exchange="CZCE")
|
||||||
|
db.add(fg)
|
||||||
|
db.flush()
|
||||||
|
db.add_all([
|
||||||
|
Contract(product_id=fg.id, code="FG2609", name="玻璃2609", is_active=True),
|
||||||
|
Contract(product_id=fg.id, code="FG2610", name="玻璃2610", is_active=True),
|
||||||
|
Contract(product_id=fg.id, code="FG2611", name="玻璃2611", is_active=True),
|
||||||
|
Contract(product_id=fg.id, code="FG2701", name="玻璃2701", is_active=True),
|
||||||
|
])
|
||||||
|
|
||||||
|
# --- Seed daily bars ---
|
||||||
|
existing_dates = {
|
||||||
|
(r.contract, r.date)
|
||||||
|
for r in db.query(DailyBar.contract, DailyBar.date).all()
|
||||||
|
}
|
||||||
|
|
||||||
|
bars_to_insert = []
|
||||||
|
for bar in SEED_BARS:
|
||||||
|
d = date.fromisoformat(bar["date"])
|
||||||
|
if (SEED_CONTRACT, d) not in existing_dates:
|
||||||
|
bars_to_insert.append(
|
||||||
|
DailyBar(
|
||||||
|
contract=SEED_CONTRACT,
|
||||||
|
date=d,
|
||||||
|
open=bar["open"],
|
||||||
|
close=bar["close"],
|
||||||
|
high=bar["high"],
|
||||||
|
low=bar["low"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if bars_to_insert:
|
||||||
|
db.add_all(bars_to_insert)
|
||||||
|
db.flush()
|
||||||
|
|
||||||
|
# Compute amp_5d
|
||||||
|
seed_bars = (
|
||||||
|
db.query(DailyBar)
|
||||||
|
.filter(DailyBar.contract == SEED_CONTRACT)
|
||||||
|
.order_by(DailyBar.date)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
for i, bar in enumerate(seed_bars):
|
||||||
|
if i >= 5:
|
||||||
|
bar.amp_5d = compute_amp_5d([b.diff for b in seed_bars[i - 5 : i]])
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
print(f"Seeded {len(bars_to_insert)} bars")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
seed()
|
||||||
@@ -0,0 +1,348 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}系统管理{% endblock %}
|
||||||
|
{% block heading %}系统管理{% endblock %}
|
||||||
|
{% block breadcrumb %}品种与合约{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.tabs { display: flex; gap: 0; margin-bottom: 24px; border-bottom: 2px solid var(--border); }
|
||||||
|
.tab-btn {
|
||||||
|
padding: 10px 20px; border: none; background: none;
|
||||||
|
font-size: 0.88rem; font-weight: 500; color: var(--sub);
|
||||||
|
cursor: pointer; text-decoration: none; border-bottom: 2px solid transparent;
|
||||||
|
margin-bottom: -2px; transition: color .12s, border-color .12s;
|
||||||
|
}
|
||||||
|
.tab-btn:hover { color: var(--fg); }
|
||||||
|
.tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; }
|
||||||
|
.tab-panel { display: none; }
|
||||||
|
.tab-panel.active { display: block; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
{% set tab = request.query_params.get('tab', 'sync') %}
|
||||||
|
|
||||||
|
<div class="tabs">
|
||||||
|
<button class="tab-btn{% if tab == 'sync' %} active{% endif %}" onclick="switchTab('sync')">📡 数据同步</button>
|
||||||
|
<button class="tab-btn{% if tab == 'product' %} active{% endif %}" onclick="switchTab('product')">🏷️ 品种管理</button>
|
||||||
|
<button class="tab-btn{% if tab == 'contract' %} active{% endif %}" onclick="switchTab('contract')">📋 合约管理</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# ═══════════════════ Tab: 数据同步 ═══════════════════ #}
|
||||||
|
<div class="tab-panel{% if tab == 'sync' %} active{% endif %}" id="tab-sync">
|
||||||
|
<div style="display:flex;align-items:center;gap:12px;margin-bottom:14px;">
|
||||||
|
<span style="font-size:0.78rem;color:var(--sub);">共 {{ total_contracts }} 个合约</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="sync-progress" style="display:none;margin-bottom:18px;padding:12px 16px;background:var(--surface);border:1px solid var(--border);border-radius:8px;">
|
||||||
|
<div style="display:flex;align-items:center;gap:10px;margin-bottom:6px;">
|
||||||
|
<span id="sync-progress-label" style="font-size:0.82rem;color:var(--fg);">正在同步...</span>
|
||||||
|
<span id="sync-progress-pct" style="font-size:0.8rem;color:var(--sub);margin-left:auto;"></span>
|
||||||
|
</div>
|
||||||
|
<div style="width:100%;height:5px;background:var(--border);border-radius:3px;overflow:hidden;">
|
||||||
|
<div id="sync-progress-bar" style="width:0%;height:100%;background:var(--accent);border-radius:3px;transition:width .3s;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if products %}
|
||||||
|
{% for p in products %}
|
||||||
|
<div style="margin-bottom:16px;border:1px solid var(--border);border-radius:10px;overflow:hidden;">
|
||||||
|
<div class="product-header"
|
||||||
|
style="display:flex;align-items:center;gap:12px;padding:12px 18px;background:var(--th-bg);cursor:pointer;user-select:none;"
|
||||||
|
onclick="toggleProduct(this)">
|
||||||
|
<span class="collapse-arrow" style="font-size:0.75rem;transition:transform .2s;display:inline-block;transform:rotate(-90deg);">▼</span>
|
||||||
|
<strong style="font-size:0.92rem;">{{ p.code }} · {{ p.name }}</strong>
|
||||||
|
<span style="font-size:0.78rem;color:var(--sub);">{{ p.exchange }}</span>
|
||||||
|
<span style="font-size:0.78rem;color:var(--sub);margin-left:8px;">{{ p.contracts|length }} 合约</span>
|
||||||
|
<form method="post" action="/admin/sync/product/{{ p.id }}" style="display:inline;margin-left:auto;" onclick="event.stopPropagation()">
|
||||||
|
<button type="submit" class="btn" style="font-size:0.78rem;padding:4px 14px;background:var(--accent);color:#fff;border-radius:5px;">同步行情</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="product-body" style="display:none;">
|
||||||
|
<table style="font-size:0.84rem;">
|
||||||
|
<tr><th style="text-align:left;padding:8px 14px;">合约</th><th style="text-align:center;padding:8px 14px;">行情</th><th style="text-align:center;padding:8px 14px;">持仓</th><th style="text-align:center;padding:8px 14px;">状态</th><th style="text-align:center;padding:8px 14px;">操作</th></tr>
|
||||||
|
{% for c in p.contracts %}
|
||||||
|
<tr>
|
||||||
|
<td style="text-align:left;padding:6px 14px;"><strong>{{ c.code }}</strong></td>
|
||||||
|
<td style="text-align:center;padding:6px 14px;">{{ c.bar_count }}</td>
|
||||||
|
<td style="text-align:center;padding:6px 14px;">{{ c.pos_days }}</td>
|
||||||
|
<td style="text-align:center;padding:6px 14px;">
|
||||||
|
{% if c.is_active %}<span class="badge badge-up">启用</span>{% else %}<span class="badge badge-down">停用</span>{% endif %}
|
||||||
|
</td>
|
||||||
|
<td style="text-align:center;padding:6px 14px;">
|
||||||
|
<a href="/contracts/{{ c.code }}" style="color:var(--accent);text-decoration:none;font-size:0.82rem;">查看</a>
|
||||||
|
<button style="background:none;border:none;color:var(--accent);cursor:pointer;font-size:0.82rem;margin-left:12px;" onclick="syncBars('{{ c.code }}')">↻ 行情</button>
|
||||||
|
<button style="background:none;border:none;color:var(--success);cursor:pointer;font-size:0.82rem;margin-left:10px;" onclick="syncPositions('{{ c.code }}')">↻ 持仓</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div style="text-align:center;padding:40px;color:var(--sub);">暂无品种,请先在「品种管理」中新建。</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# ═══════════════════ Tab: 品种管理 ═══════════════════ #}
|
||||||
|
<div class="tab-panel{% if tab == 'product' %} active{% endif %}" id="tab-product">
|
||||||
|
<div class="form-card" style="margin-bottom:24px;">
|
||||||
|
<div class="section-title">新建品种</div>
|
||||||
|
<form method="post" action="/admin/product" style="display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap;">
|
||||||
|
<div class="form-group" style="margin-bottom:0;flex:1;min-width:120px;">
|
||||||
|
<label>品种代码</label>
|
||||||
|
<input type="text" name="code" required placeholder="FG" maxlength="6" style="text-transform:uppercase;">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom:0;flex:1;min-width:120px;">
|
||||||
|
<label>品种名称</label>
|
||||||
|
<input type="text" name="name" required placeholder="玻璃">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom:0;flex:1;min-width:120px;">
|
||||||
|
<label>交易所</label>
|
||||||
|
<select name="exchange" required>
|
||||||
|
{% for ex in exchanges %}
|
||||||
|
<option value="{{ ex }}">{{ ex }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">新建</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if products %}
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table>
|
||||||
|
<tr><th>代码</th><th>名称</th><th>交易所</th><th>合约数</th><th>操作</th></tr>
|
||||||
|
{% for p in products %}
|
||||||
|
<tr>
|
||||||
|
<td><strong>{{ p.code }}</strong></td>
|
||||||
|
<td>{{ p.name }}</td>
|
||||||
|
<td>{{ p.exchange }}</td>
|
||||||
|
<td>{{ p.contracts|length }}</td>
|
||||||
|
<td>
|
||||||
|
<form method="post" action="/admin/product/{{ p.id }}/delete" onsubmit="return confirm('删除品种 {{ p.code }} 及其所有合约?')" style="display:inline;">
|
||||||
|
<button style="background:none;border:none;color:var(--danger);cursor:pointer;font-size:0.82rem;">删除</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div style="text-align:center;padding:40px;color:var(--sub);">暂无品种,请先新建。</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# ═══════════════════ Tab: 合约管理 ═══════════════════ #}
|
||||||
|
<div class="tab-panel{% if tab == 'contract' %} active{% endif %}" id="tab-contract">
|
||||||
|
{% if products %}
|
||||||
|
{# ── Add contract form ── #}
|
||||||
|
<div class="form-card" style="margin-bottom:24px;">
|
||||||
|
<div class="section-title">添加合约</div>
|
||||||
|
<form method="post" action="/admin/contract" style="display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap;">
|
||||||
|
<div class="form-group" style="margin-bottom:0;flex:1;min-width:140px;">
|
||||||
|
<label>品种</label>
|
||||||
|
<select name="product_id" required>
|
||||||
|
<option value="">-- 选择品种 --</option>
|
||||||
|
{% for p in products %}
|
||||||
|
<option value="{{ p.id }}">{{ p.code }} · {{ p.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom:0;flex:1;min-width:140px;">
|
||||||
|
<label>月份 <span style="font-weight:400;color:var(--sub);font-size:0.75rem;">如 2609</span></label>
|
||||||
|
<input type="text" name="month" required placeholder="2609" maxlength="4" style="text-transform:uppercase;">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">添加</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# ── Product tabs for contract list ── #}
|
||||||
|
{% set ctab = request.query_params.get('ctab', products[0].code if products else '') %}
|
||||||
|
<div class="tabs" style="margin-top:24px;">
|
||||||
|
{% for p in products %}
|
||||||
|
<a href="/admin/?tab=contract&ctab={{ p.code }}" class="tab-btn{% if ctab == p.code %} active{% endif %}">{{ p.code }} · {{ p.name }}</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% for p in products %}
|
||||||
|
{% if p.code == ctab and p.contracts %}
|
||||||
|
<div class="table-wrap" style="margin-bottom:20px;">
|
||||||
|
<table>
|
||||||
|
<tr><th>合约代码</th><th>月份</th><th>状态</th><th>操作</th></tr>
|
||||||
|
{% for c in p.contracts %}
|
||||||
|
<tr>
|
||||||
|
<td><strong>{{ c.code }}</strong></td>
|
||||||
|
<td>{{ c.name }}</td>
|
||||||
|
<td>
|
||||||
|
{% if c.is_active %}<span class="badge badge-up">启用</span>{% else %}<span class="badge badge-down">停用</span>{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<form method="post" action="/admin/contract/{{ c.id }}/toggle" style="display:inline;">
|
||||||
|
<button style="background:none;border:none;color:var(--sub);cursor:pointer;font-size:0.82rem;">
|
||||||
|
{% if c.is_active %}停用{% else %}启用{% endif %}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<form method="post" action="/admin/contract/{{ c.id }}/delete" style="display:inline;" onsubmit="return confirm('删除 {{ c.code }}?')">
|
||||||
|
<button style="background:none;border:none;color:var(--danger);cursor:pointer;font-size:0.82rem;">删除</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div style="text-align:center;padding:40px;color:var(--sub);">暂无品种,请先在「品种管理」中新建。</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function escapeHtml(s) {
|
||||||
|
var d = document.createElement('div');
|
||||||
|
d.appendChild(document.createTextNode(s));
|
||||||
|
return d.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
var syncTimer = null;
|
||||||
|
var progressShownAt = 0;
|
||||||
|
|
||||||
|
function showProgress() {
|
||||||
|
progressShownAt = Date.now();
|
||||||
|
document.getElementById('sync-progress').style.display = 'block';
|
||||||
|
document.getElementById('sync-progress-bar').style.width = '0%';
|
||||||
|
document.getElementById('sync-progress-pct').textContent = '';
|
||||||
|
document.getElementById('sync-progress-label').textContent = '正在同步...';
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideProgress() {
|
||||||
|
clearInterval(syncTimer);
|
||||||
|
syncTimer = null;
|
||||||
|
document.getElementById('sync-progress').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function pollProgress() {
|
||||||
|
fetch('/admin/sync-status')
|
||||||
|
.then(function(r) { return r.json(); })
|
||||||
|
.then(function(data) {
|
||||||
|
var pct = data.total > 0 ? Math.round(data.done / data.total * 100) : 0;
|
||||||
|
document.getElementById('sync-progress-bar').style.width = pct + '%';
|
||||||
|
document.getElementById('sync-progress-pct').textContent = pct + '%';
|
||||||
|
document.getElementById('sync-progress-label').textContent = data.label;
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
clearInterval(syncTimer);
|
||||||
|
syncTimer = null;
|
||||||
|
document.getElementById('sync-progress-bar').style.background = 'var(--danger)';
|
||||||
|
document.getElementById('sync-progress-label').innerHTML =
|
||||||
|
'<span style="color:var(--danger-fg);">✕ ' + escapeHtml(data.label) + '</span>' +
|
||||||
|
'<span style="display:block;font-size:0.78rem;color:var(--danger-fg);margin-top:4px;">' + escapeHtml(data.error) + '</span>';
|
||||||
|
document.getElementById('sync-progress-pct').textContent = '';
|
||||||
|
document.getElementById('sync-progress').insertAdjacentHTML('beforeend',
|
||||||
|
'<button onclick="hideProgress()" style="display:block;margin-top:10px;padding:4px 14px;border:1px solid var(--border);border-radius:5px;background:var(--surface);color:var(--fg);cursor:pointer;font-size:0.8rem;">关闭</button>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.finished) {
|
||||||
|
var elapsed = Date.now() - progressShownAt;
|
||||||
|
var delay = elapsed < 1000 ? 1000 - elapsed : 0;
|
||||||
|
setTimeout(function() {
|
||||||
|
clearInterval(syncTimer);
|
||||||
|
syncTimer = null;
|
||||||
|
document.getElementById('sync-progress-bar').style.width = '100%';
|
||||||
|
if (data.result > 0) {
|
||||||
|
document.getElementById('sync-progress-label').textContent = data.label + ' ✓ · +' + data.result;
|
||||||
|
} else if (data.total > 0) {
|
||||||
|
document.getElementById('sync-progress-label').textContent = data.label + ' ⚠ 无新数据,历史日期无可用持仓';
|
||||||
|
} else {
|
||||||
|
document.getElementById('sync-progress-label').textContent = data.label + ' ✓ 已是最新';
|
||||||
|
}
|
||||||
|
document.getElementById('sync-progress-pct').textContent = '';
|
||||||
|
document.getElementById('sync-progress').insertAdjacentHTML('beforeend',
|
||||||
|
'<button onclick="hideProgress()" style="display:block;margin-top:10px;padding:4px 14px;border:1px solid var(--border);border-radius:5px;background:var(--surface);color:var(--fg);cursor:pointer;font-size:0.8rem;">关闭</button>');
|
||||||
|
if (data.result > 0) {
|
||||||
|
var expanded = [];
|
||||||
|
document.querySelectorAll('.product-body').forEach(function(b, i) {
|
||||||
|
if (b.style.display === 'block') expanded.push(i);
|
||||||
|
});
|
||||||
|
try { sessionStorage.setItem('ft-expanded', JSON.stringify(expanded)); } catch(e) {}
|
||||||
|
setTimeout(function() { location.reload(); }, 1200);
|
||||||
|
}
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function startSync(url) {
|
||||||
|
if (syncTimer) { clearInterval(syncTimer); syncTimer = null; }
|
||||||
|
showProgress();
|
||||||
|
fetch(url, { method: 'POST' })
|
||||||
|
.then(function(r) { return r.json(); })
|
||||||
|
.then(function(data) {
|
||||||
|
if (!data.ok) {
|
||||||
|
// Another sync is already running — just show its progress
|
||||||
|
syncTimer = setInterval(pollProgress, 500);
|
||||||
|
pollProgress();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
syncTimer = setInterval(pollProgress, 500);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncBars(code) { startSync('/admin/sync/' + code + '/bg'); }
|
||||||
|
function syncPositions(code) { startSync('/admin/sync-positions/' + code + '/bg'); }
|
||||||
|
|
||||||
|
function switchTab(name) {
|
||||||
|
document.querySelectorAll('.tab-btn').forEach(function(b) { b.classList.remove('active'); });
|
||||||
|
document.querySelectorAll('.tab-panel').forEach(function(p) { p.classList.remove('active'); });
|
||||||
|
document.querySelector('.tab-btn[onclick="switchTab(\'' + name + '\')"]').classList.add('active');
|
||||||
|
document.getElementById('tab-' + name).classList.add('active');
|
||||||
|
var url = new URL(window.location);
|
||||||
|
url.searchParams.set('tab', name);
|
||||||
|
history.replaceState(null, '', url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleProduct(header) {
|
||||||
|
var body = header.nextElementSibling;
|
||||||
|
var arrow = header.querySelector('.collapse-arrow');
|
||||||
|
if (body.style.display === 'none') {
|
||||||
|
body.style.display = 'block';
|
||||||
|
arrow.style.transform = 'rotate(0deg)';
|
||||||
|
} else {
|
||||||
|
body.style.display = 'none';
|
||||||
|
arrow.style.transform = 'rotate(-90deg)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore expanded accordion state after reload
|
||||||
|
(function() {
|
||||||
|
try {
|
||||||
|
var expanded = JSON.parse(sessionStorage.getItem('ft-expanded') || '[]');
|
||||||
|
var bodies = document.querySelectorAll('.product-body');
|
||||||
|
expanded.forEach(function(i) {
|
||||||
|
if (bodies[i]) {
|
||||||
|
bodies[i].style.display = 'block';
|
||||||
|
var arrow = bodies[i].previousElementSibling.querySelector('.collapse-arrow');
|
||||||
|
if (arrow) arrow.style.transform = 'rotate(0deg)';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
sessionStorage.removeItem('ft-expanded');
|
||||||
|
} catch(e) {}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Auto-show progress bar if a sync is already running
|
||||||
|
(function() {
|
||||||
|
fetch('/admin/sync-status')
|
||||||
|
.then(function(r) { return r.json(); })
|
||||||
|
.then(function(data) {
|
||||||
|
if (data.running) {
|
||||||
|
showProgress();
|
||||||
|
syncTimer = setInterval(pollProgress, 500);
|
||||||
|
pollProgress();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{% block title %}期货量化{% endblock %}</title>
|
||||||
|
<style>
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg: #f8fafc; --surface: #fff; --fg: #1e293b; --sub: #64748b;
|
||||||
|
--border: #e2e8f0; --accent: #3b82f6; --accent-light: #eff6ff;
|
||||||
|
--th-bg: #f1f5f9; --th-fg: #475569;
|
||||||
|
--danger: #ef4444; --danger-bg: #fef2f2; --danger-fg: #dc2626;
|
||||||
|
--success: #10b981; --success-bg: #ecfdf5; --success-fg: #059669;
|
||||||
|
--warn: #f59e0b; --warn-bg: #fffbeb; --warn-fg: #d97706;
|
||||||
|
--na: #94a3b8;
|
||||||
|
--sidebar-w: 220px; --header-h: 56px;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--bg: #0f172a; --surface: #1e293b; --fg: #e2e8f0; --sub: #94a3b8;
|
||||||
|
--border: #334155; --accent: #60a5fa; --accent-light: #1e3a5f;
|
||||||
|
--th-bg: #1e293b; --th-fg: #cbd5e1;
|
||||||
|
--danger: #f87171; --danger-bg: #3b1515; --danger-fg: #fca5a5;
|
||||||
|
--success: #34d399; --success-bg: #0a1f17; --success-fg: #6ee7b7;
|
||||||
|
--warn: #fbbf24; --warn-bg: #2d2110; --warn-fg: #fcd34d;
|
||||||
|
--na: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, 'Segoe UI', system-ui, sans-serif;
|
||||||
|
color: var(--fg); background: var(--bg);
|
||||||
|
display: flex; min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Sidebar ── */
|
||||||
|
.sidebar {
|
||||||
|
width: var(--sidebar-w); min-width: var(--sidebar-w);
|
||||||
|
background: var(--surface); border-right: 1px solid var(--border);
|
||||||
|
display: flex; flex-direction: column; position: fixed;
|
||||||
|
top: 0; left: 0; bottom: 0; z-index: 50;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.sidebar-logo {
|
||||||
|
height: var(--header-h); display: flex; align-items: center;
|
||||||
|
padding: 0 20px; font-size: 1rem; font-weight: 700;
|
||||||
|
letter-spacing: 0.02em; border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.sidebar-nav { flex: 1; padding: 12px 0; }
|
||||||
|
.sidebar-nav a {
|
||||||
|
display: flex; align-items: center; gap: 10px;
|
||||||
|
padding: 10px 20px; color: var(--sub); text-decoration: none;
|
||||||
|
font-size: 0.9rem; transition: background .12s, color .12s;
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
}
|
||||||
|
.sidebar-nav a:hover { background: var(--accent-light); color: var(--fg); }
|
||||||
|
.sidebar-nav a.active {
|
||||||
|
color: var(--accent); background: var(--accent-light);
|
||||||
|
border-left-color: var(--accent); font-weight: 600;
|
||||||
|
}
|
||||||
|
.sidebar-nav .icon { font-size: 1.1rem; width: 22px; text-align: center; }
|
||||||
|
.nav-group .nav-arrow { font-size: 0.65rem; margin-left: auto; transition: transform .2s; color: var(--sub); }
|
||||||
|
.nav-group.open .nav-arrow { transform: rotate(-180deg); }
|
||||||
|
.nav-group .nav-children { display: none; }
|
||||||
|
.nav-group.open .nav-children { display: block; }
|
||||||
|
.nav-children a { padding-left: 48px !important; font-size: 0.84rem !important; }
|
||||||
|
.sidebar-footer {
|
||||||
|
padding: 16px 20px; border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.theme-btn {
|
||||||
|
width: 100%; padding: 8px; border: 1px solid var(--border); border-radius: 6px;
|
||||||
|
background: var(--surface); color: var(--fg); cursor: pointer;
|
||||||
|
font-size: 0.82rem; transition: background .12s;
|
||||||
|
}
|
||||||
|
.theme-btn:hover { background: var(--th-bg); }
|
||||||
|
|
||||||
|
/* ── Main ── */
|
||||||
|
.main {
|
||||||
|
margin-left: var(--sidebar-w); flex: 1;
|
||||||
|
display: flex; flex-direction: column; min-height: 100vh;
|
||||||
|
}
|
||||||
|
.main-header {
|
||||||
|
height: var(--header-h); min-height: var(--header-h);
|
||||||
|
padding: 0 28px; display: flex; align-items: center;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
.main-header h1 { font-size: 1.15rem; font-weight: 600; }
|
||||||
|
.breadcrumb { color: var(--sub); font-size: 0.82rem; margin-left: 12px; }
|
||||||
|
.breadcrumb a { color: var(--sub); text-decoration: none; }
|
||||||
|
.breadcrumb a:hover { color: var(--accent); }
|
||||||
|
|
||||||
|
.main-content { flex: 1; padding: 24px 28px; }
|
||||||
|
|
||||||
|
/* ── Components ── */
|
||||||
|
.stat-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; }
|
||||||
|
.stat-card {
|
||||||
|
background: var(--surface); border: 1px solid var(--border);
|
||||||
|
border-radius: 10px; padding: 18px 20px;
|
||||||
|
}
|
||||||
|
.stat-card .label { font-size: 0.78rem; color: var(--sub); margin-bottom: 6px; text-transform: uppercase; letter-spacing: 0.04em; }
|
||||||
|
.stat-card .value { font-size: 1.5rem; font-weight: 700; }
|
||||||
|
.stat-card .trend { font-size: 0.8rem; margin-top: 4px; }
|
||||||
|
.trend-up { color: var(--success-fg); }
|
||||||
|
.trend-down { color: var(--danger-fg); }
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 0.85rem; font-weight: 600; color: var(--sub);
|
||||||
|
text-transform: uppercase; letter-spacing: 0.04em;
|
||||||
|
margin-bottom: 12px; padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Table ── */
|
||||||
|
.table-wrap {
|
||||||
|
background: var(--surface); border: 1px solid var(--border);
|
||||||
|
border-radius: 10px; overflow: hidden; margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
table { border-collapse: collapse; width: 100%; font-size: 0.88rem; }
|
||||||
|
th {
|
||||||
|
background: var(--th-bg); color: var(--th-fg); font-weight: 600;
|
||||||
|
padding: 10px 14px; border-bottom: 2px solid var(--border);
|
||||||
|
text-align: center; white-space: nowrap; font-size: 0.8rem;
|
||||||
|
text-transform: uppercase; letter-spacing: 0.03em;
|
||||||
|
}
|
||||||
|
td { padding: 9px 14px; border-bottom: 1px solid var(--border); text-align: center; }
|
||||||
|
tr:last-child td { border-bottom: none; }
|
||||||
|
tr:hover td { background: var(--accent-light); }
|
||||||
|
td:first-child { font-weight: 500; }
|
||||||
|
tr.active td { background: #dbeafe !important; }
|
||||||
|
[data-theme="dark"] tr.active td { background: #1e3a5f !important; }
|
||||||
|
.na { color: var(--na); }
|
||||||
|
|
||||||
|
/* ── Badges ── */
|
||||||
|
.badge { display: inline-block; padding: 2px 10px; border-radius: 20px; font-size: 0.78rem; font-weight: 600; }
|
||||||
|
.badge-up { background: var(--danger-bg); color: var(--danger-fg); }
|
||||||
|
.badge-down { background: var(--success-bg); color: var(--success-fg); }
|
||||||
|
.badge-warn { background: var(--warn-bg); color: var(--warn-fg); }
|
||||||
|
|
||||||
|
/* ── Forms ── */
|
||||||
|
.form-card {
|
||||||
|
background: var(--surface); border: 1px solid var(--border);
|
||||||
|
border-radius: 10px; padding: 24px; max-width: 520px;
|
||||||
|
}
|
||||||
|
.form-group { margin-bottom: 16px; }
|
||||||
|
.form-group label {
|
||||||
|
display: block; font-size: 0.8rem; font-weight: 600;
|
||||||
|
color: var(--sub); margin-bottom: 5px;
|
||||||
|
text-transform: uppercase; letter-spacing: 0.03em;
|
||||||
|
}
|
||||||
|
.form-group input, .form-group select {
|
||||||
|
width: 100%; padding: 9px 12px; border: 1px solid var(--border);
|
||||||
|
border-radius: 6px; font-size: 0.9rem;
|
||||||
|
background: var(--bg); color: var(--fg);
|
||||||
|
transition: border-color .15s;
|
||||||
|
}
|
||||||
|
.form-group input:focus, .form-group select:focus {
|
||||||
|
outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-light);
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
padding: 9px 22px; border: none; border-radius: 6px;
|
||||||
|
font-size: 0.88rem; font-weight: 600; cursor: pointer;
|
||||||
|
transition: background .12s;
|
||||||
|
}
|
||||||
|
.btn-primary { background: var(--accent); color: #fff; }
|
||||||
|
.btn-primary:hover { opacity: 0.9; }
|
||||||
|
|
||||||
|
/* ── Drawer ── */
|
||||||
|
.drawer-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.35); z-index: 99; display: none; }
|
||||||
|
.drawer-overlay.show { display: block; }
|
||||||
|
.drawer { position: fixed; top: 0; right: 0; width: 360px; height: 100%; background: var(--surface); border-left: 1px solid var(--border); z-index: 100; padding: 28px 24px; transform: translateX(100%); transition: transform .25s ease; overflow-y: auto; box-shadow: -4px 0 24px rgba(0,0,0,.12); }
|
||||||
|
.drawer.show { transform: translateX(0); }
|
||||||
|
.drawer h3 { font-size: 1rem; margin-bottom: 4px; }
|
||||||
|
.drawer .date-label { color: var(--sub); font-size: 0.82rem; margin-bottom: 18px; }
|
||||||
|
.drawer .calc-table { width: 100%; border-collapse: collapse; font-size: 0.82rem; margin-bottom: 16px; }
|
||||||
|
.drawer .calc-table td { padding: 5px 10px; border: 1px solid var(--border); text-align: center; }
|
||||||
|
.drawer .calc-table tr:first-child td { background: var(--th-bg); font-weight: 600; color: var(--th-fg); }
|
||||||
|
.drawer .result { font-size: 0.85rem; line-height: 2; color: var(--fg); }
|
||||||
|
.drawer .result b { font-size: 1.15rem; }
|
||||||
|
.drawer .close-btn { position: absolute; top: 16px; right: 20px; background: none; border: none; font-size: 1.4rem; cursor: pointer; color: var(--sub); line-height: 1; }
|
||||||
|
.drawer .close-btn:hover { color: var(--fg); }
|
||||||
|
.amp-cell { cursor: pointer; color: var(--accent); font-weight: 600; }
|
||||||
|
.amp-cell:hover { text-decoration: underline; }
|
||||||
|
|
||||||
|
input[type="date"]::-webkit-calendar-picker-indicator {
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23475669' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'/%3E%3Cline x1='16' y1='2' x2='16' y2='6'/%3E%3Cline x1='8' y1='2' x2='8' y2='6'/%3E%3Cline x1='3' y1='10' x2='21' y2='10'/%3E%3C/svg%3E");
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] input[type="date"]::-webkit-calendar-picker-indicator {
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'/%3E%3Cline x1='16' y1='2' x2='16' y2='6'/%3E%3Cline x1='8' y1='2' x2='8' y2='6'/%3E%3Cline x1='3' y1='10' x2='21' y2='10'/%3E%3C/svg%3E");
|
||||||
|
filter: none;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] input[type="date"] {
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div class="sidebar-logo">📊 期货量化</div>
|
||||||
|
<nav class="sidebar-nav">
|
||||||
|
<a href="/contracts/" class="{% if active_nav == 'contracts' %}active{% endif %}">
|
||||||
|
<span class="icon">📈</span> 行情数据
|
||||||
|
</a>
|
||||||
|
<a href="/positions/" class="{% if active_nav == 'positions' %}active{% endif %}">
|
||||||
|
<span class="icon">📐</span> 对冲管理
|
||||||
|
</a>
|
||||||
|
<a href="/trades/" class="{% if active_nav == 'trades' %}active{% endif %}">
|
||||||
|
<span class="icon">📝</span> 交易记录
|
||||||
|
</a>
|
||||||
|
<a href="/options/" class="{% if active_nav == 'options' %}active{% endif %}">
|
||||||
|
<span class="icon">📊</span> 期权交易
|
||||||
|
</a>
|
||||||
|
<a href="/admin/" class="{% if active_nav == 'admin' %}active{% endif %}">
|
||||||
|
<span class="icon">⚙️</span> 系统管理
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<div style="font-size:0.78rem;color:var(--sub);margin-bottom:8px;">
|
||||||
|
👤 {{ request.state.user.username if request.state.user else '—' }}
|
||||||
|
</div>
|
||||||
|
<a href="/auth/logout" style="font-size:0.78rem;color:var(--sub);text-decoration:none;">退出登录</a>
|
||||||
|
<button class="theme-btn" style="margin-top:8px;" id="themeToggle">🌙 暗色模式</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div class="main">
|
||||||
|
<header class="main-header">
|
||||||
|
<h1>{% block heading %}概览{% endblock %}</h1>
|
||||||
|
<span class="breadcrumb">{% block breadcrumb %}{% endblock %}</span>
|
||||||
|
</header>
|
||||||
|
<div class="main-content">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-overlay" id="overlay" onclick="closeDrawer()"></div>
|
||||||
|
<div class="drawer" id="drawer">
|
||||||
|
<button class="close-btn" onclick="closeDrawer()">×</button>
|
||||||
|
<h3 id="drawer-title"></h3>
|
||||||
|
<p class="date-label" id="drawer-date"></p>
|
||||||
|
<table class="calc-table" id="drawer-table"></table>
|
||||||
|
<div class="result" id="drawer-result"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var themeBtn = document.getElementById('themeToggle');
|
||||||
|
themeBtn.addEventListener('click', toggleTheme);
|
||||||
|
function setTheme(dark) {
|
||||||
|
if (dark) {
|
||||||
|
document.documentElement.setAttribute('data-theme', 'dark');
|
||||||
|
document.documentElement.style.colorScheme = 'dark';
|
||||||
|
themeBtn.textContent = '☀️ 亮色模式';
|
||||||
|
} else {
|
||||||
|
document.documentElement.removeAttribute('data-theme');
|
||||||
|
document.documentElement.style.colorScheme = 'light';
|
||||||
|
themeBtn.textContent = '🌙 暗色模式';
|
||||||
|
}
|
||||||
|
try { localStorage.setItem('ft-theme', dark ? 'dark' : 'light'); } catch(e) {}
|
||||||
|
}
|
||||||
|
function toggleTheme() {
|
||||||
|
setTheme(document.documentElement.getAttribute('data-theme') !== 'dark');
|
||||||
|
}
|
||||||
|
function toggleNavGroup(e, id) {
|
||||||
|
e.preventDefault();
|
||||||
|
document.getElementById(id).classList.toggle('open');
|
||||||
|
}
|
||||||
|
(function() {
|
||||||
|
try {
|
||||||
|
var s = localStorage.getItem('ft-theme');
|
||||||
|
if (s === 'dark' || (!s && matchMedia('(prefers-color-scheme: dark)').matches)) setTheme(true);
|
||||||
|
} catch(e) {}
|
||||||
|
})();
|
||||||
|
|
||||||
|
var activeRow = null;
|
||||||
|
function closeDrawer() {
|
||||||
|
document.getElementById('overlay').classList.remove('show');
|
||||||
|
document.getElementById('drawer').classList.remove('show');
|
||||||
|
if (activeRow) { activeRow.classList.remove('active'); activeRow = null; }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}{{ contract }}{% endblock %}
|
||||||
|
{% block heading %}{{ contract }}{% endblock %}
|
||||||
|
{% block breadcrumb %}<a href="/contracts/">行情数据</a> / {{ contract }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="section-title">日线数据</div>
|
||||||
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;">
|
||||||
|
<p style="font-size:0.78rem;color:var(--sub);margin:0;">
|
||||||
|
振幅 = 近 5 日 (最高−最低) 均值取整 · 点击振幅值查看计算过程
|
||||||
|
</p>
|
||||||
|
<span style="font-size:0.78rem;color:var(--sub);">共 {{ total_rows }} 条</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if page == 1 and next_date %}
|
||||||
|
<div style="background:var(--accent-light);border:1px solid var(--accent);border-radius:8px;padding:12px 18px;margin-bottom:14px;display:flex;align-items:center;gap:16px;">
|
||||||
|
<span style="font-size:0.78rem;color:var(--sub);">📅 次日预测</span>
|
||||||
|
<span style="font-weight:600;">{{ next_date }} {{ next_weekday }}</span>
|
||||||
|
<span style="color:var(--sub);">预计振幅</span>
|
||||||
|
<span class="amp-cell" style="color:var(--accent);font-weight:700;font-size:1.1rem;cursor:pointer;" data-next="1">{{ next_amp }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table id="bars-table">
|
||||||
|
<tr><th>日期</th><th>星期</th><th>开盘</th><th>收盘</th><th>最高</th><th>最低</th><th>波幅</th><th>5日均振幅</th></tr>
|
||||||
|
{% for row in rows %}
|
||||||
|
<tr data-index="{{ row.global_idx }}" data-diff="{{ row.diff }}" data-date="{{ row.date }}" data-weekday="{{ row.weekday }}">
|
||||||
|
<td>{{ row.date }}</td>
|
||||||
|
<td>{{ row.weekday }}</td>
|
||||||
|
<td>{{ row.open }}</td>
|
||||||
|
<td>{{ row.close }}</td>
|
||||||
|
<td>{{ row.high }}</td>
|
||||||
|
<td>{{ row.low }}</td>
|
||||||
|
<td>{{ row.diff }}</td>
|
||||||
|
<td>
|
||||||
|
{% if row.has_amp %}
|
||||||
|
<span class="amp-cell">{{ row.amp_5d }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="na">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if total_pages > 1 %}
|
||||||
|
<div style="display:flex;align-items:center;justify-content:center;gap:6px;margin-bottom:24px;">
|
||||||
|
{% if page > 1 %}
|
||||||
|
<a class="btn" href="?page={{ page - 1 }}{% if pos_date %}&pos_date={{ pos_date }}{% endif %}" style="font-size:0.82rem;padding:5px 14px;background:var(--surface);color:var(--fg);border:1px solid var(--border);border-radius:5px;text-decoration:none;">← 上一页</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for p in range(1, total_pages + 1) %}
|
||||||
|
{% if p == page %}
|
||||||
|
<span style="font-size:0.82rem;padding:5px 12px;background:var(--accent);color:#fff;border-radius:5px;font-weight:600;">{{ p }}</span>
|
||||||
|
{% elif p <= 3 or p > total_pages - 3 or (p >= page - 1 and p <= page + 1) %}
|
||||||
|
<a class="btn" href="?page={{ p }}{% if pos_date %}&pos_date={{ pos_date }}{% endif %}" style="font-size:0.82rem;padding:5px 12px;background:var(--surface);color:var(--fg);border:1px solid var(--border);border-radius:5px;text-decoration:none;">{{ p }}</a>
|
||||||
|
{% elif p == 4 or p == total_pages - 3 %}
|
||||||
|
<span style="color:var(--sub);padding:5px 4px;">…</span>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if page < total_pages %}
|
||||||
|
<a class="btn" href="?page={{ page + 1 }}{% if pos_date %}&pos_date={{ pos_date }}{% endif %}" style="font-size:0.82rem;padding:5px 14px;background:var(--surface);color:var(--fg);border:1px solid var(--border);border-radius:5px;text-decoration:none;">下一页 →</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if pos_dates %}
|
||||||
|
<div class="section-title" style="margin-top:28px;display:flex;align-items:center;gap:12px;">
|
||||||
|
<span>持仓排名 · 前20</span>
|
||||||
|
<input type="date" id="pos-date-select" value="{{ selected_pos_date }}" min="{{ pos_dates[-1] }}" max="{{ pos_dates[0] }}" onchange="changePosDate(this.value)" style="font-size:0.82rem;padding:4px 10px;border:1px solid var(--border);border-radius:5px;background:var(--surface);color:var(--fg);cursor:pointer;">
|
||||||
|
<button onclick="openAnalysisDrawer()" style="margin-left:auto;padding:6px 16px;border:1px solid var(--accent);border-radius:5px;background:var(--accent-light);color:var(--accent);cursor:pointer;font-size:0.82rem;font-weight:600;white-space:nowrap;">AI 分析</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-bottom:24px;">
|
||||||
|
|
||||||
|
{% set types = [('volume', '成交量'), ('long', '多单持仓'), ('short', '空单持仓')] %}
|
||||||
|
{% for dtype, dlabel in types %}
|
||||||
|
<div class="table-wrap" style="margin-bottom:0;">
|
||||||
|
<table style="font-size:0.82rem;">
|
||||||
|
<tr><th colspan="4" style="font-size:0.8rem;color:var(--accent);">{{ dlabel }}</th></tr>
|
||||||
|
<tr><th>#</th><th>会员</th><th>持仓</th><th>增减</th></tr>
|
||||||
|
{% for row in pos_data[dtype] %}
|
||||||
|
<tr>
|
||||||
|
<td style="color:var(--sub);">{{ row.rank }}</td>
|
||||||
|
<td style="text-align:left;">{{ row.institution }}</td>
|
||||||
|
<td>{{ row.value }}</td>
|
||||||
|
<td style="color:{% if row.change > 0 %}var(--danger-fg){% elif row.change < 0 %}var(--success-fg){% else %}var(--sub){% endif %};">
|
||||||
|
{% if row.change > 0 %}+{% endif %}{{ row.change }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% if dtype != 'volume' %}
|
||||||
|
<tr style="background:var(--th-bg);font-weight:600;">
|
||||||
|
<td></td><td style="text-align:right;">增减合计</td>
|
||||||
|
<td></td>
|
||||||
|
<td style="color:{% if pos_totals[dtype] > 0 %}var(--danger-fg){% elif pos_totals[dtype] < 0 %}var(--success-fg){% else %}var(--sub){% endif %};">
|
||||||
|
{% if pos_totals[dtype] > 0 %}+{% endif %}{{ pos_totals[dtype] }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div id="analysisOverlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:99;" onclick="closeAnalysisDrawer()"></div>
|
||||||
|
<div id="analysisDrawer" style="display:none;position:fixed;top:0;right:0;width:700px;max-width:95vw;height:100%;background:var(--surface);border-left:1px solid var(--border);z-index:100;box-shadow:-4px 0 24px rgba(0,0,0,.12);transform:translateX(100%);transition:transform .25s ease;display:flex;flex-direction:column;">
|
||||||
|
<div style="display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid var(--border);">
|
||||||
|
<h3 style="margin:0;font-size:1rem;">持仓分析 {{ contract }}</h3>
|
||||||
|
<div style="display:flex;gap:8px;">
|
||||||
|
<button onclick="copyPrompt()" style="padding:4px 12px;border:1px solid var(--border);border-radius:4px;background:var(--surface);color:var(--fg);cursor:pointer;font-size:0.78rem;">复制</button>
|
||||||
|
<button onclick="closeAnalysisDrawer()" style="background:none;border:none;font-size:1.3rem;cursor:pointer;color:var(--sub);line-height:1;">×</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<pre id="analysisContent" style="flex:1;margin:0;padding:16px 20px;overflow-y:auto;font-size:0.78rem;line-height:1.6;white-space:pre-wrap;word-break:break-all;color:var(--fg);background:var(--bg);">加载中...</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function changePosDate(d) {
|
||||||
|
var url = new URL(window.location);
|
||||||
|
url.searchParams.set('pos_date', d);
|
||||||
|
window.location = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('.amp-cell').forEach(function(cell) {
|
||||||
|
cell.addEventListener('click', function() {
|
||||||
|
// Next-day prediction row
|
||||||
|
if (cell.dataset.next) {
|
||||||
|
var dataRows = document.querySelectorAll('#bars-table tr[data-index]');
|
||||||
|
if (dataRows.length < 5) return;
|
||||||
|
var rows = Array.from(dataRows).slice(0, 5);
|
||||||
|
showDrawer(cell, '{{ contract }}', rows, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect 5 older rows via DOM navigation (next siblings in the table)
|
||||||
|
var row = cell.parentElement.parentElement;
|
||||||
|
var olderRows = [];
|
||||||
|
var cursor = row;
|
||||||
|
for (var j = 0; j < 5; j++) {
|
||||||
|
cursor = cursor.nextElementSibling;
|
||||||
|
if (!cursor || !cursor.dataset.index) return;
|
||||||
|
olderRows.push(cursor);
|
||||||
|
}
|
||||||
|
showDrawer(cell, '{{ contract }}', olderRows, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function showDrawer(cell, product, calcRows, isNext) {
|
||||||
|
document.getElementById('drawer-title').textContent = product;
|
||||||
|
var label = isNext ? '次日预测' : '目标';
|
||||||
|
document.getElementById('drawer-date').textContent = label + ' 振幅 ' + cell.textContent.trim();
|
||||||
|
|
||||||
|
var html = '<tr><td>日期</td><td>最高−最低</td></tr>';
|
||||||
|
var sum = 0;
|
||||||
|
calcRows.forEach(function(r) {
|
||||||
|
html += '<tr><td>' + r.dataset.date + ' ' + r.dataset.weekday + '</td><td>' + r.dataset.diff + '</td></tr>';
|
||||||
|
sum += parseInt(r.dataset.diff);
|
||||||
|
});
|
||||||
|
document.getElementById('drawer-table').innerHTML = html;
|
||||||
|
document.getElementById('drawer-result').innerHTML = '合计 <b>' + sum + '</b> ÷ 5 = <b>' + (sum / 5).toFixed(1) + '</b><br>四舍五入 → <b>' + Math.round(sum / 5) + '</b>';
|
||||||
|
|
||||||
|
document.getElementById('overlay').classList.add('show');
|
||||||
|
document.getElementById('drawer').classList.add('show');
|
||||||
|
|
||||||
|
if (!isNext) {
|
||||||
|
var row = cell.parentElement.parentElement;
|
||||||
|
if (activeRow) activeRow.classList.remove('active');
|
||||||
|
row.classList.add('active');
|
||||||
|
activeRow = row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var promptText = '';
|
||||||
|
function openAnalysisDrawer() {
|
||||||
|
var overlay = document.getElementById('analysisOverlay');
|
||||||
|
var drawer = document.getElementById('analysisDrawer');
|
||||||
|
drawer.style.display = 'flex';
|
||||||
|
overlay.style.display = 'block';
|
||||||
|
requestAnimationFrame(function() {
|
||||||
|
drawer.style.transform = 'translateX(0)';
|
||||||
|
});
|
||||||
|
var content = document.getElementById('analysisContent');
|
||||||
|
if (promptText) {
|
||||||
|
content.textContent = promptText;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
content.textContent = '加载中...';
|
||||||
|
fetch('/contracts/{{ contract }}/analysis-prompt')
|
||||||
|
.then(function(r) { return r.text(); })
|
||||||
|
.then(function(t) {
|
||||||
|
promptText = t;
|
||||||
|
content.textContent = t;
|
||||||
|
})
|
||||||
|
.catch(function() { content.textContent = '加载失败'; });
|
||||||
|
}
|
||||||
|
function closeAnalysisDrawer() {
|
||||||
|
document.getElementById('analysisOverlay').style.display = 'none';
|
||||||
|
document.getElementById('analysisDrawer').style.transform = 'translateX(100%)';
|
||||||
|
}
|
||||||
|
function copyPrompt() {
|
||||||
|
navigator.clipboard.writeText(promptText).then(function() {
|
||||||
|
showToast('已复制到剪贴板');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function showToast(msg) {
|
||||||
|
var m = document.getElementById('toastMask');
|
||||||
|
if (!m) {
|
||||||
|
m = document.createElement('div');
|
||||||
|
m.id = 'toastMask';
|
||||||
|
m.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.3);z-index:999;display:flex;align-items:center;justify-content:center;transition:opacity .25s;opacity:0;';
|
||||||
|
m.onclick = function() { hideToast(); };
|
||||||
|
var b = document.createElement('div');
|
||||||
|
b.id = 'toastBox';
|
||||||
|
b.style.cssText = 'background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:32px 40px;text-align:center;box-shadow:0 8px 32px rgba(0,0,0,.18);';
|
||||||
|
m.appendChild(b);
|
||||||
|
document.body.appendChild(m);
|
||||||
|
}
|
||||||
|
var box = document.getElementById('toastBox');
|
||||||
|
box.innerHTML = '<div style="font-size:2rem;margin-bottom:12px;">✅</div><p style="font-size:1rem;font-weight:600;color:var(--fg);">' + msg + '</p>';
|
||||||
|
m.style.display = 'flex';
|
||||||
|
requestAnimationFrame(function() { m.style.opacity = '1'; });
|
||||||
|
clearTimeout(m._tid);
|
||||||
|
m._tid = setTimeout(hideToast, 1800);
|
||||||
|
}
|
||||||
|
function hideToast() {
|
||||||
|
var m = document.getElementById('toastMask');
|
||||||
|
if (!m) return;
|
||||||
|
m.style.opacity = '0';
|
||||||
|
setTimeout(function() { m.style.display = 'none'; }, 250);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}行情数据{% endblock %}
|
||||||
|
{% block heading %}行情数据{% endblock %}
|
||||||
|
{% block breadcrumb %}<a href="/contracts/">合约总览</a>{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
{% set active_product = request.query_params.get('product', product_groups[0].code if product_groups else '') %}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.tabs { display: flex; gap: 0; margin-bottom: 24px; border-bottom: 2px solid var(--border); }
|
||||||
|
.tab-btn {
|
||||||
|
padding: 10px 20px; border: none; background: none;
|
||||||
|
font-size: 0.88rem; font-weight: 500; color: var(--sub);
|
||||||
|
cursor: pointer; text-decoration: none; border-bottom: 2px solid transparent;
|
||||||
|
margin-bottom: -2px; transition: color .12s, border-color .12s;
|
||||||
|
}
|
||||||
|
.tab-btn:hover { color: var(--fg); }
|
||||||
|
.tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="tabs">
|
||||||
|
{% for pg in product_groups %}
|
||||||
|
<a href="/contracts/?product={{ pg.code }}" class="tab-btn{% if active_product == pg.code %} active{% endif %}">
|
||||||
|
{{ pg.code }} · {{ pg.name }}
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% for pg in product_groups %}
|
||||||
|
{% if pg.code == active_product %}
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table>
|
||||||
|
<tr><th>合约</th><th>日期</th><th>开盘</th><th>收盘</th><th>最高</th><th>最低</th><th>振幅</th><th>次日振幅</th><th>操作</th></tr>
|
||||||
|
{% for c in pg.contracts %}
|
||||||
|
<tr>
|
||||||
|
<td><strong>{{ c }}</strong></td>
|
||||||
|
{% if c in contract_bars %}
|
||||||
|
<td>{{ contract_bars[c].date }}</td>
|
||||||
|
<td>{{ contract_bars[c].open|int }}</td>
|
||||||
|
<td>{{ contract_bars[c].close|int }}</td>
|
||||||
|
<td>{{ contract_bars[c].high|int }}</td>
|
||||||
|
<td>{{ contract_bars[c].low|int }}</td>
|
||||||
|
<td>
|
||||||
|
{% if contract_bars[c].amp_5d %}
|
||||||
|
<span class="badge badge-up">{{ contract_bars[c].amp_5d|int }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="na">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if c in contract_next_amp %}
|
||||||
|
<span class="badge badge-warn">{{ contract_next_amp[c] }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="na">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
{% else %}
|
||||||
|
<td colspan="7" class="na">暂无数据</td>
|
||||||
|
{% endif %}
|
||||||
|
<td><a href="/contracts/{{ c }}" style="color:var(--accent);text-decoration:none;">详情 →</a></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>登录 · 期货量化</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #f8fafc; --surface: #fff; --fg: #1e293b; --sub: #64748b;
|
||||||
|
--border: #e2e8f0; --accent: #3b82f6; --danger: #ef4444;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--bg: #0f172a; --surface: #1e293b; --fg: #e2e8f0; --sub: #94a3b8;
|
||||||
|
--border: #334155; --accent: #60a5fa; --danger: #f87171;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, 'Segoe UI', system-ui, sans-serif;
|
||||||
|
background: var(--bg); color: var(--fg);
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
.login-box {
|
||||||
|
background: var(--surface); border: 1px solid var(--border);
|
||||||
|
border-radius: 12px; padding: 40px; width: 360px; max-width: 90vw;
|
||||||
|
}
|
||||||
|
.login-box h1 { font-size: 1.3rem; text-align: center; margin-bottom: 8px; }
|
||||||
|
.login-box .sub { color: var(--sub); font-size: 0.85rem; text-align: center; margin-bottom: 28px; }
|
||||||
|
.form-group { margin-bottom: 16px; }
|
||||||
|
.form-group label { display: block; font-size: 0.8rem; font-weight: 600; color: var(--sub); margin-bottom: 5px; }
|
||||||
|
.form-group input {
|
||||||
|
width: 100%; padding: 10px 12px; border: 1px solid var(--border);
|
||||||
|
border-radius: 6px; font-size: 0.9rem; background: var(--bg); color: var(--fg);
|
||||||
|
}
|
||||||
|
.form-group input:focus { outline: none; border-color: var(--accent); }
|
||||||
|
.btn {
|
||||||
|
width: 100%; padding: 10px; border: none; border-radius: 6px;
|
||||||
|
font-size: 0.9rem; font-weight: 600; cursor: pointer;
|
||||||
|
background: var(--accent); color: #fff; margin-top: 8px;
|
||||||
|
}
|
||||||
|
.btn:hover { opacity: 0.9; }
|
||||||
|
.error {
|
||||||
|
background: #fef2f2; color: var(--danger); padding: 10px 14px;
|
||||||
|
border-radius: 6px; font-size: 0.85rem; margin-bottom: 16px; text-align: center;
|
||||||
|
}
|
||||||
|
.toggle-btn {
|
||||||
|
position: fixed; top: 16px; right: 24px;
|
||||||
|
background: var(--surface); border: 1px solid var(--border);
|
||||||
|
color: var(--fg); padding: 6px 14px; border-radius: 6px;
|
||||||
|
cursor: pointer; font-size: 0.82rem; z-index: 10;
|
||||||
|
}
|
||||||
|
.toggle-btn:hover { opacity: 0.8; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<button class="toggle-btn" id="themeToggle">🌙 暗色模式</button>
|
||||||
|
<div class="login-box">
|
||||||
|
<h1>📊 期货量化系统</h1>
|
||||||
|
<p class="sub">请输入账号密码</p>
|
||||||
|
|
||||||
|
{% if error %}
|
||||||
|
<div class="error">{{ error }}</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>用户名</label>
|
||||||
|
<input type="text" name="username" required autofocus>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>密码</label>
|
||||||
|
<input type="password" name="password" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn">登 录</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var themeBtn = document.getElementById('themeToggle');
|
||||||
|
themeBtn.addEventListener('click', toggleTheme);
|
||||||
|
function setTheme(dark) {
|
||||||
|
if (dark) {
|
||||||
|
document.documentElement.setAttribute('data-theme', 'dark');
|
||||||
|
document.documentElement.style.colorScheme = 'dark';
|
||||||
|
themeBtn.textContent = '☀️ 亮色模式';
|
||||||
|
} else {
|
||||||
|
document.documentElement.removeAttribute('data-theme');
|
||||||
|
document.documentElement.style.colorScheme = 'light';
|
||||||
|
themeBtn.textContent = '🌙 暗色模式';
|
||||||
|
}
|
||||||
|
try { localStorage.setItem('ft-theme', dark ? 'dark' : 'light'); } catch(e) {}
|
||||||
|
}
|
||||||
|
function toggleTheme() {
|
||||||
|
setTheme(document.documentElement.getAttribute('data-theme') !== 'dark');
|
||||||
|
}
|
||||||
|
(function() {
|
||||||
|
try {
|
||||||
|
var s = localStorage.getItem('ft-theme');
|
||||||
|
if (s === 'dark' || (!s && matchMedia('(prefers-color-scheme: dark)').matches)) setTheme(true);
|
||||||
|
} catch(e) {}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,415 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}期权交易{% endblock %}
|
||||||
|
{% block heading %}期权交易{% endblock %}
|
||||||
|
{% block breadcrumb %}期权开平仓记录{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
{% set view = request.query_params.get('view', '') %}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.tabs { display: flex; gap: 0; margin-bottom: 24px; border-bottom: 2px solid var(--border); }
|
||||||
|
.tab-btn {
|
||||||
|
padding: 10px 20px; border: none; background: none;
|
||||||
|
font-size: 0.88rem; font-weight: 500; color: var(--sub);
|
||||||
|
cursor: pointer; text-decoration: none; border-bottom: 2px solid transparent;
|
||||||
|
margin-bottom: -2px; transition: color .12s, border-color .12s;
|
||||||
|
}
|
||||||
|
.tab-btn:hover { color: var(--fg); }
|
||||||
|
.tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; }
|
||||||
|
.tab-panel { display: none; }
|
||||||
|
.tab-panel.active { display: block; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="tabs">
|
||||||
|
<a href="/options/" class="tab-btn{% if view != 'closed' %} active{% endif %}">📋 持仓</a>
|
||||||
|
<a href="/options/?view=closed" class="tab-btn{% if view == 'closed' %} active{% endif %}">📊 已平仓</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if view != 'closed' %}
|
||||||
|
<div class="tab-panel active">
|
||||||
|
|
||||||
|
{# ── 新建开仓 ── #}
|
||||||
|
<div class="section-title">新建开仓</div>
|
||||||
|
<div class="form-card" style="margin-bottom:28px;max-width:100%;">
|
||||||
|
<form method="post" action="/options/open">
|
||||||
|
<div style="display:flex;gap:10px;align-items:flex-end;flex-wrap:wrap;">
|
||||||
|
<div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:100px;">
|
||||||
|
<label>品种</label>
|
||||||
|
<select id="productSelect" required onchange="filterContracts()">
|
||||||
|
<option value="">--</option>
|
||||||
|
{% for pcode in product_contracts %}
|
||||||
|
<option value="{{ pcode }}">{{ pcode }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:120px;">
|
||||||
|
<label>合约</label>
|
||||||
|
<select name="contract_code" id="contractSelect" required disabled>
|
||||||
|
<option value="">-- 先选品种 --</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:70px;">
|
||||||
|
<label>类型</label>
|
||||||
|
<select name="option_type" required>
|
||||||
|
{% for v, label in option_types %}
|
||||||
|
<option value="{{ v }}">{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:60px;">
|
||||||
|
<label>买卖</label>
|
||||||
|
<select name="direction" required>
|
||||||
|
{% for v, label in option_directions %}
|
||||||
|
<option value="{{ v }}">{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:90px;">
|
||||||
|
<label>行权价</label>
|
||||||
|
<input type="number" step="any" name="strike_price" required placeholder="1300" style="font-size:0.9rem;">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:80px;">
|
||||||
|
<label>权利金</label>
|
||||||
|
<input type="number" step="any" name="open_price" required placeholder="25" style="font-size:0.9rem;">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:130px;">
|
||||||
|
<label>开仓日期</label>
|
||||||
|
<input type="date" name="open_date" value="{{ today }}" required style="font-size:0.85rem;">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:80px;">
|
||||||
|
<label>手续费</label>
|
||||||
|
<input type="number" step="any" name="open_fee" value="0" style="font-size:0.9rem;">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary" style="font-size:0.82rem;padding:8px 18px;">开仓</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# ── 持仓列表 ── #}
|
||||||
|
<div class="section-title">持仓中 · {{ open_trades|length }} 笔</div>
|
||||||
|
|
||||||
|
{% if open_trades %}
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table>
|
||||||
|
<tr><th>品种</th><th>合约</th><th>类型</th><th>买卖</th><th>行权价</th><th>权利金</th><th>开仓日期</th><th>手续费</th><th>操作</th></tr>
|
||||||
|
{% for t in open_trades %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ t.product_code }}</td>
|
||||||
|
<td><strong>{{ t.contract_code.replace(t.product_code, '', 1) }}</strong></td>
|
||||||
|
<td>
|
||||||
|
{% if t.option_type == 'C' %}
|
||||||
|
<span class="badge badge-up">C</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge badge-down">P</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if t.direction == 'sell' %}
|
||||||
|
<span class="badge badge-down">卖</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge badge-up">买</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ t.strike_price }}</td>
|
||||||
|
<td>{{ t.open_price }}</td>
|
||||||
|
<td>{{ t.open_date }} {{ weekdays[t.open_date.weekday()] }}</td>
|
||||||
|
<td>{{ t.open_fee or 0 }}</td>
|
||||||
|
<td>
|
||||||
|
<button style="background:var(--accent);color:#fff;border:none;padding:3px 10px;border-radius:4px;cursor:pointer;font-size:0.78rem;"
|
||||||
|
onclick="showCloseForm({{ t.id }})">平仓</button>
|
||||||
|
<button style="background:var(--success-bg);color:var(--success-fg);border:1px solid var(--success);padding:3px 10px;border-radius:4px;cursor:pointer;font-size:0.78rem;margin-left:8px;"
|
||||||
|
onclick="showEditForm('option', {{ t.id }}, '{{ t.product_code }}', '{{ t.contract_code }}', '{{ t.option_type }}', '{{ t.direction }}', {{ t.strike_price }}, {{ t.open_price }}, {{ t.open_fee or 0 }}, '{{ t.open_date }}', null, null, null, '{{ t.status }}')">编辑</button>
|
||||||
|
<form method="post" action="/options/{{ t.id }}/delete" style="display:inline;margin-left:8px;">
|
||||||
|
<button style="background:transparent;color:#dc2626;border:1px solid #ef4444;padding:3px 10px;border-radius:4px;cursor:pointer;font-size:0.78rem;" onclick="confirmDelete(event, '确定删除此记录?', this.closest('form').action)">删除</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div style="text-align:center;padding:40px;color:var(--sub);">暂无持仓</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{# ── 平仓弹窗 ── #}
|
||||||
|
<div id="closeFormOverlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:99;" onclick="hideCloseForm()"></div>
|
||||||
|
<div id="closeFormBox" style="display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:24px;z-index:100;width:360px;max-width:90vw;">
|
||||||
|
<h3 style="margin-bottom:16px;">平仓</h3>
|
||||||
|
<form method="post" id="closeForm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>平仓日期</label>
|
||||||
|
<input type="date" name="close_date" value="{{ today }}" required style="font-size:0.85rem;">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>平仓价格</label>
|
||||||
|
<input type="number" step="any" name="close_price" required style="font-size:0.9rem;">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>手续费</label>
|
||||||
|
<input type="number" step="any" name="close_fee" value="0" style="font-size:0.9rem;">
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:10px;justify-content:flex-end;">
|
||||||
|
<button type="button" class="btn" style="background:var(--th-bg);color:var(--fg);" onclick="hideCloseForm()">取消</button>
|
||||||
|
<button type="submit" class="btn btn-primary">确认平仓</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="tab-panel active">
|
||||||
|
{# ═══════════════ 已平仓 ═══════════════ #}
|
||||||
|
|
||||||
|
{% if closed_trades %}
|
||||||
|
<div class="section-title">已平仓 · {{ closed_trades|length }} 笔</div>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table>
|
||||||
|
<tr><th>品种</th><th>合约</th><th>类型</th><th>买卖</th><th>行权价</th><th>开仓</th><th>平仓</th><th>持仓</th><th>权利金</th><th>平仓价</th><th>开仓费</th><th>平仓费</th><th>盈亏</th><th>操作</th></tr>
|
||||||
|
{% for t in closed_trades %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ t.product_code }}</td>
|
||||||
|
<td><strong>{{ t.contract_code.replace(t.product_code, '', 1) }}</strong></td>
|
||||||
|
<td>
|
||||||
|
{% if t.option_type == 'C' %}
|
||||||
|
<span class="badge badge-up">C</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge badge-down">P</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if t.direction == 'sell' %}
|
||||||
|
<span class="badge badge-down">卖</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge badge-up">买</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ t.strike_price }}</td>
|
||||||
|
<td style="font-size:0.8rem;">{{ t.open_date }} {{ weekdays[t.open_date.weekday()] }}</td>
|
||||||
|
<td style="font-size:0.8rem;">{{ t.close_date }} {{ weekdays[t.close_date.weekday()] }}</td>
|
||||||
|
<td>{{ (t.close_date - t.open_date).days }}天</td>
|
||||||
|
<td>{{ t.open_price }}</td>
|
||||||
|
<td>{{ t.close_price }}</td>
|
||||||
|
<td>{{ t.open_fee or 0 }}</td>
|
||||||
|
<td>{{ t.close_fee or 0 }}</td>
|
||||||
|
<td>
|
||||||
|
{% set p = t.pnl %}
|
||||||
|
{% if p is not none %}
|
||||||
|
<span style="font-weight:700;{% if p > 0 %}color:var(--success-fg);{% elif p < 0 %}color:var(--danger-fg);{% else %}color:var(--sub);{% endif %}">
|
||||||
|
{% if p > 0 %}+{% endif %}{{ p }}
|
||||||
|
</span>
|
||||||
|
<span style="color:var(--sub);cursor:pointer;font-size:0.75rem;margin-left:4px;"
|
||||||
|
onclick="showPnlDrawer('{{ t.product_code }} {{ t.contract_code.replace(t.product_code, '', 1) }} {{ t.option_type }}', '{{ t.direction }}', {{ t.open_price }}, {{ t.close_price }}, {{ t.open_fee or 0 }}, {{ t.close_fee or 0 }}, {{ p }})" title="计算过程">ⓘ</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button style="background:var(--success-bg);color:var(--success-fg);border:1px solid var(--success);padding:3px 10px;border-radius:4px;cursor:pointer;font-size:0.78rem;margin-left:8px;"
|
||||||
|
onclick="showEditForm('option', {{ t.id }}, '{{ t.product_code }}', '{{ t.contract_code }}', '{{ t.option_type }}', '{{ t.direction }}', {{ t.strike_price }}, {{ t.open_price }}, {{ t.open_fee or 0 }}, '{{ t.open_date }}', {{ t.close_price }}, {{ t.close_fee or 0 }}, '{{ t.close_date }}', '{{ t.status }}')">编辑</button>
|
||||||
|
<form method="post" action="/options/{{ t.id }}/delete" style="display:inline;margin-left:8px;">
|
||||||
|
<button style="background:transparent;color:#dc2626;border:1px solid #ef4444;padding:3px 10px;border-radius:4px;cursor:pointer;font-size:0.78rem;" onclick="confirmDelete(event, '确定删除此记录?', this.closest('form').action)">删除</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% set ns = namespace(total_pnl=0, total_open_fee=0, total_close_fee=0) %}
|
||||||
|
{% for t in closed_trades %}
|
||||||
|
{% set ns.total_pnl = ns.total_pnl + (t.pnl or 0) %}
|
||||||
|
{% set ns.total_open_fee = ns.total_open_fee + (t.open_fee or 0) %}
|
||||||
|
{% set ns.total_close_fee = ns.total_close_fee + (t.close_fee or 0) %}
|
||||||
|
{% endfor %}
|
||||||
|
<div style="display:flex;gap:24px;margin-bottom:24px;padding:12px 18px;background:var(--surface);border:1px solid var(--border);border-radius:8px;font-size:0.88rem;flex-wrap:wrap;">
|
||||||
|
<span>盈亏合计 <b style="{% if ns.total_pnl > 0 %}color:var(--success-fg);{% elif ns.total_pnl < 0 %}color:var(--danger-fg);{% endif %}">{% if ns.total_pnl > 0 %}+{% endif %}{{ '%.2f'|format(ns.total_pnl) }}</b></span>
|
||||||
|
<span style="color:var(--sub);">|</span>
|
||||||
|
<span>开仓手续费 <b>{{ '%.2f'|format(ns.total_open_fee) }}</b></span>
|
||||||
|
<span style="color:var(--sub);">|</span>
|
||||||
|
<span>平仓手续费 <b>{{ '%.2f'|format(ns.total_close_fee) }}</b></span>
|
||||||
|
<span style="color:var(--sub);">|</span>
|
||||||
|
<span>手续费合计 <b>{{ '%.2f'|format(ns.total_open_fee + ns.total_close_fee) }}</b></span>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div style="text-align:center;padding:60px;color:var(--sub);">暂无已平仓记录</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if view != 'closed' %}
|
||||||
|
<script>
|
||||||
|
var productContracts = {{ product_contracts | tojson }};
|
||||||
|
function filterContracts() {
|
||||||
|
var prod = document.getElementById('productSelect').value;
|
||||||
|
var sel = document.getElementById('contractSelect');
|
||||||
|
sel.innerHTML = '<option value="">-- 选择合约 --</option>';
|
||||||
|
sel.disabled = !prod;
|
||||||
|
if (prod && productContracts[prod]) {
|
||||||
|
productContracts[prod].forEach(function(c) {
|
||||||
|
var display = c.startsWith(prod) ? c.substring(prod.length) : c;
|
||||||
|
sel.innerHTML += '<option value="' + c + '">' + display + '</option>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function showCloseForm(tradeId) {
|
||||||
|
document.getElementById('closeForm').action = '/options/' + tradeId + '/close';
|
||||||
|
document.getElementById('closeFormOverlay').style.display = 'block';
|
||||||
|
document.getElementById('closeFormBox').style.display = 'block';
|
||||||
|
}
|
||||||
|
function hideCloseForm() {
|
||||||
|
document.getElementById('closeFormOverlay').style.display = 'none';
|
||||||
|
document.getElementById('closeFormBox').style.display = 'none';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div id="editFormOverlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:98;" onclick="hideEditForm()"></div>
|
||||||
|
<div id="editFormBox" style="display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:24px;z-index:99;width:400px;max-width:90vw;max-height:90vh;overflow-y:auto;">
|
||||||
|
<h3 style="margin-bottom:16px;">编辑记录</h3>
|
||||||
|
<form method="post" id="editForm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>合约</label>
|
||||||
|
<input type="text" name="contract_code" id="editContract" required style="font-size:0.9rem;">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>类型</label>
|
||||||
|
<select name="option_type" id="editOptionType" required>
|
||||||
|
{% for v, label in option_types %}
|
||||||
|
<option value="{{ v }}">{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>买卖</label>
|
||||||
|
<select name="direction" id="editDirection" required>
|
||||||
|
{% for v, label in option_directions %}
|
||||||
|
<option value="{{ v }}">{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>行权价</label>
|
||||||
|
<input type="number" step="any" name="strike_price" id="editStrike" required style="font-size:0.9rem;">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>开仓日期</label>
|
||||||
|
<input type="date" name="open_date" id="editOpenDate" required style="font-size:0.85rem;">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>权利金</label>
|
||||||
|
<input type="number" step="any" name="open_price" id="editOpenPrice" required style="font-size:0.9rem;">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>手续费</label>
|
||||||
|
<input type="number" step="any" name="open_fee" id="editOpenFee" value="0" style="font-size:0.9rem;">
|
||||||
|
</div>
|
||||||
|
<div id="editCloseFields" style="display:none;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>平仓日期</label>
|
||||||
|
<input type="date" name="close_date" id="editCloseDate" style="font-size:0.85rem;">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>平仓价格</label>
|
||||||
|
<input type="number" step="any" name="close_price" id="editClosePrice" style="font-size:0.9rem;">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>手续费</label>
|
||||||
|
<input type="number" step="any" name="close_fee" id="editCloseFee" value="0" style="font-size:0.9rem;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:10px;justify-content:flex-end;">
|
||||||
|
<button type="button" class="btn" style="background:var(--th-bg);color:var(--fg);" onclick="hideEditForm()">取消</button>
|
||||||
|
<button type="submit" class="btn btn-primary">保存</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="pnlOverlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:99;" onclick="hidePnlDrawer()"></div>
|
||||||
|
<div id="pnlBox" style="display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:24px;z-index:100;width:380px;max-width:90vw;">
|
||||||
|
<h3 style="margin-bottom:4px;" id="pnlTitle"></h3>
|
||||||
|
<p style="font-size:0.8rem;color:var(--sub);margin-bottom:14px;" id="pnlFormula"></p>
|
||||||
|
<table class="calc-table" style="width:100%;border-collapse:collapse;font-size:0.84rem;margin-bottom:12px;" id="pnlTable"></table>
|
||||||
|
<div style="font-size:1rem;" id="pnlResult"></div>
|
||||||
|
<button style="position:absolute;top:12px;right:16px;background:none;border:none;font-size:1.3rem;cursor:pointer;color:var(--sub);" onclick="hidePnlDrawer()">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function showPnlDrawer(title, direction, openPrice, closePrice, openFee, closeFee, result) {
|
||||||
|
document.getElementById('pnlTitle').textContent = title;
|
||||||
|
var dirLabel = direction === 'sell' ? '卖' : '买';
|
||||||
|
document.getElementById('pnlFormula').textContent = '方向: ' + dirLabel + ' | 每点 20 元';
|
||||||
|
var diff = direction === 'sell' ? (openPrice - closePrice) : (closePrice - openPrice);
|
||||||
|
var gross = diff * 20;
|
||||||
|
var fees = openFee + closeFee;
|
||||||
|
var html = '<tr style="background:var(--th-bg);"><td>项目</td><td>计算</td><td style="text-align:right;">金额</td></tr>';
|
||||||
|
html += '<tr><td>价差</td><td>' + (direction === 'sell' ? openPrice + ' - ' + closePrice : closePrice + ' - ' + openPrice) + '</td><td style="text-align:right;">' + diff.toFixed(2) + ' 点</td></tr>';
|
||||||
|
html += '<tr><td>毛利</td><td>' + diff.toFixed(2) + ' × 20</td><td style="text-align:right;">' + gross.toFixed(2) + '</td></tr>';
|
||||||
|
html += '<tr><td>开仓手续费</td><td></td><td style="text-align:right;color:var(--danger-fg);">-' + openFee.toFixed(2) + '</td></tr>';
|
||||||
|
html += '<tr><td>平仓手续费</td><td></td><td style="text-align:right;color:var(--danger-fg);">-' + closeFee.toFixed(2) + '</td></tr>';
|
||||||
|
document.getElementById('pnlTable').innerHTML = html;
|
||||||
|
document.getElementById('pnlResult').innerHTML = '<b>盈亏 = ' + gross.toFixed(2) + ' - ' + openFee.toFixed(2) + ' - ' + closeFee.toFixed(2) + ' = <span style="color:' + (result > 0 ? 'var(--success-fg)' : result < 0 ? 'var(--danger-fg)' : 'var(--sub)') + ';">' + (result > 0 ? '+' : '') + result.toFixed(2) + '</span></b>';
|
||||||
|
document.getElementById('pnlOverlay').style.display = 'block';
|
||||||
|
document.getElementById('pnlBox').style.display = 'block';
|
||||||
|
}
|
||||||
|
function showEditForm(type, id, productCode, contractCode, optionType, direction, strike, openPrice, openFee, openDate, closePrice, closeFee, closeDate, status) {
|
||||||
|
document.getElementById('editForm').action = '/' + type + 's/' + id + '/edit';
|
||||||
|
document.getElementById('editContract').value = contractCode;
|
||||||
|
document.getElementById('editOptionType').value = optionType;
|
||||||
|
document.getElementById('editDirection').value = direction;
|
||||||
|
document.getElementById('editStrike').value = strike;
|
||||||
|
document.getElementById('editOpenDate').value = openDate;
|
||||||
|
document.getElementById('editOpenPrice').value = openPrice;
|
||||||
|
document.getElementById('editOpenFee').value = openFee || 0;
|
||||||
|
if (status === 'closed' && closeDate) {
|
||||||
|
document.getElementById('editCloseFields').style.display = 'block';
|
||||||
|
document.getElementById('editCloseDate').value = closeDate;
|
||||||
|
document.getElementById('editClosePrice').value = closePrice;
|
||||||
|
document.getElementById('editCloseFee').value = closeFee || 0;
|
||||||
|
} else {
|
||||||
|
document.getElementById('editCloseFields').style.display = 'none';
|
||||||
|
}
|
||||||
|
document.getElementById('editFormOverlay').style.display = 'block';
|
||||||
|
document.getElementById('editFormBox').style.display = 'block';
|
||||||
|
}
|
||||||
|
function hideEditForm() {
|
||||||
|
document.getElementById('editFormOverlay').style.display = 'none';
|
||||||
|
document.getElementById('editFormBox').style.display = 'none';
|
||||||
|
}
|
||||||
|
function hidePnlDrawer() {
|
||||||
|
document.getElementById('pnlOverlay').style.display = 'none';
|
||||||
|
document.getElementById('pnlBox').style.display = 'none';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div id="confirmOverlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:199;" onclick="hideConfirm()"></div>
|
||||||
|
<div id="confirmBox" style="display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:28px 32px;z-index:200;text-align:center;max-width:90vw;">
|
||||||
|
<p id="confirmMsg" style="font-size:0.95rem;margin-bottom:20px;"></p>
|
||||||
|
<div style="display:flex;gap:10px;justify-content:center;">
|
||||||
|
<button class="btn" style="background:var(--th-bg);color:var(--fg);" onclick="hideConfirm()">取消</button>
|
||||||
|
<button class="btn" style="background:var(--danger);color:#fff;" id="confirmBtn">确认删除</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function confirmDelete(e, msg, url) {
|
||||||
|
e.preventDefault();
|
||||||
|
document.getElementById('confirmMsg').textContent = msg;
|
||||||
|
document.getElementById('confirmBtn').onclick = function() {
|
||||||
|
var f = document.createElement('form');
|
||||||
|
f.method = 'POST';
|
||||||
|
f.action = url;
|
||||||
|
document.body.appendChild(f);
|
||||||
|
f.submit();
|
||||||
|
};
|
||||||
|
document.getElementById('confirmOverlay').style.display = 'block';
|
||||||
|
document.getElementById('confirmBox').style.display = 'block';
|
||||||
|
}
|
||||||
|
function hideConfirm() {
|
||||||
|
document.getElementById('confirmOverlay').style.display = 'none';
|
||||||
|
document.getElementById('confirmBox').style.display = 'none';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}对冲管理{% endblock %}
|
||||||
|
{% block heading %}对冲管理{% endblock %}
|
||||||
|
{% block breadcrumb %}振幅锁仓 · 仓位跟踪{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
{% set error = request.query_params.get('error', '') %}
|
||||||
|
|
||||||
|
{% if error %}
|
||||||
|
<div style="background:var(--danger-bg);color:var(--danger-fg);padding:10px 16px;border-radius:6px;margin-bottom:16px;font-size:0.88rem;">{{ error }}</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{# ═══════════════ 新建轮次 ═══════════════ #}
|
||||||
|
<div class="section-title">新建轮次</div>
|
||||||
|
<div class="form-card" style="margin-bottom:28px;">
|
||||||
|
<form method="post" action="/positions/round">
|
||||||
|
<div style="display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap;">
|
||||||
|
<div class="form-group" style="margin-bottom:0;flex:1;min-width:100px;">
|
||||||
|
<label>品种</label>
|
||||||
|
<select id="productSelect" required onchange="filterContracts()">
|
||||||
|
<option value="">--</option>
|
||||||
|
{% for pcode in product_contracts %}
|
||||||
|
<option value="{{ pcode }}">{{ pcode }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom:0;flex:1;min-width:120px;">
|
||||||
|
<label>合约</label>
|
||||||
|
<select name="contract_code" id="contractSelect" required disabled>
|
||||||
|
<option value="">-- 先选品种 --</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom:0;flex:1;min-width:100px;">
|
||||||
|
<label>每日手数 N</label>
|
||||||
|
<input type="number" name="daily_hands" value="1" min="1" max="10" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">开始新轮</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if not active_rounds %}
|
||||||
|
<div style="text-align:center;padding:60px;color:var(--sub);">
|
||||||
|
<p style="font-size:1.1rem;margin-bottom:8px;">暂无活跃轮次</p>
|
||||||
|
<p style="font-size:0.82rem;">创建一轮新的交易,开始跟踪仓位和锁仓状态</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for round in active_rounds %}
|
||||||
|
{# ═══════════════ 轮次卡片 ═══════════════ #}
|
||||||
|
{% set locks = round.total_locks %}
|
||||||
|
<div style="margin-bottom:32px;border:1px solid var(--border);border-radius:10px;overflow:hidden;">
|
||||||
|
|
||||||
|
{# ── 轮次头部 ── #}
|
||||||
|
<div style="display:flex;align-items:center;gap:16px;padding:14px 20px;background:var(--th-bg);flex-wrap:wrap;">
|
||||||
|
<strong style="font-size:1rem;">{{ round.contract_code }}</strong>
|
||||||
|
<span class="badge badge-up">{{ round.daily_hands }} 手/天</span>
|
||||||
|
<span style="font-size:0.82rem;font-weight:600;{% if locks >= round.max_locks %}color:var(--danger-fg);{% endif %}">
|
||||||
|
锁仓 {{ locks }}/{{ round.max_locks }}
|
||||||
|
{% if locks >= round.max_locks %} ⚠ 熔断{% endif %}
|
||||||
|
</span>
|
||||||
|
<span style="margin-left:auto;display:flex;gap:8px;">
|
||||||
|
<button style="background:var(--surface);color:var(--fg);border:1px solid var(--border);padding:3px 12px;border-radius:4px;cursor:pointer;font-size:0.78rem;"
|
||||||
|
onclick="showMaxLocksModal({{ round.id }}, {{ round.max_locks }})">修改锁仓数</button>
|
||||||
|
<form method="post" action="/positions/{{ round.id }}/close" style="display:inline;">
|
||||||
|
<input type="hidden" name="result" value="profit_taken">
|
||||||
|
<button class="btn" style="background:var(--success);color:#fff;font-size:0.78rem;padding:4px 14px;" onclick="confirmDelete(event, '确认 {{ round.contract_code }} 止盈清仓?', this.closest('form').action)">止盈</button>
|
||||||
|
</form>
|
||||||
|
<form method="post" action="/positions/{{ round.id }}/close" style="display:inline;">
|
||||||
|
<input type="hidden" name="result" value="meltdown">
|
||||||
|
<button class="btn" style="background:var(--danger);color:#fff;font-size:0.78rem;padding:4px 14px;" onclick="confirmDelete(event, '确认 {{ round.contract_code }} 熔断清仓?', this.closest('form').action)">熔断</button>
|
||||||
|
</form>
|
||||||
|
<form method="post" action="/positions/{{ round.id }}/delete" style="display:inline;">
|
||||||
|
<button style="background:transparent;color:#dc2626;border:1px solid #ef4444;padding:3px 10px;border-radius:4px;cursor:pointer;font-size:0.78rem;" onclick="confirmDelete(event, '确定删除 {{ round.contract_code }} 整轮数据?', this.closest('form').action)">删除</button>
|
||||||
|
</form>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="padding:16px 20px;">
|
||||||
|
|
||||||
|
{# ── 止盈阈值 ── #}
|
||||||
|
{% if round.positions %}
|
||||||
|
{% set latest = round.positions[-1] %}
|
||||||
|
<div style="background:var(--accent-light);border:1px solid var(--accent);border-radius:6px;padding:10px 16px;margin-bottom:16px;display:flex;align-items:center;gap:20px;flex-wrap:wrap;">
|
||||||
|
<span style="font-size:0.78rem;color:var(--sub);">止盈阈值</span>
|
||||||
|
<span style="font-weight:700;">{{ round.daily_hands }} × {{ latest.amp_threshold }} = {{ round.daily_hands * latest.amp_threshold }} 点</span>
|
||||||
|
<span style="color:var(--sub);">|</span>
|
||||||
|
<span style="font-size:0.78rem;color:var(--sub);">当前 A = {{ latest.amp_threshold }}</span>
|
||||||
|
<span style="color:var(--sub);">|</span>
|
||||||
|
<span style="font-size:0.78rem;color:var(--sub);">总手数 {{ round.daily_hands * round.positions|length }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{# ── 添加仓位 ── #}
|
||||||
|
<div style="margin-bottom:16px;">
|
||||||
|
<form method="post" action="/positions/{{ round.id }}/add">
|
||||||
|
<div style="display:flex;gap:10px;align-items:flex-end;flex-wrap:wrap;">
|
||||||
|
<div class="form-group" style="margin-bottom:0;flex:1;min-width:100px;">
|
||||||
|
<label>开仓价</label>
|
||||||
|
<input type="number" name="open_price" required placeholder="如 1300" style="font-size:0.9rem;">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom:0;flex:1;min-width:100px;">
|
||||||
|
<label>振幅 A</label>
|
||||||
|
<input type="number" name="amp_threshold" required placeholder="如 18" style="font-size:0.9rem;">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom:0;flex:1;min-width:70px;">
|
||||||
|
<label>手数</label>
|
||||||
|
<input type="number" name="hands" value="{{ round.daily_hands }}" min="1" max="10" required style="font-size:0.9rem;">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary" style="font-size:0.82rem;padding:8px 18px;">+ 添加</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# ── 仓位表格 ── #}
|
||||||
|
{% if round.positions %}
|
||||||
|
<div class="table-wrap" style="margin-bottom:0;">
|
||||||
|
<table style="font-size:0.84rem;">
|
||||||
|
<tr>
|
||||||
|
<th>开仓价</th><th>A</th><th>锁仓价</th><th>手数</th><th>已锁</th><th>状态</th><th>操作</th>
|
||||||
|
</tr>
|
||||||
|
{% for p in round.positions|reverse %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ p.open_price }}</td>
|
||||||
|
<td>{{ p.amp_threshold }}</td>
|
||||||
|
<td style="color:var(--danger-fg);font-weight:600;">{{ p.lock_price }}</td>
|
||||||
|
<td>{{ p.hands }}</td>
|
||||||
|
<td>
|
||||||
|
{% if p.locked_count > 0 %}
|
||||||
|
<span class="badge badge-warn">{{ p.locked_count }}/{{ p.hands }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="na">0/{{ p.hands }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if p.locked_count >= p.hands %}
|
||||||
|
<span class="badge badge-down">已全锁</span>
|
||||||
|
{% elif p.locked_count > 0 %}
|
||||||
|
<span class="badge badge-warn">部分锁</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge badge-up">活跃</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div style="display:flex;gap:6px;justify-content:center;">
|
||||||
|
<button style="background:var(--warn-bg);color:var(--warn-fg);border:1px solid var(--warn);padding:2px 8px;border-radius:4px;cursor:pointer;font-size:0.75rem;font-weight:600;"
|
||||||
|
{% if p.locked_count >= p.hands %}disabled style="opacity:0.4;cursor:default;"{% endif %}
|
||||||
|
onclick="showLockConfirm({{ p.id }}, {{ p.lock_price }})">锁</button>
|
||||||
|
<form method="post" action="/positions/{{ p.id }}/unlock" style="display:inline;">
|
||||||
|
<button style="background:var(--surface);color:var(--sub);border:1px solid var(--border);padding:2px 8px;border-radius:4px;cursor:pointer;font-size:0.75rem;"
|
||||||
|
{% if p.locked_count == 0 %}disabled style="opacity:0.4;cursor:default;"{% endif %}>撤</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div style="text-align:center;padding:24px;color:var(--sub);font-size:0.84rem;">暂无仓位,请添加。</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var productContracts = {{ product_contracts | tojson }};
|
||||||
|
function filterContracts() {
|
||||||
|
var prod = document.getElementById('productSelect').value;
|
||||||
|
var sel = document.getElementById('contractSelect');
|
||||||
|
sel.innerHTML = '<option value="">-- 选择合约 --</option>';
|
||||||
|
sel.disabled = !prod;
|
||||||
|
if (prod && productContracts[prod]) {
|
||||||
|
productContracts[prod].forEach(function(c) {
|
||||||
|
var display = c.startsWith(prod) ? c.substring(prod.length) : c;
|
||||||
|
sel.innerHTML += '<option value="' + c + '">' + display + '</option>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function showMaxLocksModal(rid, currentMax) {
|
||||||
|
document.getElementById('maxLocksRid').value = rid;
|
||||||
|
document.getElementById('maxLocksInput').value = currentMax;
|
||||||
|
document.getElementById('maxLocksModal').style.display = 'block';
|
||||||
|
document.getElementById('maxLocksModalOverlay').style.display = 'block';
|
||||||
|
document.getElementById('maxLocksInput').focus();
|
||||||
|
}
|
||||||
|
function hideMaxLocksModal() {
|
||||||
|
document.getElementById('maxLocksModal').style.display = 'none';
|
||||||
|
document.getElementById('maxLocksModalOverlay').style.display = 'none';
|
||||||
|
}
|
||||||
|
function doUpdateMaxLocks() {
|
||||||
|
var rid = document.getElementById('maxLocksRid').value;
|
||||||
|
var fd = new FormData();
|
||||||
|
fd.append('max_locks', document.getElementById('maxLocksInput').value);
|
||||||
|
fetch('/positions/' + rid + '/max-locks', {method:'POST', body:fd})
|
||||||
|
.then(function() { location.reload(); });
|
||||||
|
}
|
||||||
|
function showLockConfirm(pid, lockPrice) {
|
||||||
|
document.getElementById('lockPid').value = pid;
|
||||||
|
document.getElementById('lockPriceInput').value = lockPrice;
|
||||||
|
document.getElementById('lockModal').style.display = 'block';
|
||||||
|
document.getElementById('lockModalOverlay').style.display = 'block';
|
||||||
|
document.getElementById('lockPriceInput').focus();
|
||||||
|
}
|
||||||
|
function hideLockModal() {
|
||||||
|
document.getElementById('lockModal').style.display = 'none';
|
||||||
|
document.getElementById('lockModalOverlay').style.display = 'none';
|
||||||
|
}
|
||||||
|
function doLock() {
|
||||||
|
var pid = document.getElementById('lockPid').value;
|
||||||
|
var fd = new FormData();
|
||||||
|
fd.append('lock_price', document.getElementById('lockPriceInput').value);
|
||||||
|
fetch('/positions/' + pid + '/lock', {method:'POST', body:fd})
|
||||||
|
.then(function() { location.reload(); });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="maxLocksModalOverlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:159;" onclick="hideMaxLocksModal()"></div>
|
||||||
|
<div id="maxLocksModal" style="display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:24px;z-index:160;width:300px;max-width:90vw;">
|
||||||
|
<h3 style="margin-bottom:16px;">修改锁仓上限</h3>
|
||||||
|
<input type="hidden" id="maxLocksRid">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>锁仓次数上限(熔断阈值)</label>
|
||||||
|
<input type="number" id="maxLocksInput" min="1" max="10" required style="font-size:1rem;text-align:center;font-weight:600;">
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:10px;justify-content:flex-end;margin-top:8px;">
|
||||||
|
<button class="btn" style="background:var(--th-bg);color:var(--fg);" onclick="hideMaxLocksModal()">取消</button>
|
||||||
|
<button class="btn" style="background:var(--accent);color:#fff;" onclick="doUpdateMaxLocks()">确认修改</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="lockModalOverlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:149;" onclick="hideLockModal()"></div>
|
||||||
|
<div id="lockModal" style="display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:24px;z-index:150;width:300px;max-width:90vw;">
|
||||||
|
<h3 style="margin-bottom:16px;">确认锁仓</h3>
|
||||||
|
<input type="hidden" id="lockPid">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>锁仓价位</label>
|
||||||
|
<input type="number" id="lockPriceInput" required style="font-size:1rem;text-align:center;font-weight:600;color:var(--danger-fg);">
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:10px;justify-content:flex-end;margin-top:8px;">
|
||||||
|
<button class="btn" style="background:var(--th-bg);color:var(--fg);" onclick="hideLockModal()">取消</button>
|
||||||
|
<button class="btn" style="background:var(--warn);color:#fff;" onclick="doLock()">确认锁仓</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="confirmOverlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:199;" onclick="hideConfirm()"></div>
|
||||||
|
<div id="confirmBox" style="display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:28px 32px;z-index:200;text-align:center;max-width:90vw;">
|
||||||
|
<p id="confirmMsg" style="font-size:0.95rem;margin-bottom:20px;"></p>
|
||||||
|
<div style="display:flex;gap:10px;justify-content:center;">
|
||||||
|
<button class="btn" style="background:var(--th-bg);color:var(--fg);" onclick="hideConfirm()">取消</button>
|
||||||
|
<button class="btn" style="background:var(--danger);color:#fff;" id="confirmBtn">确认</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function confirmDelete(e, msg, url) {
|
||||||
|
e.preventDefault();
|
||||||
|
document.getElementById('confirmMsg').textContent = msg;
|
||||||
|
document.getElementById('confirmBtn').onclick = function() {
|
||||||
|
var f = document.createElement('form');
|
||||||
|
f.method = 'POST';
|
||||||
|
f.action = url;
|
||||||
|
document.body.appendChild(f);
|
||||||
|
f.submit();
|
||||||
|
};
|
||||||
|
document.getElementById('confirmOverlay').style.display = 'block';
|
||||||
|
document.getElementById('confirmBox').style.display = 'block';
|
||||||
|
}
|
||||||
|
function hideConfirm() {
|
||||||
|
document.getElementById('confirmOverlay').style.display = 'none';
|
||||||
|
document.getElementById('confirmBox').style.display = 'none';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,404 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}交易记录{% endblock %}
|
||||||
|
{% block heading %}交易记录{% endblock %}
|
||||||
|
{% block breadcrumb %}开平仓记录{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
{% set view = request.query_params.get('view', '') %}
|
||||||
|
{% set product_tab = request.query_params.get('tab', '全部') %}
|
||||||
|
{% set product_codes = product_contracts.keys()|list %}
|
||||||
|
{% set grouped_open = {} %}
|
||||||
|
{% for t in open_trades %}
|
||||||
|
{% set _ = grouped_open.setdefault(t.product_code, []).append(t) %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.tabs { display: flex; gap: 0; margin-bottom: 24px; border-bottom: 2px solid var(--border); }
|
||||||
|
.tab-btn {
|
||||||
|
padding: 10px 20px; border: none; background: none;
|
||||||
|
font-size: 0.88rem; font-weight: 500; color: var(--sub);
|
||||||
|
cursor: pointer; text-decoration: none; border-bottom: 2px solid transparent;
|
||||||
|
margin-bottom: -2px; transition: color .12s, border-color .12s;
|
||||||
|
}
|
||||||
|
.tab-btn:hover { color: var(--fg); }
|
||||||
|
.tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; }
|
||||||
|
.tab-panel { display: none; }
|
||||||
|
.tab-panel.active { display: block; }
|
||||||
|
.sub-tabs { display: flex; gap: 6px; margin-bottom: 16px; flex-wrap: wrap; }
|
||||||
|
.sub-tab-btn {
|
||||||
|
padding: 4px 14px; border: 1px solid var(--border); background: var(--surface);
|
||||||
|
font-size: 0.78rem; color: var(--sub); cursor: pointer; text-decoration: none;
|
||||||
|
border-radius: 4px; transition: color .12s, border-color .12s;
|
||||||
|
}
|
||||||
|
.sub-tab-btn:hover { color: var(--fg); border-color: var(--accent); }
|
||||||
|
.sub-tab-btn.active { color: var(--accent); border-color: var(--accent); font-weight: 600; background: var(--accent-light); }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="tabs">
|
||||||
|
<a href="/trades/" class="tab-btn{% if view != 'closed' %} active{% endif %}">📋 持仓</a>
|
||||||
|
<a href="/trades/?view=closed" class="tab-btn{% if view == 'closed' %} active{% endif %}">📊 已平仓</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if view != 'closed' %}
|
||||||
|
<div class="tab-panel active">
|
||||||
|
|
||||||
|
{# ── 新建开仓 ── #}
|
||||||
|
<div class="section-title">新建开仓</div>
|
||||||
|
<div class="form-card" style="margin-bottom:28px;max-width:100%;">
|
||||||
|
<form method="post" action="/trades/open">
|
||||||
|
<div style="display:flex;gap:10px;align-items:flex-end;flex-wrap:wrap;">
|
||||||
|
<div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:100px;">
|
||||||
|
<label>品种</label>
|
||||||
|
<select id="productSelect" required onchange="filterContracts()">
|
||||||
|
<option value="">--</option>
|
||||||
|
{% for pcode in product_contracts %}
|
||||||
|
<option value="{{ pcode }}">{{ pcode }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:120px;">
|
||||||
|
<label>合约</label>
|
||||||
|
<select name="contract_code" id="contractSelect" required disabled>
|
||||||
|
<option value="">-- 先选品种 --</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:70px;">
|
||||||
|
<label>方向</label>
|
||||||
|
<select name="direction" required>
|
||||||
|
{% for v, label in directions %}
|
||||||
|
<option value="{{ v }}">{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom:0;flex:1;min-width:140px;">
|
||||||
|
<label>开仓日期</label>
|
||||||
|
<input type="date" name="open_date" value="{{ today }}" required style="font-size:0.85rem;">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:80px;">
|
||||||
|
<label>开仓价</label>
|
||||||
|
<input type="number" step="any" name="open_price" required placeholder="1300" style="font-size:0.9rem;">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:80px;">
|
||||||
|
<label>手续费</label>
|
||||||
|
<input type="number" step="any" name="open_fee" value="0" style="font-size:0.9rem;">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary" style="font-size:0.82rem;padding:8px 18px;">开仓</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# ── 持仓列表 ── #}
|
||||||
|
<div class="section-title">持仓中 · {{ open_trades|length }} 笔</div>
|
||||||
|
|
||||||
|
<div class="sub-tabs">
|
||||||
|
<a href="/trades/?tab=全部" class="sub-tab-btn{% if product_tab == '全部' %} active{% endif %}">全部</a>
|
||||||
|
{% for pc in product_codes %}
|
||||||
|
{% set count = grouped_open.get(pc, [])|length %}
|
||||||
|
<a href="/trades/?tab={{ pc }}" class="sub-tab-btn{% if product_tab == pc %} active{% endif %}">{{ pc }}{% if count %} · {{ count }}{% endif %}</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if open_trades %}
|
||||||
|
{% for pc in product_codes %}
|
||||||
|
{% if product_tab == '全部' or product_tab == pc %}
|
||||||
|
{% set trades = grouped_open.get(pc, []) %}
|
||||||
|
{% if trades %}
|
||||||
|
<div class="table-wrap" style="margin-bottom:24px;">
|
||||||
|
{% if product_tab == '全部' %}
|
||||||
|
<div class="section-title" style="font-size:0.88rem;margin-bottom:0;padding:6px 12px;">{{ pc }} · {{ trades|length }} 笔</div>
|
||||||
|
{% endif %}
|
||||||
|
<table>
|
||||||
|
<tr><th>合约</th><th>方向</th><th>开仓日期</th><th>开仓价</th><th>手续费</th><th>操作</th></tr>
|
||||||
|
{% for t in trades %}
|
||||||
|
<tr>
|
||||||
|
<td><strong>{{ t.contract_code.replace(t.product_code, '', 1) }}</strong></td>
|
||||||
|
<td>
|
||||||
|
{% if t.direction == 'short' %}
|
||||||
|
<span class="badge badge-down">空</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge badge-up">多</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ t.open_date }} {{ weekdays[t.open_date.weekday()] }}</td>
|
||||||
|
<td>{{ t.open_price }}</td>
|
||||||
|
<td>{{ t.open_fee or 0 }}</td>
|
||||||
|
<td>
|
||||||
|
<button style="background:var(--accent);color:#fff;border:none;padding:3px 10px;border-radius:4px;cursor:pointer;font-size:0.78rem;"
|
||||||
|
onclick="showCloseForm({{ t.id }})">平仓</button>
|
||||||
|
<button style="background:var(--success-bg);color:var(--success-fg);border:1px solid var(--success);padding:3px 10px;border-radius:4px;cursor:pointer;font-size:0.78rem;margin-left:8px;"
|
||||||
|
onclick="showEditForm('trade', {{ t.id }}, '{{ t.product_code }}', '{{ t.contract_code }}', '{{ t.direction }}', {{ t.open_price }}, {{ t.open_fee or 0 }}, '{{ t.open_date }}', null, null, null, '{{ t.status }}')">编辑</button>
|
||||||
|
<form method="post" action="/trades/{{ t.id }}/delete" style="display:inline;margin-left:8px;">
|
||||||
|
<button style="background:transparent;color:#dc2626;border:1px solid #ef4444;padding:3px 10px;border-radius:4px;cursor:pointer;font-size:0.78rem;" onclick="confirmDelete(event, '确定删除此记录?', this.closest('form').action)">删除</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div style="text-align:center;padding:40px;color:var(--sub);">暂无持仓</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{# ── 平仓弹窗 ── #}
|
||||||
|
<div id="closeFormOverlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:99;" onclick="hideCloseForm()"></div>
|
||||||
|
<div id="closeFormBox" style="display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:24px;z-index:100;width:360px;max-width:90vw;">
|
||||||
|
<h3 style="margin-bottom:16px;">平仓</h3>
|
||||||
|
<form method="post" id="closeForm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>平仓日期</label>
|
||||||
|
<input type="date" name="close_date" value="{{ today }}" required style="font-size:0.85rem;">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>平仓价格</label>
|
||||||
|
<input type="number" step="any" name="close_price" required style="font-size:0.9rem;">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>手续费</label>
|
||||||
|
<input type="number" step="any" name="close_fee" value="0" style="font-size:0.9rem;">
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:10px;justify-content:flex-end;">
|
||||||
|
<button type="button" class="btn" style="background:var(--th-bg);color:var(--fg);" onclick="hideCloseForm()">取消</button>
|
||||||
|
<button type="submit" class="btn btn-primary">确认平仓</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="tab-panel active">
|
||||||
|
{# ═══════════════ 已平仓 ═══════════════ #}
|
||||||
|
|
||||||
|
{% if closed_trades %}
|
||||||
|
<div class="section-title">已平仓 · {{ closed_trades|length }} 笔</div>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table>
|
||||||
|
<tr><th>品种</th><th>合约</th><th>方向</th><th>开仓</th><th>平仓</th><th>持仓</th><th>开仓价</th><th>平仓价</th><th>开仓费</th><th>平仓费</th><th>盈亏</th><th>操作</th></tr>
|
||||||
|
{% for t in closed_trades %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ t.product_code }}</td>
|
||||||
|
<td><strong>{{ t.contract_code.replace(t.product_code, '', 1) }}</strong></td>
|
||||||
|
<td>
|
||||||
|
{% if t.direction == 'short' %}
|
||||||
|
<span class="badge badge-down">空</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge badge-up">多</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td style="font-size:0.8rem;">{{ t.open_date }} {{ weekdays[t.open_date.weekday()] }}</td>
|
||||||
|
<td style="font-size:0.8rem;">{{ t.close_date }} {{ weekdays[t.close_date.weekday()] }}</td>
|
||||||
|
<td>{{ (t.close_date - t.open_date).days }}天</td>
|
||||||
|
<td>{{ t.open_price }}</td>
|
||||||
|
<td>{{ t.close_price }}</td>
|
||||||
|
<td>{{ t.open_fee or 0 }}</td>
|
||||||
|
<td>{{ t.close_fee or 0 }}</td>
|
||||||
|
<td>
|
||||||
|
{% set p = t.pnl %}
|
||||||
|
{% if p is not none %}
|
||||||
|
<span style="font-weight:700;{% if p > 0 %}color:var(--danger-fg);{% elif p < 0 %}color:var(--success-fg);{% else %}color:var(--sub);{% endif %}">
|
||||||
|
{% if p > 0 %}+{% endif %}{{ p }}
|
||||||
|
</span>
|
||||||
|
<span style="color:var(--sub);cursor:pointer;font-size:0.75rem;margin-left:4px;"
|
||||||
|
onclick="showPnlDrawer('{{ t.product_code }} {{ t.contract_code.replace(t.product_code, '', 1) }}', '{{ t.direction }}', {{ t.open_price }}, {{ t.close_price }}, {{ t.open_fee or 0 }}, {{ t.close_fee or 0 }}, {{ p }})" title="计算过程">ⓘ</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button style="background:var(--success-bg);color:var(--success-fg);border:1px solid var(--success);padding:3px 10px;border-radius:4px;cursor:pointer;font-size:0.78rem;margin-left:8px;"
|
||||||
|
onclick="showEditForm('trade', {{ t.id }}, '{{ t.product_code }}', '{{ t.contract_code }}', '{{ t.direction }}', {{ t.open_price }}, {{ t.open_fee or 0 }}, '{{ t.open_date }}', {{ t.close_price }}, {{ t.close_fee or 0 }}, '{{ t.close_date }}', '{{ t.status }}')">编辑</button>
|
||||||
|
<form method="post" action="/trades/{{ t.id }}/delete" style="display:inline;margin-left:8px;">
|
||||||
|
<button style="background:transparent;color:#dc2626;border:1px solid #ef4444;padding:3px 10px;border-radius:4px;cursor:pointer;font-size:0.78rem;" onclick="confirmDelete(event, '确定删除此记录?', this.closest('form').action)">删除</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% set ns = namespace(total_pnl=0, total_open_fee=0, total_close_fee=0) %}
|
||||||
|
{% for t in closed_trades %}
|
||||||
|
{% set ns.total_pnl = ns.total_pnl + (t.pnl or 0) %}
|
||||||
|
{% set ns.total_open_fee = ns.total_open_fee + (t.open_fee or 0) %}
|
||||||
|
{% set ns.total_close_fee = ns.total_close_fee + (t.close_fee or 0) %}
|
||||||
|
{% endfor %}
|
||||||
|
<div style="display:flex;gap:24px;margin-bottom:24px;padding:12px 18px;background:var(--surface);border:1px solid var(--border);border-radius:8px;font-size:0.88rem;flex-wrap:wrap;">
|
||||||
|
<span>盈亏合计 <b style="{% if ns.total_pnl > 0 %}color:var(--danger-fg);{% elif ns.total_pnl < 0 %}color:var(--success-fg);{% endif %}">{% if ns.total_pnl > 0 %}+{% endif %}{{ '%.2f'|format(ns.total_pnl) }}</b></span>
|
||||||
|
<span style="color:var(--sub);">|</span>
|
||||||
|
<span>开仓手续费 <b>{{ '%.2f'|format(ns.total_open_fee) }}</b></span>
|
||||||
|
<span style="color:var(--sub);">|</span>
|
||||||
|
<span>平仓手续费 <b>{{ '%.2f'|format(ns.total_close_fee) }}</b></span>
|
||||||
|
<span style="color:var(--sub);">|</span>
|
||||||
|
<span>手续费合计 <b>{{ '%.2f'|format(ns.total_open_fee + ns.total_close_fee) }}</b></span>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div style="text-align:center;padding:60px;color:var(--sub);">暂无已平仓记录</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if view != 'closed' %}
|
||||||
|
<script>
|
||||||
|
var productContracts = {{ product_contracts | tojson }};
|
||||||
|
function filterContracts() {
|
||||||
|
var prod = document.getElementById('productSelect').value;
|
||||||
|
var sel = document.getElementById('contractSelect');
|
||||||
|
sel.innerHTML = '<option value="">-- 选择合约 --</option>';
|
||||||
|
sel.disabled = !prod;
|
||||||
|
if (prod && productContracts[prod]) {
|
||||||
|
productContracts[prod].forEach(function(c) {
|
||||||
|
var display = c.startsWith(prod) ? c.substring(prod.length) : c;
|
||||||
|
sel.innerHTML += '<option value="' + c + '">' + display + '</option>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function showCloseForm(tradeId) {
|
||||||
|
document.getElementById('closeForm').action = '/trades/' + tradeId + '/close';
|
||||||
|
document.getElementById('closeFormOverlay').style.display = 'block';
|
||||||
|
document.getElementById('closeFormBox').style.display = 'block';
|
||||||
|
}
|
||||||
|
function hideCloseForm() {
|
||||||
|
document.getElementById('closeFormOverlay').style.display = 'none';
|
||||||
|
document.getElementById('closeFormBox').style.display = 'none';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div id="editFormOverlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:98;" onclick="hideEditForm()"></div>
|
||||||
|
<div id="editFormBox" style="display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:24px;z-index:99;width:400px;max-width:90vw;max-height:90vh;overflow-y:auto;">
|
||||||
|
<h3 style="margin-bottom:16px;">编辑记录</h3>
|
||||||
|
<form method="post" id="editForm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>合约</label>
|
||||||
|
<input type="text" name="contract_code" id="editContract" required style="font-size:0.9rem;">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>方向</label>
|
||||||
|
<select name="direction" id="editDirection" required>
|
||||||
|
{% for v, label in directions %}
|
||||||
|
<option value="{{ v }}">{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>开仓日期</label>
|
||||||
|
<input type="date" name="open_date" id="editOpenDate" required style="font-size:0.85rem;">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>开仓价</label>
|
||||||
|
<input type="number" step="any" name="open_price" id="editOpenPrice" required style="font-size:0.9rem;">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>开仓手续费</label>
|
||||||
|
<input type="number" step="any" name="open_fee" id="editOpenFee" value="0" style="font-size:0.9rem;">
|
||||||
|
</div>
|
||||||
|
<div id="editCloseFields" style="display:none;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>平仓日期</label>
|
||||||
|
<input type="date" name="close_date" id="editCloseDate" style="font-size:0.85rem;">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>平仓价格</label>
|
||||||
|
<input type="number" step="any" name="close_price" id="editClosePrice" style="font-size:0.9rem;">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>平仓手续费</label>
|
||||||
|
<input type="number" step="any" name="close_fee" id="editCloseFee" value="0" style="font-size:0.9rem;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:10px;justify-content:flex-end;">
|
||||||
|
<button type="button" class="btn" style="background:var(--th-bg);color:var(--fg);" onclick="hideEditForm()">取消</button>
|
||||||
|
<button type="submit" class="btn btn-primary">保存</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="pnlOverlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:99;" onclick="hidePnlDrawer()"></div>
|
||||||
|
<div id="pnlBox" style="display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:24px;z-index:100;width:380px;max-width:90vw;">
|
||||||
|
<h3 style="margin-bottom:4px;" id="pnlTitle"></h3>
|
||||||
|
<p style="font-size:0.8rem;color:var(--sub);margin-bottom:14px;" id="pnlFormula"></p>
|
||||||
|
<table class="calc-table" style="width:100%;border-collapse:collapse;font-size:0.84rem;margin-bottom:12px;" id="pnlTable"></table>
|
||||||
|
<div style="font-size:1rem;" id="pnlResult"></div>
|
||||||
|
<button style="position:absolute;top:12px;right:16px;background:none;border:none;font-size:1.3rem;cursor:pointer;color:var(--sub);" onclick="hidePnlDrawer()">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function showPnlDrawer(title, direction, openPrice, closePrice, openFee, closeFee, result) {
|
||||||
|
document.getElementById('pnlTitle').textContent = title;
|
||||||
|
var dirLabel = direction === 'short' ? '空' : '多';
|
||||||
|
document.getElementById('pnlFormula').textContent = '方向: ' + dirLabel + ' | 每点 20 元';
|
||||||
|
var diff = direction === 'short' ? (openPrice - closePrice) : (closePrice - openPrice);
|
||||||
|
var gross = diff * 20;
|
||||||
|
var fees = openFee + closeFee;
|
||||||
|
var html = '<tr style="background:var(--th-bg);"><td>项目</td><td>计算</td><td style="text-align:right;">金额</td></tr>';
|
||||||
|
html += '<tr><td>价差</td><td>' + (direction === 'short' ? openPrice + ' - ' + closePrice : closePrice + ' - ' + openPrice) + '</td><td style="text-align:right;">' + diff.toFixed(2) + ' 点</td></tr>';
|
||||||
|
html += '<tr><td>毛利</td><td>' + diff.toFixed(2) + ' × 20</td><td style="text-align:right;">' + gross.toFixed(2) + '</td></tr>';
|
||||||
|
html += '<tr><td>开仓手续费</td><td></td><td style="text-align:right;color:var(--danger-fg);">-' + openFee.toFixed(2) + '</td></tr>';
|
||||||
|
html += '<tr><td>平仓手续费</td><td></td><td style="text-align:right;color:var(--danger-fg);">-' + closeFee.toFixed(2) + '</td></tr>';
|
||||||
|
document.getElementById('pnlTable').innerHTML = html;
|
||||||
|
document.getElementById('pnlResult').innerHTML = '<b>盈亏 = ' + gross.toFixed(2) + ' - ' + openFee.toFixed(2) + ' - ' + closeFee.toFixed(2) + ' = <span style="color:' + (result > 0 ? 'var(--danger-fg)' : result < 0 ? 'var(--success-fg)' : 'var(--sub)') + ';">' + (result > 0 ? '+' : '') + result.toFixed(2) + '</span></b>';
|
||||||
|
document.getElementById('pnlOverlay').style.display = 'block';
|
||||||
|
document.getElementById('pnlBox').style.display = 'block';
|
||||||
|
}
|
||||||
|
function hidePnlDrawer() {
|
||||||
|
document.getElementById('pnlOverlay').style.display = 'none';
|
||||||
|
document.getElementById('pnlBox').style.display = 'none';
|
||||||
|
}
|
||||||
|
function showEditForm(type, id, productCode, contractCode, direction, openPrice, openFee, openDate, closePrice, closeFee, closeDate, status) {
|
||||||
|
document.getElementById('editForm').action = '/' + type + 's/' + id + '/edit';
|
||||||
|
document.getElementById('editContract').value = contractCode;
|
||||||
|
document.getElementById('editDirection').value = direction;
|
||||||
|
document.getElementById('editOpenDate').value = openDate;
|
||||||
|
document.getElementById('editOpenPrice').value = openPrice;
|
||||||
|
document.getElementById('editOpenFee').value = openFee || 0;
|
||||||
|
if (status === 'closed' && closeDate) {
|
||||||
|
document.getElementById('editCloseFields').style.display = 'block';
|
||||||
|
document.getElementById('editCloseDate').value = closeDate;
|
||||||
|
document.getElementById('editClosePrice').value = closePrice;
|
||||||
|
document.getElementById('editCloseFee').value = closeFee || 0;
|
||||||
|
} else {
|
||||||
|
document.getElementById('editCloseFields').style.display = 'none';
|
||||||
|
}
|
||||||
|
document.getElementById('editFormOverlay').style.display = 'block';
|
||||||
|
document.getElementById('editFormBox').style.display = 'block';
|
||||||
|
}
|
||||||
|
function hideEditForm() {
|
||||||
|
document.getElementById('editFormOverlay').style.display = 'none';
|
||||||
|
document.getElementById('editFormBox').style.display = 'none';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div id="confirmOverlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:199;" onclick="hideConfirm()"></div>
|
||||||
|
<div id="confirmBox" style="display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:28px 32px;z-index:200;text-align:center;max-width:90vw;">
|
||||||
|
<p id="confirmMsg" style="font-size:0.95rem;margin-bottom:20px;"></p>
|
||||||
|
<div style="display:flex;gap:10px;justify-content:center;">
|
||||||
|
<button class="btn" style="background:var(--th-bg);color:var(--fg);" onclick="hideConfirm()">取消</button>
|
||||||
|
<button class="btn" style="background:var(--danger);color:#fff;" id="confirmBtn">确认删除</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function confirmDelete(e, msg, url) {
|
||||||
|
e.preventDefault();
|
||||||
|
document.getElementById('confirmMsg').textContent = msg;
|
||||||
|
document.getElementById('confirmBtn').onclick = function() {
|
||||||
|
var f = document.createElement('form');
|
||||||
|
f.method = 'POST';
|
||||||
|
f.action = url;
|
||||||
|
document.body.appendChild(f);
|
||||||
|
f.submit();
|
||||||
|
};
|
||||||
|
document.getElementById('confirmOverlay').style.display = 'block';
|
||||||
|
document.getElementById('confirmBox').style.display = 'block';
|
||||||
|
}
|
||||||
|
function hideConfirm() {
|
||||||
|
document.getElementById('confirmOverlay').style.display = 'none';
|
||||||
|
document.getElementById('confirmBox').style.display = 'none';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
Binary file not shown.
@@ -0,0 +1,11 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
image: ft-app
|
||||||
|
container_name: app
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
- ../日报:/app/output
|
||||||
|
restart: unless-stopped
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
fastapi==0.115.6
|
||||||
|
uvicorn==0.34.0
|
||||||
|
sqlalchemy==2.0.36
|
||||||
|
jinja2==3.1.4
|
||||||
|
apscheduler==3.11.0
|
||||||
|
akshare==1.16.72
|
||||||
|
python-multipart==0.0.19
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
# 玻璃 FG609 前20会员持仓排名 — 7月逐日
|
||||||
|
|
||||||
|
数据来源: CZCE 郑商所 (akshare.get_rank_table_czce)
|
||||||
|
合约: FG609(2026年9月,当前主力)
|
||||||
|
周期: 2026-07-01 ~ 2026-07-23 (17个交易日)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、逐日总览
|
||||||
|
|
||||||
|
| 日期 | 总成交量 | 总多单 | 总空单 | 净头寸 | VolTop1 | Top1占比 |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| 07/01 | 1,930,182 | 949,206 | 1,324,156 | -374,950 | 东证期货 | 15.5% |
|
||||||
|
| 07/02 | 1,477,486 | 937,099 | 1,326,235 | -389,136 | 东证期货 | 16.0% |
|
||||||
|
| 07/03 | 1,618,011 | 950,716 | 1,279,121 | -328,405 | 东证期货 | 16.8% |
|
||||||
|
| 07/06 | 1,647,953 | 916,094 | 1,297,613 | -381,519 | 东证期货 | 16.3% |
|
||||||
|
| 07/07 | 1,730,408 | 1,021,142 | 1,456,665 | -435,523 | 东证期货 | 15.2% |
|
||||||
|
| 07/08 | 1,663,671 | 973,767 | 1,330,893 | -357,126 | 东证期货 | 15.9% |
|
||||||
|
| 07/09 | 1,430,861 | 979,649 | 1,370,384 | -390,735 | 东证期货 | 15.5% |
|
||||||
|
| 07/10 | 1,800,991 | 934,281 | 1,297,305 | -363,024 | 东证期货 | 16.7% |
|
||||||
|
| 07/13 | 1,638,225 | 975,465 | 1,411,878 | -436,413 | 东证期货 | 15.4% |
|
||||||
|
| 07/14 | 1,458,782 | 981,524 | 1,422,912 | -441,388 | 东证期货 | 15.5% |
|
||||||
|
| 07/15 | 1,485,987 | 976,429 | 1,416,227 | -439,798 | 东证期货 | 17.1% |
|
||||||
|
| **07/16** | **1,981,774** | 1,095,204 | 1,537,419 | **-442,215** | 东证期货 | 15.3% |
|
||||||
|
| **07/17** | **2,960,457** | 1,094,199 | 1,519,906 | -425,707 | 东证期货 | 14.6% |
|
||||||
|
| 07/20 | 2,300,832 | 1,073,446 | 1,502,020 | -428,574 | **国泰君安** | 15.4% |
|
||||||
|
| 07/21 | 2,026,738 | 1,056,659 | 1,439,583 | -382,924 | **国泰君安** | 16.2% |
|
||||||
|
| 07/22 | 2,096,680 | 997,951 | 1,340,675 | -342,724 | **国泰君安** | 18.2% |
|
||||||
|
| 07/23 | 2,068,815 | 977,891 | 1,324,813 | -346,922 | **国泰君安** | 15.4% |
|
||||||
|
|
||||||
|
## 二、重点会员逐日成交量
|
||||||
|
|
||||||
|
高盛期货罕见进入成交量排名(仅7/02有量2.6万手),但持空单排名稳定前5,另见净头寸表。
|
||||||
|
|
||||||
|
| 日期 | 国泰君安 | 东证期货 | 中信期货 | 永安期货 | 华泰期货 | 方正中期 |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| 07/01 | 233,783 | 299,440 | 259,117 | 45,015 | 147,842 | --- |
|
||||||
|
| 07/02 | 199,925(-33k) | 236,489(-63k) | 216,100(-43k) | --- | 142,175 | 27,160 |
|
||||||
|
| 07/03 | 188,489(-11k) | 271,786(+35k) | 217,696 | --- | 141,025 | 36,401 |
|
||||||
|
| 07/06 | 180,733 | 269,269 | 214,732 | --- | 122,190 | 32,959 |
|
||||||
|
| 07/07 | 217,992(+37k) | 263,822 | 223,750 | --- | 103,020 | 36,855 |
|
||||||
|
| 07/08 | 201,416 | 263,706 | 221,831 | 30,318 | 121,393 | 33,206 |
|
||||||
|
| 07/09 | 168,335(-33k) | 221,584(-42k) | 189,717(-32k) | --- | 86,397(-35k) | 23,539 |
|
||||||
|
| 07/10 | 188,944 | 301,215(+80k) | 225,996(+36k) | 33,801 | 145,975(+60k) | 40,865(+17k) |
|
||||||
|
| 07/13 | 237,251(+48k) | 252,675(-49k) | 211,025 | 30,497 | 104,144(-42k) | 29,512 |
|
||||||
|
| 07/14 | 168,782(-68k) | 226,504(-26k) | 208,536 | --- | 86,121(-18k) | 24,781 |
|
||||||
|
| 07/15 | 194,201(+25k) | 254,317(+28k) | 208,665 | --- | 87,238 | 30,096 |
|
||||||
|
| **07/16** | 273,551(+80k) | 303,239(+49k) | 256,628(+48k) | 40,758 | 122,426(+35k) | 36,676 |
|
||||||
|
| **07/17** | 367,306(+94k) | **432,210**(+129k) | 368,707(+112k) | --- | 187,645(+65k) | 69,986(+33k) |
|
||||||
|
| 07/20 | 354,441 | 293,859(-138k) | 297,056(-72k) | --- | 155,125 | 40,688 |
|
||||||
|
| 07/21 | 328,322(-26k) | 308,655(+15k) | 263,508(-34k) | 27,796 | 113,330(-42k) | 40,499 |
|
||||||
|
| 07/22 | 381,347(+53k) | 294,420 | 300,350(+37k) | 36,730 | 123,276 | 41,792 |
|
||||||
|
| 07/23 | 318,971(-62k) | 300,477 | 269,227(-31k) | --- | 120,984 | 36,205 |
|
||||||
|
|
||||||
|
## 三、重点会员逐日净头寸(多 - 空)
|
||||||
|
|
||||||
|
| 日期 | 国泰君安 | 东证期货 | 中信期货 | 永安期货 | 华泰期货 | 方正中期 | **高盛期货** |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| 07/01 | -92,666 | -95,915 | -109,038 | +6,574 | -68,428 | +60,825 | **-157,673** |
|
||||||
|
| 07/02 | -109,331 | -90,770 | -109,832 | -35,371 | -53,548 | +6,179 | +7,256 |
|
||||||
|
| 07/03 | -102,242 | -87,013 | -89,675 | -30,395 | -40,001 | +6,503 | **-177,843** |
|
||||||
|
| 07/06 | -115,523 | -92,972 | -107,866 | -27,659 | -70,262 | +7,302 | **-178,421** |
|
||||||
|
| 07/07 | -122,252 | -99,256 | -117,756 | -25,952 | -18,146 | +7,059 | **-190,467** |
|
||||||
|
| 07/08 | -101,630 | -97,468 | -112,048 | +6,088 | -46,264 | +5,480 | **-191,861** |
|
||||||
|
| 07/09 | -116,054 | -104,643 | -111,114 | -32,427 | -71,119 | +9,083 | **-199,514** |
|
||||||
|
| 07/10 | -100,286 | -100,733 | -103,834 | +5,922 | -45,536 | +8,017 | **-194,311** |
|
||||||
|
| 07/13 | -119,242 | -108,172 | -125,378 | +10,977 | -17,849 | +10,657 | **-194,729** |
|
||||||
|
| 07/14 | -130,405 | -100,958 | -121,409 | -17,893 | -18,680 | +10,158 | **-196,440** |
|
||||||
|
| 07/15 | -126,199 | -98,211 | -127,246 | -17,504 | -71,785 | +8,840 | **-190,739** |
|
||||||
|
| **07/16** | **-126,934** | **-117,147** | **-126,616** | **+6,741** | -24,580 | +7,371 | **-203,618** |
|
||||||
|
| **07/17** | -116,531 | -105,050 | **-144,715** | -15,082 | -23,576 | +8,735 | **-218,070** |
|
||||||
|
| 07/20 | -86,493 | **-115,884** | **-141,896** | -20,301 | **-105,208** | +6,407 | **-219,290** |
|
||||||
|
| 07/21 | **-64,475** | **-124,442** | -102,917 | +5,466 | -32,130 | +3,932 | **-203,387** |
|
||||||
|
| 07/22 | **-48,705** | -99,658 | -102,830 | +308 | -48,643 | +3,158 | **-188,460** |
|
||||||
|
| **07/23** | **-45,296** | **-113,002** | -106,827 | +1,022 | -27,976 | +2,858 | **-183,405** |
|
||||||
|
|
||||||
|
## 四、关键发现
|
||||||
|
|
||||||
|
### 1. 暴跌前后(7/16-7/17)的会员行为
|
||||||
|
|
||||||
|
**7/16(跌-2.11%,放量)**
|
||||||
|
- 总成交量从148万→198万,放大34%
|
||||||
|
- 东证、国泰君安、中信三家增量最猛:国泰君安+80k,东证+49k,中信+48k
|
||||||
|
- 多空前10均有明显增仓,但净空头仍维持-44万
|
||||||
|
|
||||||
|
**7/17(跌-4.05%,放量暴跌)**
|
||||||
|
- 总成交量飙至**296万**(月内最高,+50%)
|
||||||
|
- 东证期货成交量**43.2万**(月内最高个股)
|
||||||
|
- 中信期货净空飙至-144,715(月内最空)
|
||||||
|
- 东证净空-105k,国泰君安净空-117k(但比16日略收窄)
|
||||||
|
|
||||||
|
### 2. 头把交椅易主
|
||||||
|
|
||||||
|
- **1-16日**:东证期货稳坐成交量第一(日均26万手)
|
||||||
|
- **7/20起**:**国泰君安**反超东证,成交量连续四天第一(最高38万手)
|
||||||
|
- 同时国泰君安**净空头寸急剧收窄**:从-127k(7/16)缩至-45k(7/23),在减空单
|
||||||
|
|
||||||
|
### 3. 全月净空格局
|
||||||
|
|
||||||
|
- 中信期货持续全月最空,净空维持在-10万~-14万
|
||||||
|
- 东证期货净空稳定在-9万~-12万
|
||||||
|
- 国泰君安7月下旬明显减空
|
||||||
|
- 方正中期是上榜中**唯一持续净多**的会员(+3k~+10k)
|
||||||
|
- 整个品种全月净空在-33万~-44万之间
|
||||||
|
|
||||||
|
### 4. 外资参与:高盛期货 — 沉默的巨量空头
|
||||||
|
|
||||||
|
高盛期货的持仓行为与中资会员截然不同:
|
||||||
|
|
||||||
|
- **不做量**:全月仅7/02一次进入成交量前20(2.6万手),其余日子成交量排名全在前20之外
|
||||||
|
- **但持空单全月排名前5**:净空头寸从7月初 -15.8万持续加至最高 -21.9万(7/20)
|
||||||
|
- **7/02短暂翻多**:当天净头寸 +7,256(唯一一次净多),但次日迅速回补至 -17.8万
|
||||||
|
|
||||||
|
| 日期 | 净空头寸 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| 07/01 | -157,673 | 月初即有大规模的底仓 |
|
||||||
|
| 07/07→07/09 | -190k→-200k | 持续加仓 |
|
||||||
|
| **07/16→07/17** | -203k→**-218k** | **暴跌中继续加空** |
|
||||||
|
| 07/20 | **-219,290** | **全月最空**(价格最低点889附近) |
|
||||||
|
| 07/21→07/23 | -203k→-183k | 小幅减仓,仍维持18万+空头 |
|
||||||
|
|
||||||
|
**高盛空仓与价格走势高度同步**:价格从960跌至890过程中,高盛空单从-19万增到-22万。7/20后价格反弹,高盛才开始减空。说明这是**基于基本面判断的套保/策略空头,而非短线投机**,2-3周建仓周期,不参与日内成交量排名。
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,103 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>FG609 前20会员持仓排名</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f6fa; padding: 24px; color: #1a1a2e; }
|
||||||
|
.header { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 20px; }
|
||||||
|
.header h1 { font-size: 20px; font-weight: 600; }
|
||||||
|
.header span { font-size: 13px; color: #666; }
|
||||||
|
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 12px; }
|
||||||
|
.card { background: #fff; border-radius: 10px; padding: 16px; box-shadow: 0 1px 4px rgba(0,0,0,0.08); }
|
||||||
|
.card .top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
|
||||||
|
.card .rank { font-size: 12px; color: #888; font-weight: 600; }
|
||||||
|
.card .name { font-size: 14px; font-weight: 600; }
|
||||||
|
.card .vol { font-size: 22px; font-weight: 700; }
|
||||||
|
.card .vol small { font-size: 13px; font-weight: 400; }
|
||||||
|
.chg-up { color: #e74c3c; }
|
||||||
|
.chg-down { color: #27ae60; }
|
||||||
|
.chg-0 { color: #999; }
|
||||||
|
.row2 { display: flex; gap: 12px; margin-top: 10px; padding-top: 10px; border-top: 1px solid #eee; }
|
||||||
|
.row2 > div { flex: 1; }
|
||||||
|
.row2 label { font-size: 11px; color: #888; display: block; }
|
||||||
|
.row2 .num { font-size: 15px; font-weight: 600; }
|
||||||
|
.net { font-size: 13px; font-weight: 600; margin-top: 6px; padding-top: 6px; border-top: 1px dashed #eee; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<h1>FG609 前20会员持仓排名</h1>
|
||||||
|
<span>2026-07-23 · 郑州商品交易所</span>
|
||||||
|
</div>
|
||||||
|
<div class="grid" id="grid"></div>
|
||||||
|
<script>
|
||||||
|
const data = [
|
||||||
|
{"rank":1,"name":"国泰君安(代客)","vol":318971,"vol_chg":-62376,"long_oi":147305,"long_chg":2980,"short_oi":192601,"short_chg":-429,"net":-45296},
|
||||||
|
{"rank":2,"name":"东证期货(代客)","vol":300477,"vol_chg":6057,"long_oi":70403,"long_chg":-15227,"short_oi":183405,"short_chg":-5055,"net":-113002},
|
||||||
|
{"rank":3,"name":"中信期货(代客)","vol":269227,"vol_chg":-31123,"long_oi":63443,"long_chg":-528,"short_oi":170270,"short_chg":1821,"net":-106827},
|
||||||
|
{"rank":4,"name":"五矿期货(代客)","vol":136245,"vol_chg":43666,"long_oi":63046,"long_chg":-5745,"short_oi":141610,"short_chg":28996,"net":-78564},
|
||||||
|
{"rank":5,"name":"华泰期货(代客)","vol":120984,"vol_chg":-2292,"long_oi":60040,"long_chg":272,"short_oi":88016,"short_chg":-2187,"net":-27976},
|
||||||
|
{"rank":6,"name":"冠通期货(代客)","vol":98116,"vol_chg":47780,"long_oi":56357,"long_chg":-663,"short_oi":71413,"short_chg":-9151,"net":-15056},
|
||||||
|
{"rank":7,"name":"银河期货(代客)","vol":86560,"vol_chg":-14234,"long_oi":50631,"long_chg":-3588,"short_oi":56787,"short_chg":-5343,"net":-6156},
|
||||||
|
{"rank":8,"name":"广发期货(代客)","vol":86232,"vol_chg":-19280,"long_oi":50128,"long_chg":615,"short_oi":48861,"short_chg":-5848,"net":1267},
|
||||||
|
{"rank":9,"name":"华闻期货(代客)","vol":83156,"vol_chg":-23036,"long_oi":49883,"long_chg":4237,"short_oi":39419,"short_chg":245,"net":10464},
|
||||||
|
{"rank":10,"name":"中泰期货(代客)","vol":81458,"vol_chg":2858,"long_oi":44652,"long_chg":1020,"short_oi":39076,"short_chg":877,"net":5576},
|
||||||
|
{"rank":11,"name":"中信建投(代客)","vol":63270,"vol_chg":4221,"long_oi":43748,"long_chg":2819,"short_oi":38532,"short_chg":-1972,"net":5216},
|
||||||
|
{"rank":12,"name":"光大期货(代客)","vol":62166,"vol_chg":196,"long_oi":42149,"long_chg":1972,"short_oi":38427,"short_chg":488,"net":3722},
|
||||||
|
{"rank":13,"name":"国信期货(代客)","vol":60570,"vol_chg":-5459,"long_oi":32668,"long_chg":1356,"short_oi":35369,"short_chg":-1415,"net":-2701},
|
||||||
|
{"rank":14,"name":"招商期货(代客)","vol":54286,"vol_chg":9923,"long_oi":32363,"long_chg":-1863,"short_oi":30159,"short_chg":-5047,"net":2204},
|
||||||
|
{"rank":15,"name":"海通期货(代客)","vol":47715,"vol_chg":17,"long_oi":31829,"long_chg":-6109,"short_oi":29086,"short_chg":-5276,"net":2743},
|
||||||
|
{"rank":16,"name":"南华期货(代客)","vol":46644,"vol_chg":13351,"long_oi":30128,"long_chg":1924,"short_oi":28816,"short_chg":662,"net":1312},
|
||||||
|
{"rank":17,"name":"瑞达期货(代客)","vol":43583,"vol_chg":8153,"long_oi":29777,"long_chg":-5192,"short_oi":25507,"short_chg":3068,"net":4270},
|
||||||
|
{"rank":18,"name":"安粮期货(代客)","vol":37862,"vol_chg":9527,"long_oi":27348,"long_chg":82,"short_oi":24953,"short_chg":-2943,"net":2395},
|
||||||
|
{"rank":19,"name":"方正中期(代客)","vol":36205,"vol_chg":-5587,"long_oi":26834,"long_chg":1832,"short_oi":23976,"short_chg":-2526,"net":2858},
|
||||||
|
{"rank":20,"name":"徽商期货(代客)","vol":35088,"vol_chg":-1103,"long_oi":25159,"long_chg":-5,"short_oi":18530,"short_chg":211,"net":6629}
|
||||||
|
];
|
||||||
|
|
||||||
|
function cls(v) {
|
||||||
|
if (v > 0) return 'chg-up';
|
||||||
|
if (v < 0) return 'chg-down';
|
||||||
|
return 'chg-0';
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmt(v) { return v.toLocaleString(); }
|
||||||
|
|
||||||
|
function chgStr(v) {
|
||||||
|
const s = v > 0 ? '+' : '';
|
||||||
|
return s + fmt(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
const grid = document.getElementById('grid');
|
||||||
|
data.forEach(d => {
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'card';
|
||||||
|
card.innerHTML = `
|
||||||
|
<div class="top">
|
||||||
|
<span class="rank">#${d.rank}</span>
|
||||||
|
<span class="name">${d.name}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="vol">${fmt(d.vol)} <small class="${cls(d.vol_chg)}">(${chgStr(d.vol_chg)})</small></div>
|
||||||
|
</div>
|
||||||
|
<div class="row2">
|
||||||
|
<div>
|
||||||
|
<label>多单</label>
|
||||||
|
<div class="num">${fmt(d.long_oi)}</div>
|
||||||
|
<div class="${cls(d.long_chg)}" style="font-size:12px">${chgStr(d.long_chg)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>空单</label>
|
||||||
|
<div class="num">${fmt(d.short_oi)}</div>
|
||||||
|
<div class="${cls(d.short_chg)}" style="font-size:12px">${chgStr(d.short_chg)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="net ${cls(d.net)}">净头寸 ${chgStr(d.net)}</div>
|
||||||
|
`;
|
||||||
|
grid.appendChild(card);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,392 @@
|
|||||||
|
"""五大期货交易所前20会员成交持仓排名数据获取工具。
|
||||||
|
|
||||||
|
交易所对照:
|
||||||
|
- CZCE 郑商所 → get_rank_table_czce (玻璃FG、纯碱SA、甲醇MA等)
|
||||||
|
- SHFE 上期所 → get_shfe_rank_table (铜CU、铝AL、螺纹RB、原油SC等)
|
||||||
|
- DCE 大商所 → get_dce_rank_table (铁矿石I、豆粕M、棕榈P等)
|
||||||
|
- CFFEX 中金所 → get_cffex_rank_table (IF、IC、IH、T等)
|
||||||
|
- GFEX 广期所 → futures_gfex_position_rank (工业硅SI、碳酸锂LC等)
|
||||||
|
|
||||||
|
交易所公布规则:
|
||||||
|
- CZCE: 同时公布品种汇总 + 各合约明细
|
||||||
|
- SHFE/CFFEX: 只公布合约明细,品种汇总由函数自行加总
|
||||||
|
- DCE: 只公布品种排名
|
||||||
|
- GFEX: 公布合约明细
|
||||||
|
"""
|
||||||
|
|
||||||
|
import akshare as ak
|
||||||
|
import pandas as pd
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
EXCHANGE_CODE = {
|
||||||
|
"CZCE": "CZCE",
|
||||||
|
"SHFE": "SHFE",
|
||||||
|
"DCE": "DCE",
|
||||||
|
"CFFEX": "CFFEX",
|
||||||
|
"GFEX": "GFEX",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 常见品种 -> 交易所映射
|
||||||
|
VARIETY_EXCHANGE = {
|
||||||
|
# 郑商所 CZCE
|
||||||
|
"FG": "CZCE", "SA": "CZCE", "MA": "CZCE", "TA": "CZCE",
|
||||||
|
"RM": "CZCE", "OI": "CZCE", "ZC": "CZCE", "SF": "CZCE",
|
||||||
|
"SM": "CZCE", "UR": "CZCE", "PF": "CZCE", "PX": "CZCE",
|
||||||
|
"SH": "CZCE", "PR": "CZCE", "PL": "CZCE", "AP": "CZCE",
|
||||||
|
"CJ": "CZCE", "CY": "CZCE", "PK": "CZCE", "CF": "CZCE",
|
||||||
|
"SR": "CZCE",
|
||||||
|
# 上期所 SHFE
|
||||||
|
"CU": "SHFE", "AL": "SHFE", "ZN": "SHFE", "PB": "SHFE",
|
||||||
|
"NI": "SHFE", "SN": "SHFE", "AU": "SHFE", "AG": "SHFE",
|
||||||
|
"RB": "SHFE", "HC": "SHFE", "WR": "SHFE", "FU": "SHFE",
|
||||||
|
"BU": "SHFE", "RU": "SHFE", "SC": "SHFE", "NR": "SHFE",
|
||||||
|
"SP": "SHFE", "SS": "SHFE", "LU": "SHFE", "BC": "SHFE",
|
||||||
|
"AO": "SHFE", "BR": "SHFE", "EC": "SHFE",
|
||||||
|
# 大商所 DCE
|
||||||
|
"C": "DCE", "CS": "DCE", "A": "DCE", "B": "DCE",
|
||||||
|
"M": "DCE", "Y": "DCE", "P": "DCE", "FB": "DCE",
|
||||||
|
"BB": "DCE", "JD": "DCE", "L": "DCE", "V": "DCE",
|
||||||
|
"PP": "DCE", "J": "DCE", "JM": "DCE", "I": "DCE",
|
||||||
|
"EG": "DCE", "RR": "DCE", "EB": "DCE", "PG": "DCE",
|
||||||
|
"LH": "DCE", "LG": "DCE", "BZ": "DCE",
|
||||||
|
# 中金所 CFFEX
|
||||||
|
"IF": "CFFEX", "IC": "CFFEX", "IH": "CFFEX", "IM": "CFFEX",
|
||||||
|
"T": "CFFEX", "TF": "CFFEX", "TS": "CFFEX", "TL": "CFFEX",
|
||||||
|
# 广期所 GFEX
|
||||||
|
"SI": "GFEX", "LC": "GFEX", "PS": "GFEX",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 各交易所的品种代码列表(用于 get_rank_sum)
|
||||||
|
ALL_VARS_CZCE = ["WH","PM","CF","SR","TA","OI","RI","MA","FG","RS","RM","ZC",
|
||||||
|
"JR","LR","SF","SM","WT","TC","GN","RO","ER","SRX","SRY",
|
||||||
|
"WSX","WSY","CY","AP","UR","CJ","SA","PK","PF","PX","SH","PR","PL"]
|
||||||
|
ALL_VARS_DCE = ["C","CS","A","B","M","Y","P","FB","BB","JD","L","V","PP",
|
||||||
|
"J","JM","I","EG","RR","EB","PG","LH","LG","BZ"]
|
||||||
|
ALL_VARS_SHFE = ["CU","AL","ZN","PB","NI","SN","AU","AG","RB","WR","HC",
|
||||||
|
"FU","BU","RU","SC","NR","SP","SS","LU","BC","AO","BR","EC"]
|
||||||
|
ALL_VARS_CFFEX = ["IF","IC","IM","IH","T","TF","TS","TL"]
|
||||||
|
ALL_VARS_GFEX = ["SI","LC","PS"]
|
||||||
|
|
||||||
|
# 没有统一字段名映射时用的 to_int 辅助
|
||||||
|
def _to_int(x):
|
||||||
|
if isinstance(x, str):
|
||||||
|
return int(x.replace(",", ""))
|
||||||
|
return int(x)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 底层接口,按交易所获取当日前20排名
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def fetch_top20(date: str = None, exchange: str = None, variety: str = None) -> pd.DataFrame:
|
||||||
|
"""获取指定交易日该品种的前20会员成交持仓排名。
|
||||||
|
|
||||||
|
主力合约检测调用 :func:`main_contract`,本函数自动定位。
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
date : str or None
|
||||||
|
交易日,格式 YYYY-MM-DD 或 YYYYMMDD。None 表示最新。
|
||||||
|
exchange : str or None
|
||||||
|
交易所代码(CZCE/SHFE/DCE/CFFEX/GFEX)。None 时从 variety 推算。
|
||||||
|
variety : str
|
||||||
|
品种代码(如 "FG", "RB", "I", "IF")。如果为 None 则返回全品种。
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
pd.DataFrame
|
||||||
|
前20会员排名数据。
|
||||||
|
列:rank, party_name, vol, vol_chg,
|
||||||
|
long_party_name, long_open_interest, long_open_interest_chg,
|
||||||
|
short_party_name, short_open_interest, short_open_interest_chg,
|
||||||
|
symbol, exchange
|
||||||
|
"""
|
||||||
|
if exchange is None and variety is not None:
|
||||||
|
exchange = VARIETY_EXCHANGE.get(variety.upper())
|
||||||
|
if exchange is None:
|
||||||
|
raise ValueError(f"未知品种 {variety},请指定 exchange 参数")
|
||||||
|
|
||||||
|
if exchange == "CZCE":
|
||||||
|
return _fetch_czce(date, variety)
|
||||||
|
elif exchange == "SHFE":
|
||||||
|
return _fetch_shfe(date, variety)
|
||||||
|
elif exchange == "DCE":
|
||||||
|
return _fetch_dce(date, variety)
|
||||||
|
elif exchange == "CFFEX":
|
||||||
|
return _fetch_cffex(date, variety)
|
||||||
|
elif exchange == "GFEX":
|
||||||
|
return _fetch_gfex(date, variety)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"不支持的交易所: {exchange}")
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_czce(date=None, variety=None):
|
||||||
|
rank = ak.get_rank_table_czce(date=date)
|
||||||
|
if variety:
|
||||||
|
variety = variety.upper()
|
||||||
|
if variety in rank:
|
||||||
|
# 品种级别汇总
|
||||||
|
return _normalize_czce(rank[variety], variety, "CZCE")
|
||||||
|
# 合约级别: 找到该品种的所有合约
|
||||||
|
contract_keys = [k for k in rank if k.startswith(variety) and k != variety]
|
||||||
|
if not contract_keys:
|
||||||
|
raise ValueError(f"未找到品种 {variety} 的数据")
|
||||||
|
# 返回汇总版
|
||||||
|
return _normalize_czce(rank.get(variety, pd.DataFrame()), variety, "CZCE")
|
||||||
|
else:
|
||||||
|
# 全部品种 - 返回汇总
|
||||||
|
return rank # dict, 调用方自己处理
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_shfe(date=None, variety=None):
|
||||||
|
vars_list = [variety.upper()] if variety else None
|
||||||
|
rank = ak.get_shfe_rank_table(date=date, vars_list=vars_list)
|
||||||
|
# SHFE 返回 dict: {合约代码: DataFrame}
|
||||||
|
rows = []
|
||||||
|
for symbol, df in rank.items():
|
||||||
|
for _, r in df.iterrows():
|
||||||
|
rows.append({
|
||||||
|
"rank": int(r["名次"]),
|
||||||
|
"party_name": r["会员简称"],
|
||||||
|
"vol": r["成交量"],
|
||||||
|
"vol_chg": r["成交量增减"],
|
||||||
|
"long_party_name": r["持买单量-会员简称"],
|
||||||
|
"long_open_interest": r["持买单量-合计"],
|
||||||
|
"long_open_interest_chg": r["持买单量-增减"],
|
||||||
|
"short_party_name": r["持卖单量-会员简称"],
|
||||||
|
"short_open_interest": r["持卖单量-合计"],
|
||||||
|
"short_open_interest_chg": r["持卖单量-增减"],
|
||||||
|
"symbol": symbol,
|
||||||
|
})
|
||||||
|
df = pd.DataFrame(rows)
|
||||||
|
if variety:
|
||||||
|
df = df[df["symbol"].str.startswith(variety.upper())]
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_dce(date=None, variety=None):
|
||||||
|
vars_list = [variety.upper()] if variety else None
|
||||||
|
rank = ak.get_dce_rank_table(date=date, vars_list=vars_list)
|
||||||
|
rows = []
|
||||||
|
for symbol, df in rank.items():
|
||||||
|
for _, r in df.iterrows():
|
||||||
|
rows.append({
|
||||||
|
"rank": int(r["名次"]),
|
||||||
|
"party_name": r["会员简称"],
|
||||||
|
"vol": r["成交量"],
|
||||||
|
"vol_chg": r["成交量增减"],
|
||||||
|
"long_party_name": r["持买单-会员简称"],
|
||||||
|
"long_open_interest": r["持买单"],
|
||||||
|
"long_open_interest_chg": r["持买单增减"],
|
||||||
|
"short_party_name": r["持卖单-会员简称"],
|
||||||
|
"short_open_interest": r["持卖单"],
|
||||||
|
"short_open_interest_chg": r["持卖单增减"],
|
||||||
|
"symbol": symbol,
|
||||||
|
})
|
||||||
|
return pd.DataFrame(rows)
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_cffex(date=None, variety=None):
|
||||||
|
vars_list = [variety.upper()] if variety else None
|
||||||
|
rank = ak.get_cffex_rank_table(date=date, vars_list=vars_list)
|
||||||
|
rows = []
|
||||||
|
for symbol, df in rank.items():
|
||||||
|
for _, r in df.iterrows():
|
||||||
|
rows.append({
|
||||||
|
"rank": int(r["名次"]),
|
||||||
|
"party_name": r["会员简称"],
|
||||||
|
"vol": r["成交量"],
|
||||||
|
"vol_chg": r["成交量增减"],
|
||||||
|
"long_party_name": r["持买单-会员简称"],
|
||||||
|
"long_open_interest": r["持买单-合计"],
|
||||||
|
"long_open_interest_chg": r["持买单-增减"],
|
||||||
|
"short_party_name": r["持卖单-会员简称"],
|
||||||
|
"short_open_interest": r["持卖单-合计"],
|
||||||
|
"short_open_interest_chg": r["持卖单-增减"],
|
||||||
|
"symbol": symbol,
|
||||||
|
})
|
||||||
|
return pd.DataFrame(rows)
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_gfex(date=None, variety=None):
|
||||||
|
vars_list = [variety.upper()] if variety else None
|
||||||
|
rank = ak.futures_gfex_position_rank(date=date, vars_list=vars_list)
|
||||||
|
rows = []
|
||||||
|
if isinstance(rank, dict):
|
||||||
|
for symbol, df in rank.items():
|
||||||
|
for _, r in df.iterrows():
|
||||||
|
rows.append({
|
||||||
|
"rank": int(r["名次"]),
|
||||||
|
"party_name": r["会员简称"],
|
||||||
|
"vol": r["成交量"],
|
||||||
|
"vol_chg": r["成交量增减"],
|
||||||
|
"long_party_name": r["持买单-会员简称"],
|
||||||
|
"long_open_interest": r["持买单-合计"],
|
||||||
|
"long_open_interest_chg": r["持买单-增减"],
|
||||||
|
"short_party_name": r["持卖单-会员简称"],
|
||||||
|
"short_open_interest": r["持卖单-合计"],
|
||||||
|
"short_open_interest_chg": r["持卖单-增减"],
|
||||||
|
"symbol": symbol,
|
||||||
|
})
|
||||||
|
return pd.DataFrame(rows)
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_czce(df, variety, exchange):
|
||||||
|
"""标准化 CZCE 品种汇总 DataFrame 字段名"""
|
||||||
|
df = df.copy()
|
||||||
|
df["symbol"] = df.get("symbol", variety)
|
||||||
|
df["exchange"] = exchange
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 主力合约检测
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def main_contract(variety: str, date: str = None) -> str:
|
||||||
|
"""检测该品种在指定交易日的主力合约代码。
|
||||||
|
|
||||||
|
逻辑:成交量/持仓量最大的合约即为主力。
|
||||||
|
"""
|
||||||
|
variety = variety.upper()
|
||||||
|
exchange = VARIETY_EXCHANGE.get(variety)
|
||||||
|
if exchange is None:
|
||||||
|
raise ValueError(f"未知品种 {variety}")
|
||||||
|
|
||||||
|
rank = ak.get_rank_table_czce(date=date) if exchange == "CZCE" else {}
|
||||||
|
if exchange == "CZCE":
|
||||||
|
# CZCE: 合约级别的 key 是 品种+年月 (FG609), 品种级是 FG
|
||||||
|
contract_keys = [k for k in rank if k.startswith(variety) and k != variety]
|
||||||
|
if not contract_keys:
|
||||||
|
raise ValueError(f"未找到品种 {variety} 的合约数据")
|
||||||
|
vols = {}
|
||||||
|
for ck in contract_keys:
|
||||||
|
df = rank[ck]
|
||||||
|
vol_sum = df["vol"].apply(_to_int).sum()
|
||||||
|
vols[ck] = vol_sum
|
||||||
|
return max(vols, key=vols.get)
|
||||||
|
else:
|
||||||
|
# 其他交易所:通过成交额最大的标的判断
|
||||||
|
raise NotImplementedError(f"{exchange} 主力合约检测待实现")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 品种汇总对比(多交易所多品种)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def rank_summary(date: str = None, varieties: list = None) -> pd.DataFrame:
|
||||||
|
"""多品种前N会员成交量/持仓汇总对比(仅支持 CZCE 品种)。
|
||||||
|
|
||||||
|
使用 get_rank_table_czce 的品种级别汇总数据,
|
||||||
|
返回各品种前5/10/15/20成交量和多空持仓总和。
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
date : str
|
||||||
|
交易日。
|
||||||
|
varieties : list
|
||||||
|
品种代码列表。默认 FG, SA, MA, CF, TA, UR, PF, SM。
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
pd.DataFrame
|
||||||
|
每行一个品种,含品种级前N成交量和多空持仓总和。
|
||||||
|
"""
|
||||||
|
if varieties is None:
|
||||||
|
varieties = ["FG", "SA", "MA", "CF", "TA", "UR", "PF", "SM"]
|
||||||
|
rank = ak.get_rank_table_czce(date=date)
|
||||||
|
rows = []
|
||||||
|
for var in varieties:
|
||||||
|
var = var.upper()
|
||||||
|
if var not in rank:
|
||||||
|
continue
|
||||||
|
df = rank[var]
|
||||||
|
df["vol_int"] = df["vol"].apply(_to_int)
|
||||||
|
df["long_int"] = df["long_open_interest"].apply(_to_int)
|
||||||
|
df["short_int"] = df["short_open_interest"].apply(_to_int)
|
||||||
|
total_vol = df["vol_int"].sum()
|
||||||
|
vol_5 = df.head(5)["vol_int"].sum()
|
||||||
|
vol_10 = df.head(10)["vol_int"].sum()
|
||||||
|
long_sum = df["long_int"].sum()
|
||||||
|
short_sum = df["short_int"].sum()
|
||||||
|
rows.append({
|
||||||
|
"var": var,
|
||||||
|
"vol_total": total_vol,
|
||||||
|
"vol_top5": vol_5,
|
||||||
|
"vol_top10": vol_10,
|
||||||
|
"vol_top20": df["vol_int"].sum(),
|
||||||
|
"long_sum": long_sum,
|
||||||
|
"short_sum": short_sum,
|
||||||
|
"net": long_sum - short_sum,
|
||||||
|
})
|
||||||
|
return pd.DataFrame(rows).sort_values("vol_total", ascending=False)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 主力合约前20成交量
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def main_top20(variety: str, date: str = None) -> pd.DataFrame:
|
||||||
|
"""获取品种主力合约的前20会员成交量排名。
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
variety : str
|
||||||
|
品种代码,如 "FG"。
|
||||||
|
date : str
|
||||||
|
交易日。None 表示最新。
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
pd.DataFrame
|
||||||
|
主力合约前20会员成交量排名 + 多空持仓排名。
|
||||||
|
"""
|
||||||
|
exchange = VARIETY_EXCHANGE.get(variety.upper())
|
||||||
|
if exchange != "CZCE":
|
||||||
|
raise NotImplementedError(f"目前仅支持 CZCE 品种的主力合约前20查询")
|
||||||
|
|
||||||
|
rank = ak.get_rank_table_czce(date=date)
|
||||||
|
contract_keys = [k for k in rank if k.startswith(variety.upper()) and k != variety.upper()]
|
||||||
|
vols = {ck: rank[ck]["vol"].apply(_to_int).sum() for ck in contract_keys}
|
||||||
|
main_code = max(vols, key=vols.get)
|
||||||
|
|
||||||
|
df = rank[main_code].copy()
|
||||||
|
df["symbol"] = main_code
|
||||||
|
# 数值列处理(CZCE 返回字符串含逗号)
|
||||||
|
for col in ["vol", "vol_chg", "long_open_interest", "long_open_interest_chg",
|
||||||
|
"short_open_interest", "short_open_interest_chg"]:
|
||||||
|
df[col] = df[col].apply(_to_int)
|
||||||
|
return df.sort_values("rank")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 输出格式化
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def print_top20(df: pd.DataFrame, title: str = ""):
|
||||||
|
"""打印前20成交量排名 + 多空前10。"""
|
||||||
|
if title:
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print(f" {title}")
|
||||||
|
print(f"{'='*60}")
|
||||||
|
|
||||||
|
print(f"\n【成交量前20】")
|
||||||
|
print(f"{'排名':>4} {'会员':<20} {'成交量':>10} {'变化':>10}")
|
||||||
|
print("-" * 50)
|
||||||
|
for _, r in df.iterrows():
|
||||||
|
print(f"{int(r['rank']):>4} {str(r['vol_party_name']):<20} "
|
||||||
|
f"{int(r['vol']):>10,} {int(r['vol_chg']):>10,}")
|
||||||
|
|
||||||
|
print(f"\n【持多单前10】")
|
||||||
|
print(f"{'排名':>4} {'会员':<20} {'多单':>10} {'变化':>10}")
|
||||||
|
print("-" * 50)
|
||||||
|
for _, r in df.head(10).iterrows():
|
||||||
|
print(f"{int(r['rank']):>4} {str(r['long_party_name']):<20} "
|
||||||
|
f"{int(r['long_open_interest']):>10,} {int(r['long_open_interest_chg']):>10,}")
|
||||||
|
|
||||||
|
print(f"\n【持空单前10】")
|
||||||
|
print(f"{'排名':>4} {'会员':<20} {'空单':>10} {'变化':>10}")
|
||||||
|
print("-" * 50)
|
||||||
|
for _, r in df.head(10).iterrows():
|
||||||
|
print(f"{int(r['rank']):>4} {str(r['short_party_name']):<20} "
|
||||||
|
f"{int(r['short_open_interest']):>10,} {int(r['short_open_interest_chg']):>10,}")
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
"""拉取玻璃FG609 7月份所有交易日的前20会员持仓排名数据"""
|
||||||
|
import akshare as ak
|
||||||
|
import pandas as pd
|
||||||
|
import time
|
||||||
|
from datetime import date, timedelta
|
||||||
|
|
||||||
|
def to_int(x):
|
||||||
|
if isinstance(x, str):
|
||||||
|
return int(x.replace(",", ""))
|
||||||
|
return int(x)
|
||||||
|
|
||||||
|
def get_rank_czce_safe(d):
|
||||||
|
"""安全调用,失败重试"""
|
||||||
|
try:
|
||||||
|
return ak.get_rank_table_czce(date=d)
|
||||||
|
except Exception as e:
|
||||||
|
print(f" [{d}] 失败: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 7月交易日(去掉周末)
|
||||||
|
trading_days = []
|
||||||
|
start = date(2026, 7, 1)
|
||||||
|
end = date(2026, 7, 23)
|
||||||
|
d = start
|
||||||
|
while d <= end:
|
||||||
|
if d.weekday() < 5: # Mon-Fri
|
||||||
|
trading_days.append(d)
|
||||||
|
d += timedelta(days=1)
|
||||||
|
|
||||||
|
print(f"共 {len(trading_days)} 个交易日")
|
||||||
|
|
||||||
|
all_data = {} # date_str -> DataFrame (FG609 member rank)
|
||||||
|
|
||||||
|
for dt in trading_days:
|
||||||
|
ds = dt.strftime("%Y%m%d")
|
||||||
|
print(f"拉取 {ds}...", end=" ", flush=True)
|
||||||
|
rank = get_rank_czce_safe(ds)
|
||||||
|
if rank and "FG609" in rank:
|
||||||
|
df = rank["FG609"].copy()
|
||||||
|
for col in ["vol", "vol_chg", "long_open_interest", "long_open_interest_chg",
|
||||||
|
"short_open_interest", "short_open_interest_chg"]:
|
||||||
|
df[col] = df[col].apply(to_int)
|
||||||
|
df = df.sort_values("rank")
|
||||||
|
all_data[ds] = df
|
||||||
|
# 摘要
|
||||||
|
total_vol = df["vol"].sum()
|
||||||
|
top1 = df.iloc[0]
|
||||||
|
top3_vol = df.head(3)["vol"].sum()
|
||||||
|
long_sum = df["long_open_interest"].sum()
|
||||||
|
short_sum = df["short_open_interest"].sum()
|
||||||
|
print(f"OK vol={total_vol/10000:.0f}万 top1={top1['vol_party_name']}({top1['vol']/10000:.0f}万) net={long_sum-short_sum:+,}")
|
||||||
|
else:
|
||||||
|
print(f"SKIP(无FG609数据)")
|
||||||
|
time.sleep(0.3)
|
||||||
|
|
||||||
|
print(f"\n成功获取 {len(all_data)} 天数据")
|
||||||
|
|
||||||
|
# ====== 汇总:逐日总成交量 & 前5会员 ======
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("【逐日汇总】FG609 前20会员成交量 & 多空净头寸")
|
||||||
|
print("="*80)
|
||||||
|
print(f"{'日期':>8} {'总成交量':>10} {'总多单':>10} {'总空单':>10} {'净头寸':>10} {'VolTop1':<18} {'VolTop1占比':>8}")
|
||||||
|
print("-"*80)
|
||||||
|
for ds in sorted(all_data.keys()):
|
||||||
|
df = all_data[ds]
|
||||||
|
tv = df["vol"].sum()
|
||||||
|
ls = df["long_open_interest"].sum()
|
||||||
|
ss = df["short_open_interest"].sum()
|
||||||
|
t1 = df.iloc[0]
|
||||||
|
share = t1["vol"] / tv * 100
|
||||||
|
d = f"{ds[:4]}-{ds[4:6]}-{ds[6:]}"
|
||||||
|
print(f"{d:>8} {tv:>10,} {ls:>10,} {ss:>10,} {ls-ss:>10,} {t1['vol_party_name']:<18} {share:>7.1f}%")
|
||||||
|
|
||||||
|
# ====== 重点会员逐日变化 ======
|
||||||
|
key_members = ["国泰君安(代客)", "东证期货(代客)", "中信期货(代客)",
|
||||||
|
"永安期货(代客)", "华泰期货(代客)", "方正中期(代客)", "高盛期货(代客)"]
|
||||||
|
print("\n" + "="*100)
|
||||||
|
print("【重点会员逐日成交量】")
|
||||||
|
print("="*100)
|
||||||
|
header = f"{'日期':>8}"
|
||||||
|
for m in key_members:
|
||||||
|
header += f" {m:<16}"
|
||||||
|
print(header)
|
||||||
|
print("-"*100)
|
||||||
|
for ds in sorted(all_data.keys()):
|
||||||
|
df = all_data[ds]
|
||||||
|
line = f"{ds[:4]}-{ds[4:6]}-{ds[6:]:>8}"
|
||||||
|
for m in key_members:
|
||||||
|
row = df[df["vol_party_name"] == m]
|
||||||
|
if not row.empty:
|
||||||
|
vol = row.iloc[0]["vol"]
|
||||||
|
chg = row.iloc[0]["vol_chg"]
|
||||||
|
line += f" {vol:>6,}({chg:>+6,})"
|
||||||
|
else:
|
||||||
|
line += f" {'---':>16}"
|
||||||
|
print(line)
|
||||||
|
|
||||||
|
# ====== 重点会员逐日净头寸(多-空) ======
|
||||||
|
print("\n" + "="*100)
|
||||||
|
print("【重点会员逐日净头寸(多单-空单)】")
|
||||||
|
print("="*100)
|
||||||
|
header = f"{'日期':>8}"
|
||||||
|
for m in key_members:
|
||||||
|
header += f" {m:<16}"
|
||||||
|
print(header)
|
||||||
|
print("-"*100)
|
||||||
|
for ds in sorted(all_data.keys()):
|
||||||
|
df = all_data[ds]
|
||||||
|
line = f"{ds[:4]}-{ds[4:6]}-{ds[6:]:>8}"
|
||||||
|
for m in key_members:
|
||||||
|
row = df[df["vol_party_name"] == m]
|
||||||
|
if not row.empty:
|
||||||
|
r = row.iloc[0]
|
||||||
|
net = r["long_open_interest"] - r["short_open_interest"]
|
||||||
|
line += f" {net:>+8,}"
|
||||||
|
else:
|
||||||
|
# try other name columns
|
||||||
|
row_l = df[df["long_party_name"] == m]
|
||||||
|
row_s = df[df["short_party_name"] == m]
|
||||||
|
long_v = row_l.iloc[0]["long_open_interest"] if not row_l.empty else 0
|
||||||
|
short_v = row_s.iloc[0]["short_open_interest"] if not row_s.empty else 0
|
||||||
|
net = long_v - short_v
|
||||||
|
line += f" {net:>+8,}"
|
||||||
|
print(line)
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>振幅锁仓策略 — FG2609 玻璃</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #fff; --fg: #1e293b; --sub: #64748b;
|
||||||
|
--th-bg: #f1f5f9; --th-fg: #334155; --th-border: #cbd5e1;
|
||||||
|
--td-border: #e2e8f0; --td-even: #f8fafc; --td-first: #334155;
|
||||||
|
--na: #94a3b8;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--bg: #0f172a; --fg: #e2e8f0; --sub: #94a3b8;
|
||||||
|
--th-bg: #1e293b; --th-fg: #cbd5e1; --th-border: #334155;
|
||||||
|
--td-border: #1e293b; --td-even: #1a2332; --td-first: #e2e8f0;
|
||||||
|
--na: #64748b;
|
||||||
|
}
|
||||||
|
body { font-family: -apple-system, 'Segoe UI', system-ui, sans-serif; padding: 24px; max-width: 1100px; margin: 0 auto; color: var(--fg); background: var(--bg); }
|
||||||
|
h1 { font-size: 1.4rem; margin-bottom: 8px; }
|
||||||
|
.sub { color: var(--sub); font-size: 0.85rem; margin-bottom: 16px; }
|
||||||
|
.sub a { color: #3b82f6; cursor: pointer; text-decoration: none; }
|
||||||
|
.sub a:hover { text-decoration: underline; }
|
||||||
|
.formula { color: var(--sub); font-size: 0.8rem; margin: 0; padding: 6px 12px; background: var(--th-bg); border-radius: 4px; display: inline-block; }
|
||||||
|
table { border-collapse: collapse; width: 100%; font-size: 0.9rem; }
|
||||||
|
th { background: var(--th-bg); color: var(--th-fg); font-weight: 600; padding: 8px 10px; border: 1px solid var(--th-border); text-align: center; white-space: nowrap; }
|
||||||
|
td { padding: 6px 10px; border: 1px solid var(--td-border); text-align: center; }
|
||||||
|
tr:nth-child(even) td { background: var(--td-even); }
|
||||||
|
td:first-child { font-weight: 500; color: var(--td-first); }
|
||||||
|
tr.active td { background: #dbeafe !important; }
|
||||||
|
[data-theme="dark"] tr.active td { background: #1e3a5f !important; }
|
||||||
|
td:nth-child(2) { font-size: 0.78rem; color: var(--sub); }
|
||||||
|
.na { color: var(--na); }
|
||||||
|
|
||||||
|
.toggle-btn { position: fixed; top: 16px; right: 24px; background: var(--th-bg); border: 1px solid var(--th-border); color: var(--th-fg); padding: 6px 14px; border-radius: 6px; cursor: pointer; font-size: 0.82rem; z-index: 10; }
|
||||||
|
.toggle-btn:hover { opacity: 0.8; }
|
||||||
|
|
||||||
|
.drawer-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.35); z-index: 99; display: none; }
|
||||||
|
.drawer-overlay.show { display: block; }
|
||||||
|
.drawer { position: fixed; top: 0; right: 0; width: 340px; height: 100%; background: var(--bg); border-left: 1px solid var(--th-border); z-index: 100; padding: 24px; transform: translateX(100%); transition: transform .25s ease; overflow-y: auto; box-shadow: -4px 0 20px rgba(0,0,0,.15); }
|
||||||
|
.drawer.show { transform: translateX(0); }
|
||||||
|
.drawer h3 { font-size: 1rem; margin-bottom: 4px; }
|
||||||
|
.drawer .date-label { color: var(--sub); font-size: 0.82rem; margin-bottom: 16px; }
|
||||||
|
.drawer .calc-table { width: 100%; border-collapse: collapse; font-size: 0.82rem; margin-bottom: 14px; }
|
||||||
|
.drawer .calc-table td { padding: 4px 8px; border: 1px solid var(--td-border); text-align: center; }
|
||||||
|
.drawer .calc-table tr:first-child td { background: var(--th-bg); font-weight: 600; color: var(--th-fg); }
|
||||||
|
.drawer .result { font-size: 0.85rem; line-height: 1.8; color: var(--fg); }
|
||||||
|
.drawer .result b { font-size: 1.1rem; }
|
||||||
|
.drawer .close-btn { position: absolute; top: 16px; right: 20px; background: none; border: none; font-size: 1.4rem; cursor: pointer; color: var(--sub); line-height: 1; }
|
||||||
|
.drawer .close-btn:hover { color: var(--fg); }
|
||||||
|
.amp-cell { cursor: pointer; text-decoration: underline; text-decoration-style: dotted; text-underline-offset: 3px; }
|
||||||
|
.amp-cell:hover { color: #3b82f6; }
|
||||||
|
.nav-link { color: var(--sub); font-size: 0.82rem; margin-bottom: 4px; }
|
||||||
|
.nav-link a { color: #3b82f6; text-decoration: none; }
|
||||||
|
.nav-link a:hover { text-decoration: underline; }
|
||||||
|
.section { margin-bottom: 8px; }
|
||||||
|
.section h2 { font-size: 1.1rem; display: flex; align-items: center; gap: 8px; margin: 16px 0 10px; padding: 4px 0; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<button class="toggle-btn" id="themeToggle">🌙 暗夜</button>
|
||||||
|
|
||||||
|
<h1>振幅锁仓策略 — FG2609 玻璃</h1>
|
||||||
|
<p class="sub"><a href="index.html">返回总览</a> | <span>FG2609</span> | <a href="fg2610.html">FG2610</a> | <a href="fg2611.html">FG2611</a> | <a href="fg2701.html">FG2701</a></p>
|
||||||
|
<p class="formula">振幅算法:近 5 日均振幅 = 过去 5 个交易日 (最高 − 最低) 的均值,结果四舍五入取整</p>
|
||||||
|
|
||||||
|
<div class="section" id="sec-fg2609">
|
||||||
|
<h2>FG2609 玻璃</h2>
|
||||||
|
<table>
|
||||||
|
<tr><th>日期</th><th>星期</th><th>开盘</th><th>收盘</th><th>最高</th><th>最低</th><th>最高最低差价</th><th>振幅</th></tr>
|
||||||
|
<tr><td>2026/7/1</td><td>周三</td><td>972</td><td>961</td><td>972</td><td>956</td><td>16</td><td class="na">-</td></tr>
|
||||||
|
<tr><td>2026/7/2</td><td>周四</td><td>960</td><td>966</td><td>972</td><td>957</td><td>15</td><td class="na">-</td></tr>
|
||||||
|
<tr><td>2026/7/3</td><td>周五</td><td>965</td><td>973</td><td>979</td><td>962</td><td>17</td><td class="na">-</td></tr>
|
||||||
|
<tr><td>2026/7/6</td><td>周一</td><td>977</td><td>967</td><td>982</td><td>964</td><td>18</td><td class="na">-</td></tr>
|
||||||
|
<tr><td>2026/7/7</td><td>周二</td><td>966</td><td>952</td><td>966</td><td>951</td><td>15</td><td class="na">-</td></tr>
|
||||||
|
<tr><td>2026/7/8</td><td>周三</td><td>954</td><td>962</td><td>966</td><td>951</td><td>15</td><td class="amp-cell">16</td></tr>
|
||||||
|
<tr><td>2026/7/9</td><td>周四</td><td>963</td><td>957</td><td>964</td><td>949</td><td>15</td><td class="amp-cell">16</td></tr>
|
||||||
|
<tr><td>2026/7/10</td><td>周五</td><td>956</td><td>965</td><td>974</td><td>952</td><td>22</td><td class="amp-cell">16</td></tr>
|
||||||
|
<tr><td>2026/7/13</td><td>周一</td><td>965</td><td>952</td><td>967</td><td>950</td><td>17</td><td class="amp-cell">17</td></tr>
|
||||||
|
<tr><td>2026/7/14</td><td>周二</td><td>954</td><td>951</td><td>956</td><td>944</td><td>12</td><td class="amp-cell">17</td></tr>
|
||||||
|
<tr><td>2026/7/15</td><td>周三</td><td>950</td><td>949</td><td>958</td><td>944</td><td>14</td><td class="amp-cell">16</td></tr>
|
||||||
|
<tr><td>2026/7/16</td><td>周四</td><td>949</td><td>929</td><td>952</td><td>927</td><td>25</td><td class="amp-cell">16</td></tr>
|
||||||
|
<tr><td>2026/7/17</td><td>周五</td><td>931</td><td>900</td><td>931</td><td>899</td><td>32</td><td class="amp-cell">18</td></tr>
|
||||||
|
<tr><td>2026/7/20</td><td>周一</td><td>902</td><td>890</td><td>908</td><td>889</td><td>19</td><td class="amp-cell">20</td></tr>
|
||||||
|
<tr><td>2026/7/21</td><td>周二</td><td>891</td><td>904</td><td>910</td><td>891</td><td>19</td><td class="amp-cell">20</td></tr>
|
||||||
|
<tr><td>2026/7/22</td><td>周三</td><td>908</td><td>913</td><td>918</td><td>900</td><td>18</td><td class="amp-cell">22</td></tr>
|
||||||
|
<tr><td>2026/7/23</td><td>周四</td><td>914</td><td>899</td><td>917</td><td>892</td><td>25</td><td class="amp-cell">23</td></tr>
|
||||||
|
<tr><td>2026/7/24</td><td>周五</td><td>900</td><td>908</td><td>912</td><td>891</td><td>21</td><td class="amp-cell">23</td></tr>
|
||||||
|
<tr><td>2026/7/27</td><td>周一</td><td class="na">-</td><td class="na">-</td><td class="na">-</td><td class="na">-</td><td>0</td><td class="amp-cell">20</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-overlay" id="overlay" onclick="closeDrawer()"></div>
|
||||||
|
<div class="drawer" id="drawer">
|
||||||
|
<button class="close-btn" onclick="closeDrawer()">×</button>
|
||||||
|
<h3 id="drawer-title"></h3>
|
||||||
|
<p class="date-label" id="drawer-date"></p>
|
||||||
|
<table class="calc-table" id="drawer-table"></table>
|
||||||
|
<div class="result" id="drawer-result"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var themeBtn = document.getElementById('themeToggle');
|
||||||
|
themeBtn.addEventListener('click', toggleTheme);
|
||||||
|
function setTheme(dark) {
|
||||||
|
if (dark) {
|
||||||
|
document.documentElement.setAttribute('data-theme', 'dark');
|
||||||
|
themeBtn.textContent = '☀️ 日间';
|
||||||
|
} else {
|
||||||
|
document.documentElement.removeAttribute('data-theme');
|
||||||
|
themeBtn.textContent = '🌙 暗夜';
|
||||||
|
}
|
||||||
|
try { localStorage.setItem('ft-theme', dark ? 'dark' : 'light'); } catch(e) {}
|
||||||
|
}
|
||||||
|
function toggleTheme() {
|
||||||
|
setTheme(document.documentElement.getAttribute('data-theme') !== 'dark');
|
||||||
|
}
|
||||||
|
(function() {
|
||||||
|
try {
|
||||||
|
var s = localStorage.getItem('ft-theme');
|
||||||
|
if (s === 'dark' || (!s && matchMedia('(prefers-color-scheme: dark)').matches)) setTheme(true);
|
||||||
|
} catch(e) {}
|
||||||
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
var activeRow = null;
|
||||||
|
function closeDrawer() {
|
||||||
|
document.getElementById('overlay').classList.remove('show');
|
||||||
|
document.getElementById('drawer').classList.remove('show');
|
||||||
|
if (activeRow) { activeRow.classList.remove('active'); activeRow = null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('.amp-cell').forEach(function(cell) {
|
||||||
|
cell.addEventListener('click', function() {
|
||||||
|
var row = cell.parentElement;
|
||||||
|
var tbl = row.closest('table');
|
||||||
|
var rows = tbl.querySelectorAll('tr');
|
||||||
|
var h2 = tbl.closest('.section').querySelector('h2');
|
||||||
|
var product = h2.childNodes[h2.childNodes.length - 1].textContent.trim();
|
||||||
|
|
||||||
|
var ranges = [];
|
||||||
|
for (var i = 1; i < rows.length; i++) {
|
||||||
|
var cells = rows[i].querySelectorAll('td');
|
||||||
|
ranges.push({
|
||||||
|
date: cells[0].textContent.trim(),
|
||||||
|
wk: cells[1].textContent.trim(),
|
||||||
|
diff: parseInt(cells[6].textContent) || 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var idx = Array.prototype.indexOf.call(rows, row) - 1;
|
||||||
|
var start = idx - 5;
|
||||||
|
if (start < 0) return;
|
||||||
|
|
||||||
|
var ampVal = cell.textContent.trim();
|
||||||
|
document.getElementById('drawer-title').textContent = product;
|
||||||
|
document.getElementById('drawer-date').textContent = '目标日期: ' + ranges[idx].date + ' ' + ranges[idx].wk + ' 振幅 ' + ampVal;
|
||||||
|
|
||||||
|
var html = '<tr><td>日期</td><td>最高−最低</td></tr>';
|
||||||
|
var sum = 0;
|
||||||
|
for (var j = start; j < idx; j++) {
|
||||||
|
html += '<tr><td>' + ranges[j].date + ' ' + ranges[j].wk + '</td><td>' + ranges[j].diff + '</td></tr>';
|
||||||
|
sum += ranges[j].diff;
|
||||||
|
}
|
||||||
|
document.getElementById('drawer-table').innerHTML = html;
|
||||||
|
document.getElementById('drawer-result').innerHTML = '合计 <b>' + sum + '</b> ÷ 5 = <b>' + (sum / 5).toFixed(1) + '</b><br>四舍五入 → <b>' + Math.round(sum / 5) + '</b>';
|
||||||
|
|
||||||
|
document.getElementById('overlay').classList.add('show');
|
||||||
|
document.getElementById('drawer').classList.add('show');
|
||||||
|
|
||||||
|
if (activeRow) activeRow.classList.remove('active');
|
||||||
|
row.classList.add('active');
|
||||||
|
activeRow = row;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>振幅锁仓策略 — FG2610 玻璃</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #fff; --fg: #1e293b; --sub: #64748b;
|
||||||
|
--th-bg: #f1f5f9; --th-fg: #334155; --th-border: #cbd5e1;
|
||||||
|
--td-border: #e2e8f0; --td-even: #f8fafc; --td-first: #334155;
|
||||||
|
--na: #94a3b8;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--bg: #0f172a; --fg: #e2e8f0; --sub: #94a3b8;
|
||||||
|
--th-bg: #1e293b; --th-fg: #cbd5e1; --th-border: #334155;
|
||||||
|
--td-border: #1e293b; --td-even: #1a2332; --td-first: #e2e8f0;
|
||||||
|
--na: #64748b;
|
||||||
|
}
|
||||||
|
body { font-family: -apple-system, 'Segoe UI', system-ui, sans-serif; padding: 24px; max-width: 1100px; margin: 0 auto; color: var(--fg); background: var(--bg); }
|
||||||
|
h1 { font-size: 1.4rem; margin-bottom: 8px; }
|
||||||
|
.sub { color: var(--sub); font-size: 0.85rem; margin-bottom: 16px; }
|
||||||
|
.sub a { color: #3b82f6; cursor: pointer; text-decoration: none; }
|
||||||
|
.sub a:hover { text-decoration: underline; }
|
||||||
|
.formula { color: var(--sub); font-size: 0.8rem; margin: 0; padding: 6px 12px; background: var(--th-bg); border-radius: 4px; display: inline-block; }
|
||||||
|
table { border-collapse: collapse; width: 100%; font-size: 0.9rem; }
|
||||||
|
th { background: var(--th-bg); color: var(--th-fg); font-weight: 600; padding: 8px 10px; border: 1px solid var(--th-border); text-align: center; white-space: nowrap; }
|
||||||
|
td { padding: 6px 10px; border: 1px solid var(--td-border); text-align: center; }
|
||||||
|
tr:nth-child(even) td { background: var(--td-even); }
|
||||||
|
td:first-child { font-weight: 500; color: var(--td-first); }
|
||||||
|
tr.active td { background: #dbeafe !important; }
|
||||||
|
[data-theme="dark"] tr.active td { background: #1e3a5f !important; }
|
||||||
|
td:nth-child(2) { font-size: 0.78rem; color: var(--sub); }
|
||||||
|
.na { color: var(--na); }
|
||||||
|
|
||||||
|
.toggle-btn { position: fixed; top: 16px; right: 24px; background: var(--th-bg); border: 1px solid var(--th-border); color: var(--th-fg); padding: 6px 14px; border-radius: 6px; cursor: pointer; font-size: 0.82rem; z-index: 10; }
|
||||||
|
.toggle-btn:hover { opacity: 0.8; }
|
||||||
|
|
||||||
|
.section { margin-bottom: 8px; }
|
||||||
|
.section h2 { font-size: 1.1rem; display: flex; align-items: center; gap: 8px; margin: 16px 0 10px; padding: 4px 0; }
|
||||||
|
|
||||||
|
.drawer-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.35); z-index: 99; display: none; }
|
||||||
|
.drawer-overlay.show { display: block; }
|
||||||
|
.drawer { position: fixed; top: 0; right: 0; width: 340px; height: 100%; background: var(--bg); border-left: 1px solid var(--th-border); z-index: 100; padding: 24px; transform: translateX(100%); transition: transform .25s ease; overflow-y: auto; box-shadow: -4px 0 20px rgba(0,0,0,.15); }
|
||||||
|
.drawer.show { transform: translateX(0); }
|
||||||
|
.drawer h3 { font-size: 1rem; margin-bottom: 4px; }
|
||||||
|
.drawer .date-label { color: var(--sub); font-size: 0.82rem; margin-bottom: 16px; }
|
||||||
|
.drawer .calc-table { width: 100%; border-collapse: collapse; font-size: 0.82rem; margin-bottom: 14px; }
|
||||||
|
.drawer .calc-table td { padding: 4px 8px; border: 1px solid var(--td-border); text-align: center; }
|
||||||
|
.drawer .calc-table tr:first-child td { background: var(--th-bg); font-weight: 600; color: var(--th-fg); }
|
||||||
|
.drawer .result { font-size: 0.85rem; line-height: 1.8; color: var(--fg); }
|
||||||
|
.drawer .result b { font-size: 1.1rem; }
|
||||||
|
.drawer .close-btn { position: absolute; top: 16px; right: 20px; background: none; border: none; font-size: 1.4rem; cursor: pointer; color: var(--sub); line-height: 1; }
|
||||||
|
.drawer .close-btn:hover { color: var(--fg); }
|
||||||
|
.amp-cell { cursor: pointer; text-decoration: underline; text-decoration-style: dotted; text-underline-offset: 3px; }
|
||||||
|
.amp-cell:hover { color: #3b82f6; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<button class="toggle-btn" id="themeToggle">🌙 暗夜</button>
|
||||||
|
|
||||||
|
<h1>振幅锁仓策略 — FG2610 玻璃</h1>
|
||||||
|
<p class="sub"><a href="index.html">返回总览</a> | <a href="fg2609.html">FG2609</a> | <span>FG2610</span> | <a href="fg2611.html">FG2611</a> | <a href="fg2701.html">FG2701</a></p>
|
||||||
|
<p class="formula">振幅算法:近 5 日均振幅 = 过去 5 个交易日 (最高 − 最低) 的均值,结果四舍五入取整</p>
|
||||||
|
|
||||||
|
<div class="section" id="sec-fg2610">
|
||||||
|
<h2>FG2610 玻璃</h2>
|
||||||
|
<table>
|
||||||
|
<tr><th>日期</th><th>星期</th><th>开盘</th><th>收盘</th><th>最高</th><th>最低</th><th>最高最低差价</th><th>振幅</th></tr>
|
||||||
|
<tr><td>2026/7/1</td><td>周三</td><td>1015</td><td>1007</td><td>1015</td><td>1000</td><td>15</td><td class="na">-</td></tr>
|
||||||
|
<tr><td>2026/7/2</td><td>周四</td><td>1005</td><td>1007</td><td>1014</td><td>1000</td><td>14</td><td class="na">-</td></tr>
|
||||||
|
<tr><td>2026/7/3</td><td>周五</td><td>1007</td><td>1013</td><td>1021</td><td>1005</td><td>16</td><td class="na">-</td></tr>
|
||||||
|
<tr><td>2026/7/6</td><td>周一</td><td>1015</td><td>1007</td><td>1021</td><td>1006</td><td>15</td><td class="na">-</td></tr>
|
||||||
|
<tr><td>2026/7/7</td><td>周二</td><td>1007</td><td>991</td><td>1007</td><td>990</td><td>17</td><td class="na">-</td></tr>
|
||||||
|
<tr><td>2026/7/8</td><td>周三</td><td>994</td><td>999</td><td>1003</td><td>989</td><td>14</td><td class="amp-cell">15</td></tr>
|
||||||
|
<tr><td>2026/7/9</td><td>周四</td><td>1000</td><td>995</td><td>1000</td><td>989</td><td>11</td><td class="amp-cell">15</td></tr>
|
||||||
|
<tr><td>2026/7/10</td><td>周五</td><td>995</td><td>1001</td><td>1009</td><td>989</td><td>20</td><td class="amp-cell">15</td></tr>
|
||||||
|
<tr><td>2026/7/13</td><td>周一</td><td>1004</td><td>986</td><td>1004</td><td>986</td><td>18</td><td class="amp-cell">15</td></tr>
|
||||||
|
<tr><td>2026/7/14</td><td>周二</td><td>988</td><td>987</td><td>991</td><td>980</td><td>11</td><td class="amp-cell">16</td></tr>
|
||||||
|
<tr><td>2026/7/15</td><td>周三</td><td>987</td><td>985</td><td>992</td><td>981</td><td>11</td><td class="amp-cell">15</td></tr>
|
||||||
|
<tr><td>2026/7/16</td><td>周四</td><td>984</td><td>966</td><td>987</td><td>961</td><td>26</td><td class="amp-cell">14</td></tr>
|
||||||
|
<tr><td>2026/7/17</td><td>周五</td><td>967</td><td>935</td><td>967</td><td>934</td><td>33</td><td class="amp-cell">17</td></tr>
|
||||||
|
<tr><td>2026/7/20</td><td>周一</td><td>937</td><td>929</td><td>943</td><td>928</td><td>15</td><td class="amp-cell">20</td></tr>
|
||||||
|
<tr><td>2026/7/21</td><td>周二</td><td>930</td><td>942</td><td>948</td><td>930</td><td>18</td><td class="amp-cell">19</td></tr>
|
||||||
|
<tr><td>2026/7/22</td><td>周三</td><td>947</td><td>947</td><td>954</td><td>937</td><td>17</td><td class="amp-cell">21</td></tr>
|
||||||
|
<tr><td>2026/7/23</td><td>周四</td><td>948</td><td>937</td><td>951</td><td>931</td><td>20</td><td class="amp-cell">22</td></tr>
|
||||||
|
<tr><td>2026/7/24</td><td>周五</td><td>938</td><td>944</td><td>947</td><td>928</td><td>19</td><td class="amp-cell">21</td></tr>
|
||||||
|
<tr><td>2026/7/27</td><td>周一</td><td class="na">-</td><td class="na">-</td><td class="na">-</td><td class="na">-</td><td>0</td><td class="amp-cell">18</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-overlay" id="overlay" onclick="closeDrawer()"></div>
|
||||||
|
<div class="drawer" id="drawer">
|
||||||
|
<button class="close-btn" onclick="closeDrawer()">×</button>
|
||||||
|
<h3 id="drawer-title"></h3>
|
||||||
|
<p class="date-label" id="drawer-date"></p>
|
||||||
|
<table class="calc-table" id="drawer-table"></table>
|
||||||
|
<div class="result" id="drawer-result"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var themeBtn = document.getElementById('themeToggle');
|
||||||
|
themeBtn.addEventListener('click', toggleTheme);
|
||||||
|
function setTheme(dark) {
|
||||||
|
if (dark) {
|
||||||
|
document.documentElement.setAttribute('data-theme', 'dark');
|
||||||
|
themeBtn.textContent = '☀️ 日间';
|
||||||
|
} else {
|
||||||
|
document.documentElement.removeAttribute('data-theme');
|
||||||
|
themeBtn.textContent = '🌙 暗夜';
|
||||||
|
}
|
||||||
|
try { localStorage.setItem('ft-theme', dark ? 'dark' : 'light'); } catch(e) {}
|
||||||
|
}
|
||||||
|
function toggleTheme() {
|
||||||
|
setTheme(document.documentElement.getAttribute('data-theme') !== 'dark');
|
||||||
|
}
|
||||||
|
(function() {
|
||||||
|
try {
|
||||||
|
var s = localStorage.getItem('ft-theme');
|
||||||
|
if (s === 'dark' || (!s && matchMedia('(prefers-color-scheme: dark)').matches)) setTheme(true);
|
||||||
|
} catch(e) {}
|
||||||
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
var activeRow = null;
|
||||||
|
function closeDrawer() {
|
||||||
|
document.getElementById('overlay').classList.remove('show');
|
||||||
|
document.getElementById('drawer').classList.remove('show');
|
||||||
|
if (activeRow) { activeRow.classList.remove('active'); activeRow = null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('.amp-cell').forEach(function(cell) {
|
||||||
|
cell.addEventListener('click', function() {
|
||||||
|
var row = cell.parentElement;
|
||||||
|
var tbl = row.closest('table');
|
||||||
|
var rows = tbl.querySelectorAll('tr');
|
||||||
|
var h2 = tbl.closest('.section').querySelector('h2');
|
||||||
|
var product = h2.childNodes[h2.childNodes.length - 1].textContent.trim();
|
||||||
|
|
||||||
|
var ranges = [];
|
||||||
|
for (var i = 1; i < rows.length; i++) {
|
||||||
|
var cells = rows[i].querySelectorAll('td');
|
||||||
|
ranges.push({
|
||||||
|
date: cells[0].textContent.trim(),
|
||||||
|
wk: cells[1].textContent.trim(),
|
||||||
|
diff: parseInt(cells[6].textContent) || 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var idx = Array.prototype.indexOf.call(rows, row) - 1;
|
||||||
|
var start = idx - 5;
|
||||||
|
if (start < 0) return;
|
||||||
|
|
||||||
|
var ampVal = cell.textContent.trim();
|
||||||
|
document.getElementById('drawer-title').textContent = product;
|
||||||
|
document.getElementById('drawer-date').textContent = '目标日期: ' + ranges[idx].date + ' ' + ranges[idx].wk + ' 振幅 ' + ampVal;
|
||||||
|
|
||||||
|
var html = '<tr><td>日期</td><td>最高−最低</td></tr>';
|
||||||
|
var sum = 0;
|
||||||
|
for (var j = start; j < idx; j++) {
|
||||||
|
html += '<tr><td>' + ranges[j].date + ' ' + ranges[j].wk + '</td><td>' + ranges[j].diff + '</td></tr>';
|
||||||
|
sum += ranges[j].diff;
|
||||||
|
}
|
||||||
|
document.getElementById('drawer-table').innerHTML = html;
|
||||||
|
document.getElementById('drawer-result').innerHTML = '合计 <b>' + sum + '</b> ÷ 5 = <b>' + (sum / 5).toFixed(1) + '</b><br>四舍五入 → <b>' + Math.round(sum / 5) + '</b>';
|
||||||
|
|
||||||
|
document.getElementById('overlay').classList.add('show');
|
||||||
|
document.getElementById('drawer').classList.add('show');
|
||||||
|
|
||||||
|
if (activeRow) activeRow.classList.remove('active');
|
||||||
|
row.classList.add('active');
|
||||||
|
activeRow = row;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>振幅锁仓策略 — FG2611 玻璃</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #fff; --fg: #1e293b; --sub: #64748b;
|
||||||
|
--th-bg: #f1f5f9; --th-fg: #334155; --th-border: #cbd5e1;
|
||||||
|
--td-border: #e2e8f0; --td-even: #f8fafc; --td-first: #334155;
|
||||||
|
--na: #94a3b8;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--bg: #0f172a; --fg: #e2e8f0; --sub: #94a3b8;
|
||||||
|
--th-bg: #1e293b; --th-fg: #cbd5e1; --th-border: #334155;
|
||||||
|
--td-border: #1e293b; --td-even: #1a2332; --td-first: #e2e8f0;
|
||||||
|
--na: #64748b;
|
||||||
|
}
|
||||||
|
body { font-family: -apple-system, 'Segoe UI', system-ui, sans-serif; padding: 24px; max-width: 1100px; margin: 0 auto; color: var(--fg); background: var(--bg); }
|
||||||
|
h1 { font-size: 1.4rem; margin-bottom: 8px; }
|
||||||
|
.sub { color: var(--sub); font-size: 0.85rem; margin-bottom: 16px; }
|
||||||
|
.sub a { color: #3b82f6; cursor: pointer; text-decoration: none; }
|
||||||
|
.sub a:hover { text-decoration: underline; }
|
||||||
|
.formula { color: var(--sub); font-size: 0.8rem; margin: 0; padding: 6px 12px; background: var(--th-bg); border-radius: 4px; display: inline-block; }
|
||||||
|
table { border-collapse: collapse; width: 100%; font-size: 0.9rem; }
|
||||||
|
th { background: var(--th-bg); color: var(--th-fg); font-weight: 600; padding: 8px 10px; border: 1px solid var(--th-border); text-align: center; white-space: nowrap; }
|
||||||
|
td { padding: 6px 10px; border: 1px solid var(--td-border); text-align: center; }
|
||||||
|
tr:nth-child(even) td { background: var(--td-even); }
|
||||||
|
td:first-child { font-weight: 500; color: var(--td-first); }
|
||||||
|
tr.active td { background: #dbeafe !important; }
|
||||||
|
[data-theme="dark"] tr.active td { background: #1e3a5f !important; }
|
||||||
|
td:nth-child(2) { font-size: 0.78rem; color: var(--sub); }
|
||||||
|
.na { color: var(--na); }
|
||||||
|
|
||||||
|
.toggle-btn { position: fixed; top: 16px; right: 24px; background: var(--th-bg); border: 1px solid var(--th-border); color: var(--th-fg); padding: 6px 14px; border-radius: 6px; cursor: pointer; font-size: 0.82rem; z-index: 10; }
|
||||||
|
.toggle-btn:hover { opacity: 0.8; }
|
||||||
|
|
||||||
|
.amp-cell { cursor: pointer; text-decoration: underline; text-decoration-style: dotted; text-underline-offset: 3px; }
|
||||||
|
.amp-cell:hover { color: #3b82f6; }
|
||||||
|
|
||||||
|
.drawer-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.35); z-index: 99; display: none; }
|
||||||
|
.drawer-overlay.show { display: block; }
|
||||||
|
.drawer { position: fixed; top: 0; right: 0; width: 340px; height: 100%; background: var(--bg); border-left: 1px solid var(--th-border); z-index: 100; padding: 24px; transform: translateX(100%); transition: transform .25s ease; overflow-y: auto; box-shadow: -4px 0 20px rgba(0,0,0,.15); }
|
||||||
|
.drawer.show { transform: translateX(0); }
|
||||||
|
.drawer h3 { font-size: 1rem; margin-bottom: 4px; }
|
||||||
|
.drawer .date-label { color: var(--sub); font-size: 0.82rem; margin-bottom: 16px; }
|
||||||
|
.drawer .calc-table { width: 100%; border-collapse: collapse; font-size: 0.82rem; margin-bottom: 14px; }
|
||||||
|
.drawer .calc-table td { padding: 4px 8px; border: 1px solid var(--td-border); text-align: center; }
|
||||||
|
.drawer .calc-table tr:first-child td { background: var(--th-bg); font-weight: 600; color: var(--th-fg); }
|
||||||
|
.drawer .result { font-size: 0.85rem; line-height: 1.8; color: var(--fg); }
|
||||||
|
.drawer .result b { font-size: 1.1rem; }
|
||||||
|
.drawer .close-btn { position: absolute; top: 16px; right: 20px; background: none; border: none; font-size: 1.4rem; cursor: pointer; color: var(--sub); line-height: 1; }
|
||||||
|
.drawer .close-btn:hover { color: var(--fg); }
|
||||||
|
|
||||||
|
.section { margin-bottom: 8px; }
|
||||||
|
.section h2 { font-size: 1.1rem; display: flex; align-items: center; gap: 8px; margin: 16px 0 10px; padding: 4px 0; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<button class="toggle-btn" id="themeToggle">🌙 暗夜</button>
|
||||||
|
|
||||||
|
<h1>振幅锁仓策略 — 玻璃 FG2611</h1>
|
||||||
|
<p class="sub"><a href="index.html">返回总览</a> | <a href="fg2609.html">FG2609</a> | <a href="fg2610.html">FG2610</a> | <span>FG2611</span> | <a href="fg2701.html">FG2701</a></p>
|
||||||
|
<p class="formula">振幅算法:近 5 日均振幅 = 过去 5 个交易日 (最高 − 最低) 的均值,结果四舍五入取整</p>
|
||||||
|
|
||||||
|
<div class="section" id="sec-fg2611">
|
||||||
|
<h2> FG2611 玻璃</h2>
|
||||||
|
<table>
|
||||||
|
<tr><th>日期</th><th>星期</th><th>开盘</th><th>收盘</th><th>最高</th><th>最低</th><th>最高最低差价</th><th>振幅</th></tr>
|
||||||
|
<tr><td>2026/7/1</td><td>周三</td><td>1011</td><td>1001</td><td>1011</td><td>994</td><td>17</td><td class="na">-</td></tr>
|
||||||
|
<tr><td>2026/7/2</td><td>周四</td><td>1001</td><td>1002</td><td>1008</td><td>994</td><td>14</td><td class="na">-</td></tr>
|
||||||
|
<tr><td>2026/7/3</td><td>周五</td><td>1002</td><td>1010</td><td>1017</td><td>999</td><td>18</td><td class="na">-</td></tr>
|
||||||
|
<tr><td>2026/7/6</td><td>周一</td><td>1013</td><td>1003</td><td>1019</td><td>1002</td><td>17</td><td class="na">-</td></tr>
|
||||||
|
<tr><td>2026/7/7</td><td>周二</td><td>1003</td><td>984</td><td>1003</td><td>983</td><td>20</td><td class="na">-</td></tr>
|
||||||
|
<tr><td>2026/7/8</td><td>周三</td><td>984</td><td>993</td><td>996</td><td>981</td><td>15</td><td class="amp-cell">17</td></tr>
|
||||||
|
<tr><td>2026/7/9</td><td>周四</td><td>994</td><td>987</td><td>994</td><td>981</td><td>13</td><td class="amp-cell">17</td></tr>
|
||||||
|
<tr><td>2026/7/10</td><td>周五</td><td>986</td><td>996</td><td>1004</td><td>983</td><td>21</td><td class="amp-cell">17</td></tr>
|
||||||
|
<tr><td>2026/7/13</td><td>周一</td><td>996</td><td>980</td><td>1001</td><td>980</td><td>21</td><td class="amp-cell">17</td></tr>
|
||||||
|
<tr><td>2026/7/14</td><td>周二</td><td>982</td><td>983</td><td>986</td><td>974</td><td>12</td><td class="amp-cell">18</td></tr>
|
||||||
|
<tr><td>2026/7/15</td><td>周三</td><td>982</td><td>979</td><td>988</td><td>975</td><td>13</td><td class="amp-cell">16</td></tr>
|
||||||
|
<tr><td>2026/7/16</td><td>周四</td><td>979</td><td>960</td><td>982</td><td>957</td><td>25</td><td class="amp-cell">16</td></tr>
|
||||||
|
<tr><td>2026/7/17</td><td>周五</td><td>962</td><td>932</td><td>962</td><td>931</td><td>31</td><td class="amp-cell">18</td></tr>
|
||||||
|
<tr><td>2026/7/20</td><td>周一</td><td>934</td><td>926</td><td>939</td><td>924</td><td>15</td><td class="amp-cell">20</td></tr>
|
||||||
|
<tr><td>2026/7/21</td><td>周二</td><td>929</td><td>934</td><td>941</td><td>926</td><td>15</td><td class="amp-cell">19</td></tr>
|
||||||
|
<tr><td>2026/7/22</td><td>周三</td><td>936</td><td>939</td><td>945</td><td>929</td><td>16</td><td class="amp-cell">20</td></tr>
|
||||||
|
<tr><td>2026/7/23</td><td>周四</td><td>938</td><td>931</td><td>941</td><td>925</td><td>16</td><td class="amp-cell">20</td></tr>
|
||||||
|
<tr><td>2026/7/24</td><td>周五</td><td>931</td><td>939</td><td>941</td><td>922</td><td>19</td><td class="amp-cell">19</td></tr>
|
||||||
|
<tr><td>2026/7/27</td><td>周一</td><td class="na">-</td><td class="na">-</td><td class="na">-</td><td class="na">-</td><td>0</td><td class="amp-cell">16</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-overlay" id="overlay" onclick="closeDrawer()"></div>
|
||||||
|
<div class="drawer" id="drawer">
|
||||||
|
<button class="close-btn" onclick="closeDrawer()">×</button>
|
||||||
|
<h3 id="drawer-title"></h3>
|
||||||
|
<p class="date-label" id="drawer-date"></p>
|
||||||
|
<table class="calc-table" id="drawer-table"></table>
|
||||||
|
<div class="result" id="drawer-result"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var themeBtn = document.getElementById('themeToggle');
|
||||||
|
themeBtn.addEventListener('click', toggleTheme);
|
||||||
|
function setTheme(dark) {
|
||||||
|
if (dark) {
|
||||||
|
document.documentElement.setAttribute('data-theme', 'dark');
|
||||||
|
themeBtn.textContent = '☀️ 日间';
|
||||||
|
} else {
|
||||||
|
document.documentElement.removeAttribute('data-theme');
|
||||||
|
themeBtn.textContent = '🌙 暗夜';
|
||||||
|
}
|
||||||
|
try { localStorage.setItem('ft-theme', dark ? 'dark' : 'light'); } catch(e) {}
|
||||||
|
}
|
||||||
|
function toggleTheme() {
|
||||||
|
setTheme(document.documentElement.getAttribute('data-theme') !== 'dark');
|
||||||
|
}
|
||||||
|
(function() {
|
||||||
|
try {
|
||||||
|
var s = localStorage.getItem('ft-theme');
|
||||||
|
if (s === 'dark' || (!s && matchMedia('(prefers-color-scheme: dark)').matches)) setTheme(true);
|
||||||
|
} catch(e) {}
|
||||||
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
var activeRow = null;
|
||||||
|
function closeDrawer() {
|
||||||
|
document.getElementById('overlay').classList.remove('show');
|
||||||
|
document.getElementById('drawer').classList.remove('show');
|
||||||
|
if (activeRow) { activeRow.classList.remove('active'); activeRow = null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('.amp-cell').forEach(function(cell) {
|
||||||
|
cell.addEventListener('click', function() {
|
||||||
|
var row = cell.parentElement;
|
||||||
|
var tbl = row.closest('table');
|
||||||
|
var rows = tbl.querySelectorAll('tr');
|
||||||
|
var h2 = tbl.closest('.section').querySelector('h2');
|
||||||
|
var product = h2.childNodes[h2.childNodes.length - 1].textContent.trim();
|
||||||
|
|
||||||
|
var ranges = [];
|
||||||
|
for (var i = 1; i < rows.length; i++) {
|
||||||
|
var cells = rows[i].querySelectorAll('td');
|
||||||
|
ranges.push({
|
||||||
|
date: cells[0].textContent.trim(),
|
||||||
|
wk: cells[1].textContent.trim(),
|
||||||
|
diff: parseInt(cells[6].textContent) || 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var idx = Array.prototype.indexOf.call(rows, row) - 1;
|
||||||
|
var start = idx - 5;
|
||||||
|
if (start < 0) return;
|
||||||
|
|
||||||
|
var ampVal = cell.textContent.trim();
|
||||||
|
document.getElementById('drawer-title').textContent = product;
|
||||||
|
document.getElementById('drawer-date').textContent = '目标日期: ' + ranges[idx].date + ' ' + ranges[idx].wk + ' 振幅 ' + ampVal;
|
||||||
|
|
||||||
|
var html = '<tr><td>日期</td><td>最高−最低</td></tr>';
|
||||||
|
var sum = 0;
|
||||||
|
for (var j = start; j < idx; j++) {
|
||||||
|
html += '<tr><td>' + ranges[j].date + ' ' + ranges[j].wk + '</td><td>' + ranges[j].diff + '</td></tr>';
|
||||||
|
sum += ranges[j].diff;
|
||||||
|
}
|
||||||
|
document.getElementById('drawer-table').innerHTML = html;
|
||||||
|
document.getElementById('drawer-result').innerHTML = '合计 <b>' + sum + '</b> ÷ 5 = <b>' + (sum / 5).toFixed(1) + '</b><br>四舍五入 → <b>' + Math.round(sum / 5) + '</b>';
|
||||||
|
|
||||||
|
document.getElementById('overlay').classList.add('show');
|
||||||
|
document.getElementById('drawer').classList.add('show');
|
||||||
|
|
||||||
|
if (activeRow) activeRow.classList.remove('active');
|
||||||
|
row.classList.add('active');
|
||||||
|
activeRow = row;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>振幅锁仓策略 — 玻璃 FG2701</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #fff; --fg: #1e293b; --sub: #64748b;
|
||||||
|
--th-bg: #f1f5f9; --th-fg: #334155; --th-border: #cbd5e1;
|
||||||
|
--td-border: #e2e8f0; --td-even: #f8fafc; --td-first: #334155;
|
||||||
|
--na: #94a3b8;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--bg: #0f172a; --fg: #e2e8f0; --sub: #94a3b8;
|
||||||
|
--th-bg: #1e293b; --th-fg: #cbd5e1; --th-border: #334155;
|
||||||
|
--td-border: #1e293b; --td-even: #1a2332; --td-first: #e2e8f0;
|
||||||
|
--na: #64748b;
|
||||||
|
}
|
||||||
|
body { font-family: -apple-system, 'Segoe UI', system-ui, sans-serif; padding: 24px; max-width: 1100px; margin: 0 auto; color: var(--fg); background: var(--bg); }
|
||||||
|
h1 { font-size: 1.4rem; margin-bottom: 8px; }
|
||||||
|
.sub { color: var(--sub); font-size: 0.85rem; margin-bottom: 16px; }
|
||||||
|
.sub a { color: #3b82f6; cursor: pointer; text-decoration: none; }
|
||||||
|
.sub a:hover { text-decoration: underline; }
|
||||||
|
.formula { color: var(--sub); font-size: 0.8rem; margin: 0; padding: 6px 12px; background: var(--th-bg); border-radius: 4px; display: inline-block; }
|
||||||
|
table { border-collapse: collapse; width: 100%; font-size: 0.9rem; }
|
||||||
|
th { background: var(--th-bg); color: var(--th-fg); font-weight: 600; padding: 8px 10px; border: 1px solid var(--th-border); text-align: center; white-space: nowrap; }
|
||||||
|
td { padding: 6px 10px; border: 1px solid var(--td-border); text-align: center; }
|
||||||
|
tr:nth-child(even) td { background: var(--td-even); }
|
||||||
|
td:first-child { font-weight: 500; color: var(--td-first); }
|
||||||
|
tr.active td { background: #dbeafe !important; }
|
||||||
|
[data-theme="dark"] tr.active td { background: #1e3a5f !important; }
|
||||||
|
td:nth-child(2) { font-size: 0.78rem; color: var(--sub); }
|
||||||
|
.na { color: var(--na); }
|
||||||
|
|
||||||
|
.toggle-btn { position: fixed; top: 16px; right: 24px; background: var(--th-bg); border: 1px solid var(--th-border); color: var(--th-fg); padding: 6px 14px; border-radius: 6px; cursor: pointer; font-size: 0.82rem; z-index: 10; }
|
||||||
|
.toggle-btn:hover { opacity: 0.8; }
|
||||||
|
|
||||||
|
.drawer-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.35); z-index: 99; display: none; }
|
||||||
|
.drawer-overlay.show { display: block; }
|
||||||
|
.drawer { position: fixed; top: 0; right: 0; width: 340px; height: 100%; background: var(--bg); border-left: 1px solid var(--th-border); z-index: 100; padding: 24px; transform: translateX(100%); transition: transform .25s ease; overflow-y: auto; box-shadow: -4px 0 20px rgba(0,0,0,.15); }
|
||||||
|
.drawer.show { transform: translateX(0); }
|
||||||
|
.drawer h3 { font-size: 1rem; margin-bottom: 4px; }
|
||||||
|
.drawer .date-label { color: var(--sub); font-size: 0.82rem; margin-bottom: 16px; }
|
||||||
|
.drawer .calc-table { width: 100%; border-collapse: collapse; font-size: 0.82rem; margin-bottom: 14px; }
|
||||||
|
.drawer .calc-table td { padding: 4px 8px; border: 1px solid var(--td-border); text-align: center; }
|
||||||
|
.drawer .calc-table tr:first-child td { background: var(--th-bg); font-weight: 600; color: var(--th-fg); }
|
||||||
|
.drawer .result { font-size: 0.85rem; line-height: 1.8; color: var(--fg); }
|
||||||
|
.drawer .result b { font-size: 1.1rem; }
|
||||||
|
.drawer .close-btn { position: absolute; top: 16px; right: 20px; background: none; border: none; font-size: 1.4rem; cursor: pointer; color: var(--sub); line-height: 1; }
|
||||||
|
.drawer .close-btn:hover { color: var(--fg); }
|
||||||
|
.amp-cell { cursor: pointer; text-decoration: underline; text-decoration-style: dotted; text-underline-offset: 3px; }
|
||||||
|
.amp-cell:hover { color: #3b82f6; }
|
||||||
|
.section { margin-bottom: 8px; }
|
||||||
|
.section h2 { font-size: 1.1rem; display: flex; align-items: center; gap: 8px; margin: 16px 0 10px; padding: 4px 0; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<button class="toggle-btn" id="themeToggle">🌙 暗夜</button>
|
||||||
|
|
||||||
|
<h1>振幅锁仓策略 — FG2701</h1>
|
||||||
|
<p class="sub"><a href="index.html">返回总览</a> | <a href="fg2609.html">FG2609</a> | <a href="fg2610.html">FG2610</a> | <a href="fg2611.html">FG2611</a> | <span>FG2701</span></p>
|
||||||
|
<p class="formula">振幅算法:近 5 日均振幅 = 过去 5 个交易日 (最高 − 最低) 的均值,结果四舍五入取整</p>
|
||||||
|
|
||||||
|
<div class="section" id="sec-fg2701">
|
||||||
|
<h2>FG2701 玻璃</h2>
|
||||||
|
<table>
|
||||||
|
<tr><th>日期</th><th>星期</th><th>开盘</th><th>收盘</th><th>最高</th><th>最低</th><th>最高最低差价</th><th>振幅</th></tr>
|
||||||
|
<tr><td>2026/7/1</td><td>周三</td><td>1059</td><td>1054</td><td>1062</td><td>1050</td><td>12</td><td class="na">-</td></tr>
|
||||||
|
<tr><td>2026/7/2</td><td>周四</td><td>1051</td><td>1055</td><td>1061</td><td>1050</td><td>11</td><td class="na">-</td></tr>
|
||||||
|
<tr><td>2026/7/3</td><td>周五</td><td>1053</td><td>1062</td><td>1070</td><td>1053</td><td>17</td><td class="na">-</td></tr>
|
||||||
|
<tr><td>2026/7/6</td><td>周一</td><td>1065</td><td>1058</td><td>1071</td><td>1056</td><td>15</td><td class="na">-</td></tr>
|
||||||
|
<tr><td>2026/7/7</td><td>周二</td><td>1056</td><td>1042</td><td>1058</td><td>1041</td><td>17</td><td class="na">-</td></tr>
|
||||||
|
<tr><td>2026/7/8</td><td>周三</td><td>1043</td><td>1048</td><td>1051</td><td>1036</td><td>15</td><td class="amp-cell">14</td></tr>
|
||||||
|
<tr><td>2026/7/9</td><td>周四</td><td>1048</td><td>1043</td><td>1049</td><td>1037</td><td>12</td><td class="amp-cell">15</td></tr>
|
||||||
|
<tr><td>2026/7/10</td><td>周五</td><td>1041</td><td>1048</td><td>1056</td><td>1039</td><td>17</td><td class="amp-cell">15</td></tr>
|
||||||
|
<tr><td>2026/7/13</td><td>周一</td><td>1049</td><td>1035</td><td>1050</td><td>1034</td><td>16</td><td class="amp-cell">15</td></tr>
|
||||||
|
<tr><td>2026/7/14</td><td>周二</td><td>1035</td><td>1035</td><td>1041</td><td>1031</td><td>10</td><td class="amp-cell">15</td></tr>
|
||||||
|
<tr><td>2026/7/15</td><td>周三</td><td>1035</td><td>1030</td><td>1038</td><td>1028</td><td>10</td><td class="amp-cell">14</td></tr>
|
||||||
|
<tr><td>2026/7/16</td><td>周四</td><td>1031</td><td>1016</td><td>1033</td><td>1014</td><td>19</td><td class="amp-cell">13</td></tr>
|
||||||
|
<tr><td>2026/7/17</td><td>周五</td><td>1017</td><td>989</td><td>1017</td><td>988</td><td>29</td><td class="amp-cell">14</td></tr>
|
||||||
|
<tr><td>2026/7/20</td><td>周一</td><td>989</td><td>985</td><td>996</td><td>983</td><td>13</td><td class="amp-cell">17</td></tr>
|
||||||
|
<tr><td>2026/7/21</td><td>周二</td><td>987</td><td>987</td><td>992</td><td>981</td><td>11</td><td class="amp-cell">16</td></tr>
|
||||||
|
<tr><td>2026/7/22</td><td>周三</td><td>991</td><td>987</td><td>993</td><td>977</td><td>16</td><td class="amp-cell">16</td></tr>
|
||||||
|
<tr><td>2026/7/23</td><td>周四</td><td>989</td><td>979</td><td>990</td><td>974</td><td>16</td><td class="amp-cell">18</td></tr>
|
||||||
|
<tr><td>2026/7/24</td><td>周五</td><td>978</td><td>985</td><td>987</td><td>972</td><td>15</td><td class="amp-cell">17</td></tr>
|
||||||
|
<tr><td>2026/7/27</td><td>周一</td><td class="na">-</td><td class="na">-</td><td class="na">-</td><td class="na">-</td><td>0</td><td class="amp-cell">14</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-overlay" id="overlay" onclick="closeDrawer()"></div>
|
||||||
|
<div class="drawer" id="drawer">
|
||||||
|
<button class="close-btn" onclick="closeDrawer()">×</button>
|
||||||
|
<h3 id="drawer-title"></h3>
|
||||||
|
<p class="date-label" id="drawer-date"></p>
|
||||||
|
<table class="calc-table" id="drawer-table"></table>
|
||||||
|
<div class="result" id="drawer-result"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var themeBtn = document.getElementById('themeToggle');
|
||||||
|
themeBtn.addEventListener('click', toggleTheme);
|
||||||
|
function setTheme(dark) {
|
||||||
|
if (dark) {
|
||||||
|
document.documentElement.setAttribute('data-theme', 'dark');
|
||||||
|
themeBtn.textContent = '☀️ 日间';
|
||||||
|
} else {
|
||||||
|
document.documentElement.removeAttribute('data-theme');
|
||||||
|
themeBtn.textContent = '🌙 暗夜';
|
||||||
|
}
|
||||||
|
try { localStorage.setItem('ft-theme', dark ? 'dark' : 'light'); } catch(e) {}
|
||||||
|
}
|
||||||
|
function toggleTheme() {
|
||||||
|
setTheme(document.documentElement.getAttribute('data-theme') !== 'dark');
|
||||||
|
}
|
||||||
|
(function() {
|
||||||
|
try {
|
||||||
|
var s = localStorage.getItem('ft-theme');
|
||||||
|
if (s === 'dark' || (!s && matchMedia('(prefers-color-scheme: dark)').matches)) setTheme(true);
|
||||||
|
} catch(e) {}
|
||||||
|
})();
|
||||||
|
|
||||||
|
var activeRow = null;
|
||||||
|
function closeDrawer() {
|
||||||
|
document.getElementById('overlay').classList.remove('show');
|
||||||
|
document.getElementById('drawer').classList.remove('show');
|
||||||
|
if (activeRow) { activeRow.classList.remove('active'); activeRow = null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('.amp-cell').forEach(function(cell) {
|
||||||
|
cell.addEventListener('click', function() {
|
||||||
|
var row = cell.parentElement;
|
||||||
|
var tbl = row.closest('table');
|
||||||
|
var rows = tbl.querySelectorAll('tr');
|
||||||
|
|
||||||
|
var ranges = [];
|
||||||
|
for (var i = 1; i < rows.length; i++) {
|
||||||
|
var cells = rows[i].querySelectorAll('td');
|
||||||
|
ranges.push({
|
||||||
|
date: cells[0].textContent.trim(),
|
||||||
|
wk: cells[1].textContent.trim(),
|
||||||
|
diff: parseInt(cells[6].textContent) || 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var idx = Array.prototype.indexOf.call(rows, row) - 1;
|
||||||
|
var start = idx - 5;
|
||||||
|
if (start < 0) return;
|
||||||
|
|
||||||
|
var ampVal = cell.textContent.trim();
|
||||||
|
var h2 = tbl.closest('.section').querySelector('h2');
|
||||||
|
document.getElementById('drawer-title').textContent = h2.childNodes[h2.childNodes.length - 1].textContent.trim();
|
||||||
|
document.getElementById('drawer-date').textContent = '目标日期: ' + ranges[idx].date + ' ' + ranges[idx].wk + ' 振幅 ' + ampVal;
|
||||||
|
|
||||||
|
var html = '<tr><td>日期</td><td>最高−最低</td></tr>';
|
||||||
|
var sum = 0;
|
||||||
|
for (var j = start; j < idx; j++) {
|
||||||
|
html += '<tr><td>' + ranges[j].date + ' ' + ranges[j].wk + '</td><td>' + ranges[j].diff + '</td></tr>';
|
||||||
|
sum += ranges[j].diff;
|
||||||
|
}
|
||||||
|
document.getElementById('drawer-table').innerHTML = html;
|
||||||
|
document.getElementById('drawer-result').innerHTML = '合计 <b>' + sum + '</b> ÷ 5 = <b>' + (sum / 5).toFixed(1) + '</b><br>四舍五入 → <b>' + Math.round(sum / 5) + '</b>';
|
||||||
|
|
||||||
|
document.getElementById('overlay').classList.add('show');
|
||||||
|
document.getElementById('drawer').classList.add('show');
|
||||||
|
|
||||||
|
if (activeRow) activeRow.classList.remove('active');
|
||||||
|
row.classList.add('active');
|
||||||
|
activeRow = row;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>振幅锁仓策略</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #fff; --fg: #1e293b; --sub: #64748b;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--bg: #0f172a; --fg: #e2e8f0; --sub: #94a3b8;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, 'Segoe UI', system-ui, sans-serif; padding: 48px 24px;
|
||||||
|
max-width: 600px; margin: 0 auto; color: var(--fg); background: var(--bg);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
h1 { font-size: 1.6rem; margin-bottom: 8px; }
|
||||||
|
.sub { color: var(--sub); font-size: 0.85rem; margin-bottom: 40px; }
|
||||||
|
.card {
|
||||||
|
display: block; padding: 24px; margin: 16px 0; border-radius: 12px;
|
||||||
|
text-decoration: none; font-size: 1.1rem; font-weight: 600;
|
||||||
|
transition: transform .15s, box-shadow .15s;
|
||||||
|
border: 1px solid var(--sub);
|
||||||
|
}
|
||||||
|
.card:hover { transform: translateY(-2px); }
|
||||||
|
.card { color: #059669; border-color: #a7f3d0; background: #ecfdf5; }
|
||||||
|
[data-theme="dark"] .card { color: #6ee7b7; border-color: #064e3b; background: #0a1f17; }
|
||||||
|
.card .desc { font-size: 0.8rem; font-weight: 400; color: var(--sub); margin-top: 4px; }
|
||||||
|
.toggle-btn {
|
||||||
|
position: fixed; top: 16px; right: 24px; background: var(--bg);
|
||||||
|
border: 1px solid var(--sub); color: var(--fg);
|
||||||
|
padding: 6px 14px; border-radius: 6px; cursor: pointer; font-size: 0.82rem; z-index: 10;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<button class="toggle-btn" id="themeToggle">🌙 暗夜</button>
|
||||||
|
|
||||||
|
<h1>振幅锁仓策略 — 玻璃</h1>
|
||||||
|
<p class="sub">2026年7月 · 郑商所</p>
|
||||||
|
|
||||||
|
<a class="card" href="fg2609.html">FG2609 <div class="desc">当前主力</div></a>
|
||||||
|
<a class="card" href="fg2610.html">FG2610 <div class="desc">次主力</div></a>
|
||||||
|
<a class="card" href="fg2611.html">FG2611 <div class="desc">次主力</div></a>
|
||||||
|
<a class="card" href="fg2701.html">FG2701 <div class="desc">远月合约</div></a>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var themeBtn = document.getElementById('themeToggle');
|
||||||
|
themeBtn.addEventListener('click', toggleTheme);
|
||||||
|
function setTheme(dark) {
|
||||||
|
if (dark) {
|
||||||
|
document.documentElement.setAttribute('data-theme', 'dark');
|
||||||
|
themeBtn.textContent = '☀️ 日间';
|
||||||
|
} else {
|
||||||
|
document.documentElement.removeAttribute('data-theme');
|
||||||
|
themeBtn.textContent = '🌙 暗夜';
|
||||||
|
}
|
||||||
|
try { localStorage.setItem('ft-theme', dark ? 'dark' : 'light'); } catch(e) {}
|
||||||
|
}
|
||||||
|
function toggleTheme() {
|
||||||
|
setTheme(document.documentElement.getAttribute('data-theme') !== 'dark');
|
||||||
|
}
|
||||||
|
(function() {
|
||||||
|
try {
|
||||||
|
var s = localStorage.getItem('ft-theme');
|
||||||
|
if (s === 'dark' || (!s && matchMedia('(prefers-color-scheme: dark)').matches)) setTheme(true);
|
||||||
|
} catch(e) {}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,356 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>振幅锁仓策略 · 操作流程</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #fff; --fg: #1e293b; --sub: #64748b;
|
||||||
|
--th-bg: #f1f5f9; --th-fg: #334155; --th-border: #cbd5e1;
|
||||||
|
--td-border: #e2e8f0; --td-even: #f8fafc;
|
||||||
|
--code-bg: #f1f5f9; --blockquote-bg: #f8fafc; --blockquote-border: #e2e8f0;
|
||||||
|
--hr-color: #e2e8f0; --link: #3b82f6;
|
||||||
|
--tag-bg: #dbeafe; --tag-fg: #1e40af;
|
||||||
|
--red-tag-bg: #fee2e2; --red-tag-fg: #991b1b;
|
||||||
|
--green-tag-bg: #dcfce7; --green-tag-fg: #166534;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--bg: #0f172a; --fg: #e2e8f0; --sub: #94a3b8;
|
||||||
|
--th-bg: #1e293b; --th-fg: #cbd5e1; --th-border: #334155;
|
||||||
|
--td-border: #1e293b; --td-even: #1a2332;
|
||||||
|
--code-bg: #1e293b; --blockquote-bg: #1a2332; --blockquote-border: #334155;
|
||||||
|
--hr-color: #334155; --link: #60a5fa;
|
||||||
|
--tag-bg: #1e3a5f; --tag-fg: #93c5fd;
|
||||||
|
--red-tag-bg: #3b1c1c; --red-tag-fg: #fca5a5;
|
||||||
|
--green-tag-bg: #1c3b1c; --green-tag-fg: #86efac;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, 'Segoe UI', system-ui, sans-serif;
|
||||||
|
padding: 24px; max-width: 860px; margin: 0 auto;
|
||||||
|
color: var(--fg); background: var(--bg); line-height: 1.75;
|
||||||
|
}
|
||||||
|
h1 { font-size: 1.5rem; margin-bottom: 4px; }
|
||||||
|
h2 { font-size: 1.2rem; margin: 36px 0 14px; padding-bottom: 6px; border-bottom: 1px solid var(--hr-color); }
|
||||||
|
h3 { font-size: 1.05rem; margin: 24px 0 10px; color: var(--th-fg); }
|
||||||
|
p { margin: 12px 0; }
|
||||||
|
a { color: var(--link); text-decoration: none; }
|
||||||
|
a:hover { text-decoration: underline; }
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: var(--sub); font-size: 0.88rem; margin-bottom: 28px;
|
||||||
|
padding: 8px 14px; background: var(--blockquote-bg);
|
||||||
|
border-left: 3px solid var(--blockquote-border); border-radius: 0 4px 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr { border: none; border-top: 1px solid var(--hr-color); margin: 28px 0; }
|
||||||
|
|
||||||
|
/* tables */
|
||||||
|
table {
|
||||||
|
border-collapse: collapse; width: 100%; font-size: 0.88rem;
|
||||||
|
margin: 14px 0 20px;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background: var(--th-bg); color: var(--th-fg); font-weight: 600;
|
||||||
|
padding: 8px 12px; border: 1px solid var(--th-border); text-align: center; white-space: nowrap;
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
padding: 7px 12px; border: 1px solid var(--td-border); text-align: center;
|
||||||
|
}
|
||||||
|
tr:nth-child(even) td { background: var(--td-even); }
|
||||||
|
td:first-child { font-weight: 500; }
|
||||||
|
|
||||||
|
/* code */
|
||||||
|
code {
|
||||||
|
background: var(--code-bg); padding: 2px 6px; border-radius: 3px;
|
||||||
|
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; font-size: 0.85em;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
background: var(--code-bg); padding: 14px 18px; border-radius: 6px;
|
||||||
|
overflow-x: auto; font-size: 0.84rem; line-height: 1.6;
|
||||||
|
border: 1px solid var(--td-border);
|
||||||
|
}
|
||||||
|
pre code { background: none; padding: 0; }
|
||||||
|
|
||||||
|
/* blockquote */
|
||||||
|
blockquote {
|
||||||
|
margin: 12px 0; padding: 10px 18px;
|
||||||
|
background: var(--blockquote-bg); border-left: 3px solid var(--blockquote-border);
|
||||||
|
border-radius: 0 4px 4px 0; color: var(--sub); font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
blockquote p { margin: 4px 0; }
|
||||||
|
|
||||||
|
/* ordered & unordered lists */
|
||||||
|
ol, ul { padding-left: 24px; margin: 10px 0; }
|
||||||
|
li { margin: 6px 0; }
|
||||||
|
|
||||||
|
/* condition tags */
|
||||||
|
.tag { display: inline-block; padding: 2px 8px; border-radius: 3px; font-size: 0.82rem; font-weight: 600; }
|
||||||
|
.tag-ok { background: var(--green-tag-bg); color: var(--green-tag-fg); }
|
||||||
|
.tag-no { background: var(--red-tag-bg); color: var(--red-tag-fg); }
|
||||||
|
|
||||||
|
/* event emoji labels */
|
||||||
|
.label-win { color: #16a34a; }
|
||||||
|
.label-lock { color: #d97706; }
|
||||||
|
.label-cut { color: #dc2626; }
|
||||||
|
|
||||||
|
/* theme toggle */
|
||||||
|
.toggle-btn {
|
||||||
|
position: fixed; top: 16px; right: 24px;
|
||||||
|
background: var(--th-bg); border: 1px solid var(--th-border); color: var(--th-fg);
|
||||||
|
padding: 6px 14px; border-radius: 6px; cursor: pointer; font-size: 0.82rem; z-index: 10;
|
||||||
|
}
|
||||||
|
.toggle-btn:hover { opacity: 0.8; }
|
||||||
|
|
||||||
|
/* responsive */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
body { padding: 16px; }
|
||||||
|
pre { padding: 10px 12px; font-size: 0.78rem; }
|
||||||
|
table { font-size: 0.78rem; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<button class="toggle-btn" id="themeToggle">🌙 暗夜</button>
|
||||||
|
|
||||||
|
<h1>振幅锁仓策略 · 操作流程</h1>
|
||||||
|
<p class="subtitle">
|
||||||
|
玻璃期货 FG 主力合约 · 每日累仓做空 + 振幅止盈 + 3 锁熔断
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════ -->
|
||||||
|
<h2>一、策略思想</h2>
|
||||||
|
<p>
|
||||||
|
玻璃期货受地产下行拖累,长期空头趋势明确。不做方向判断,<strong>默认每天做空</strong>,
|
||||||
|
用近 5 日均振幅作为动态止盈/锁仓阈值。回调是利润,反弹锁仓扛,连扛 3 次认输出局。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════ -->
|
||||||
|
<h2>二、核心参数</h2>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr><th>参数</th><th>取值</th><th>说明</th></tr>
|
||||||
|
<tr><td>交易方向</td><td>空</td><td>每天开盘无条件做空</td></tr>
|
||||||
|
<tr><td>每日手数</td><td>1 手(20 吨)</td><td>可等比放大至 N 手/天</td></tr>
|
||||||
|
<tr><td>每点价值</td><td>20 元</td><td>玻璃期货合约规格</td></tr>
|
||||||
|
<tr><td>振幅阈值</td><td>近 5 日 (最高−最低) 均值</td><td>每日盘前重新计算</td></tr>
|
||||||
|
<tr><td>锁仓上限</td><td>3 次</td><td>累计触发后清仓重来</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<blockquote>
|
||||||
|
<p><strong>多手缩放</strong>:单合约每日开 N 手时,止盈阈值等比放大为 <strong>N × A</strong>,保持触发难度不变。锁仓阈值和熔断规则不受手数影响——每手空单仍按各自开仓时的 A 独立判断锁仓,累计 3 锁熔断不变。</p>
|
||||||
|
</blockquote>
|
||||||
|
|
||||||
|
<h3>仓位管理</h3>
|
||||||
|
<p>
|
||||||
|
<strong>总手数</strong>:<code>N = ⌊权益金(万元)⌋</code>,向下取整。
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>分配规则</strong>:按沉淀资金从大到小取前三个合约(假设 FG09 > FG11 > FG01),轮询分配 N 手:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr><th>N</th><th>FG09</th><th>FG11</th><th>FG01</th></tr>
|
||||||
|
<tr><td>1</td><td>1</td><td>—</td><td>—</td></tr>
|
||||||
|
<tr><td>2</td><td>1</td><td>1</td><td>—</td></tr>
|
||||||
|
<tr><td>3</td><td>1</td><td>1</td><td>1</td></tr>
|
||||||
|
<tr><td>4</td><td>2</td><td>1</td><td>1</td></tr>
|
||||||
|
<tr><td>5</td><td>2</td><td>2</td><td>1</td></tr>
|
||||||
|
<tr><td>6</td><td>2</td><td>2</td><td>2</td></tr>
|
||||||
|
<tr><td>7</td><td>3</td><td>2</td><td>2</td></tr>
|
||||||
|
<tr><td>…</td><td>…</td><td>…</td><td>…</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<blockquote>
|
||||||
|
<p>合约 p(按沉淀资金降序,p=1,2,3)分配 ⌊(N + 3 − p) / 3⌋ 手。各合约独立运行策略(独立 A、独立锁仓计数器),一轮熔断不影响其他合约。</p>
|
||||||
|
</blockquote>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════ -->
|
||||||
|
<h2>三、每日操作流程</h2>
|
||||||
|
|
||||||
|
<h3>盘前(1 分钟)</h3>
|
||||||
|
<ol>
|
||||||
|
<li>取前 5 个交易日的 (最高 − 最低),计算均值 → <strong>当日振幅阈值 A</strong></li>
|
||||||
|
<li>检查当前持仓状态:
|
||||||
|
<ul>
|
||||||
|
<li>无持仓 → 新轮次开始</li>
|
||||||
|
<li>有活跃空单 → 继续累仓</li>
|
||||||
|
<li>有锁仓 → 继续累仓(锁仓不阻止开新空单)</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>开盘</h3>
|
||||||
|
<p><strong>无条件开 N 手空单</strong>(N = 每日手数,默认 1),记录开仓价和该手对应的锁仓阈值(= 当日振幅阈值 A)。</p>
|
||||||
|
|
||||||
|
<pre><code>第 N 手空单: 开仓价 = 今日开盘价, 锁仓阈值 = 当日 A</code></pre>
|
||||||
|
|
||||||
|
<h3>盘中监控</h3>
|
||||||
|
<p>用日内最高价、最低价判断触发:</p>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr><th>条件</th><th>触发</th><th>动作</th></tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>总浮盈 ≥ N × A 点</strong>(N = 每日手数)</td>
|
||||||
|
<td><span class="label-win">🟢 止盈</span></td>
|
||||||
|
<td>全部清仓(活跃空单 + 锁仓对),获利了结,轮次结束</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>某空单亏损 ≥ 该手开仓时的振幅阈值</strong></td>
|
||||||
|
<td><span class="label-lock">🔒 锁仓</span></td>
|
||||||
|
<td>对该手开多单锁仓,冻结亏损。该手从"活跃"移入"锁仓"</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>累计锁仓 ≥ 3 次</strong></td>
|
||||||
|
<td><span class="label-cut">🔴 3 锁熔断</span></td>
|
||||||
|
<td>全部清仓(活跃空单按结算价平仓 + 锁仓对平仓),认输重来</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>以上均未触发</td>
|
||||||
|
<td>— 持仓</td>
|
||||||
|
<td>所有活跃空单和锁仓对继续持有至次日</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3>盘中判断细节</h3>
|
||||||
|
<ul>
|
||||||
|
<li>如果止盈和锁仓同时可能触发(日内振幅足够大),用<strong>收盘方向</strong>判断哪个先触发:收盘 > 开盘 → 锁仓先触发,反之止盈先触发。</li>
|
||||||
|
<li>当天新开的空单和昨天留下的活跃空单<strong>平等对待</strong>——任何一笔活跃空单亏损达到它自己的锁仓阈值,就锁那一笔。</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════ -->
|
||||||
|
<h2>四、锁仓机制</h2>
|
||||||
|
<p>锁仓 = 对亏损达到阈值的空单,<strong>额外开一手多单</strong>,形成锁仓对。</p>
|
||||||
|
|
||||||
|
<pre><code>锁仓对 = 空单@开仓价 + 多单@(开仓价 + 阈值)
|
||||||
|
锁仓对盈亏 = (开仓价 − 现价) + (现价 − (开仓价 + 阈值))
|
||||||
|
= −阈值 (固定,不再随价格变动)</code></pre>
|
||||||
|
|
||||||
|
<p><strong>一旦锁仓,该笔亏损被冻结,不会扩大。</strong> 但锁仓对也不贡献利润——只有活跃空单才能在价格下跌时产生收益。</p>
|
||||||
|
|
||||||
|
<p>锁仓对在两种情况下平仓:</p>
|
||||||
|
<ol>
|
||||||
|
<li>后续总浮盈触发止盈 → 和所有头寸一起清仓</li>
|
||||||
|
<li>累计 3 锁触发熔断 → 和所有头寸一起清仓</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════ -->
|
||||||
|
<h2>五、清仓规则</h2>
|
||||||
|
|
||||||
|
<h3><span class="label-win">🟢</span> 止盈清仓</h3>
|
||||||
|
<p><strong>触发条件</strong>:当日最低价对应的总浮盈 ≥ N × 当日振幅阈值 A(N = 每日手数)</p>
|
||||||
|
<p>总浮盈 = 所有活跃空单浮盈 + 所有锁仓对冻结盈亏</p>
|
||||||
|
<ul>
|
||||||
|
<li>清仓所有活跃空单(按 trigger 价格平仓)</li>
|
||||||
|
<li>清仓所有锁仓对(空单和多单同时平仓)</li>
|
||||||
|
<li>轮次结束,锁仓计数器归零,次日重新开始</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3><span class="label-cut">🔴</span> 3 锁熔断</h3>
|
||||||
|
<p><strong>触发条件</strong>:累计锁仓次数 ≥ 3(跨日累计,不清零直到轮次结束)</p>
|
||||||
|
<ul>
|
||||||
|
<li>活跃空单按当日结算价平仓</li>
|
||||||
|
<li>锁仓对同步平仓</li>
|
||||||
|
<li>认输出局,轮次结束,计数器归零</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════ -->
|
||||||
|
<h2>六、适用条件</h2>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr><th>条件</th><th>说明</th></tr>
|
||||||
|
<tr><td><span class="tag tag-ok">✅</span> 长期空头趋势</td><td>每日默认做空有正期望</td></tr>
|
||||||
|
<tr><td><span class="tag tag-ok">✅</span> 日均振幅 > 15 点</td><td>阈值足够覆盖交易成本</td></tr>
|
||||||
|
<tr><td><span class="tag tag-ok">✅</span> 无极端单边上涨</td><td>连续锁仓是最怕的场景</td></tr>
|
||||||
|
<tr><td><span class="tag tag-no">❌</span> 趋势反转(熊→牛)</td><td>会连续 3 锁出局,建议暂停</td></tr>
|
||||||
|
<tr><td><span class="tag tag-no">❌</span> 振幅 < 10 点</td><td>利润不够覆盖滑点</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════ -->
|
||||||
|
<h2>七、操作示例</h2>
|
||||||
|
<blockquote><p>以下示例均假设每点价值 20 元。</p></blockquote>
|
||||||
|
|
||||||
|
<h3>示例 1:顺利止盈(N=1)</h3>
|
||||||
|
<table>
|
||||||
|
<tr><th>时间</th><th>操作</th><th>价格</th><th>说明</th></tr>
|
||||||
|
<tr><td>第 1 天开盘</td><td>开空 1 手</td><td>1300</td><td>A=18,开仓</td></tr>
|
||||||
|
<tr><td>第 1 天收盘</td><td>持仓</td><td>1285</td><td>浮盈 15 点 < 18,不触发,留仓</td></tr>
|
||||||
|
<tr><td>第 2 天开盘</td><td>开空 1 手</td><td>1280</td><td>A=20</td></tr>
|
||||||
|
<tr><td>第 2 天盘中</td><td>—</td><td>1260</td><td>手1浮盈=40,手2浮盈=20,总浮盈=60 ≥ 20,<span class="label-win">🟢 止盈!</span></td></tr>
|
||||||
|
</table>
|
||||||
|
<p><strong>结果</strong>:利润 = 60 点 × 20 = <strong>+1,200 元</strong>。即使没锁仓,两手持仓让总浮盈更快达到阈值。</p>
|
||||||
|
|
||||||
|
<h3>示例 2:锁仓后止盈</h3>
|
||||||
|
<table>
|
||||||
|
<tr><th>时间</th><th>操作</th><th>价格</th><th>说明</th></tr>
|
||||||
|
<tr><td>第 1 天开盘</td><td>开空 1 手</td><td>1300</td><td>A=18</td></tr>
|
||||||
|
<tr><td>第 1 天盘中</td><td>价格反弹</td><td>1318</td><td>空单浮亏 = 18 ≥ 18,<span class="label-lock">🔒 锁仓!</span> 多单 @1318,冻结亏损 −18 点</td></tr>
|
||||||
|
<tr><td>第 2 天开盘</td><td>开空 1 手</td><td>1315</td><td>A=20,新活跃空单</td></tr>
|
||||||
|
<tr><td>第 2 天盘中</td><td>—</td><td>1277</td><td>活跃浮盈 = 1315−1277 = 38,总浮盈 = 38+(−18) = 20 ≥ 20,<span class="label-win">🟢 止盈!</span></td></tr>
|
||||||
|
</table>
|
||||||
|
<p><strong>结果</strong>:利润 = 20 点 × 20 = <strong>+400 元</strong>。锁仓冻结了 18 点亏损,活跃空单多跌了 20 点覆盖亏损后触发。</p>
|
||||||
|
|
||||||
|
<h3>示例 3:3 锁熔断</h3>
|
||||||
|
<table>
|
||||||
|
<tr><th>时间</th><th>操作</th><th>价格</th><th>说明</th></tr>
|
||||||
|
<tr><td>第 1 天</td><td>开空 → 锁仓</td><td>1300 → 1318</td><td>A=18,锁仓 1,冻结 −18</td></tr>
|
||||||
|
<tr><td>第 2 天</td><td>开空 → 锁仓</td><td>1315 → 1337</td><td>A=22,锁仓 2,冻结 −22</td></tr>
|
||||||
|
<tr><td>第 3 天</td><td>开空 → 锁仓</td><td>1330 → 1350</td><td>A=20,锁仓 3,<span class="label-cut">🔴 熔断!</span></td></tr>
|
||||||
|
</table>
|
||||||
|
<p><strong>结果</strong>:全部清仓。亏损 = (−18)+(−22)+(−20)+(活跃空单结算亏损) ≈ <strong>−1,200 元以上</strong>。连续 3 天反弹,认输出局。</p>
|
||||||
|
|
||||||
|
<h3>示例 4:多手缩放(N=2)</h3>
|
||||||
|
<table>
|
||||||
|
<tr><th>时间</th><th>操作</th><th>价格</th><th>说明</th></tr>
|
||||||
|
<tr><td>第 1 天开盘</td><td>开空 2 手</td><td>1300</td><td>A=18,止盈阈值 = 2×18 = 36 点</td></tr>
|
||||||
|
<tr><td>第 1 天盘中</td><td>—</td><td>1282</td><td>每手浮盈 = 18,总浮盈 = 36 ≥ 36,<span class="label-win">🟢 止盈!</span></td></tr>
|
||||||
|
</table>
|
||||||
|
<p><strong>结果</strong>:利润 = 36 点 × 20 = <strong>+720 元</strong>。相比 N=1(利润 360 元),翻倍但触发难度不变——每手仍只需跌 18 点。</p>
|
||||||
|
|
||||||
|
<h3>关键公式速查</h3>
|
||||||
|
<table>
|
||||||
|
<tr><th>场景</th><th>止盈阈值</th><th>触发时利润(恰卡阈值)</th></tr>
|
||||||
|
<tr><td>N 手/天,无锁仓</td><td>N × A</td><td>N × A × 20</td></tr>
|
||||||
|
<tr><td>N 手/天,K 个锁仓对</td><td>N × A</td><td>N × A × 20(锁仓亏损已内嵌在总浮盈中)</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// ── 暗夜模式 ──
|
||||||
|
var themeBtn = document.getElementById('themeToggle');
|
||||||
|
function setTheme(dark) {
|
||||||
|
if (dark) {
|
||||||
|
document.documentElement.setAttribute('data-theme', 'dark');
|
||||||
|
themeBtn.textContent = '☀️ 日间';
|
||||||
|
} else {
|
||||||
|
document.documentElement.removeAttribute('data-theme');
|
||||||
|
themeBtn.textContent = '🌙 暗夜';
|
||||||
|
}
|
||||||
|
try { localStorage.setItem('ft-strategy-theme', dark ? 'dark' : 'light'); } catch(e) {}
|
||||||
|
}
|
||||||
|
themeBtn.addEventListener('click', function() {
|
||||||
|
setTheme(document.documentElement.getAttribute('data-theme') !== 'dark');
|
||||||
|
});
|
||||||
|
(function() {
|
||||||
|
try {
|
||||||
|
var s = localStorage.getItem('ft-strategy-theme');
|
||||||
|
if (s === 'dark' || (!s && matchMedia('(prefers-color-scheme: dark)').matches)) setTheme(true);
|
||||||
|
} catch(e) {}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+186
@@ -0,0 +1,186 @@
|
|||||||
|
# 振幅锁仓策略 · 操作流程
|
||||||
|
|
||||||
|
> 玻璃期货 FG 主力合约 · 每日累仓做空 + 振幅止盈 + 3 锁熔断
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、策略思想
|
||||||
|
|
||||||
|
玻璃期货受地产下行拖累,长期空头趋势明确。不做方向判断,**默认每天做空**,用近 5 日均振幅作为动态止盈/锁仓阈值。回调是利润,反弹锁仓扛,连扛 3 次认输出局。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、核心参数
|
||||||
|
|
||||||
|
| 参数 | 取值 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| 交易方向 | 空 | 每天开盘无条件做空 |
|
||||||
|
| 每日手数 | 1 手(20 吨) | 可等比放大至 N 手/天 |
|
||||||
|
| 每点价值 | 20 元 | 玻璃期货合约规格 |
|
||||||
|
| 振幅阈值 | 近 5 日 (最高−最低) 均值 | 每日盘前重新计算 |
|
||||||
|
| 锁仓上限 | 3 次 | 累计触发后清仓重来 |
|
||||||
|
|
||||||
|
> **多手缩放**:单合约每日开 N 手时,止盈阈值等比放大为 **N × A**,保持触发难度不变。锁仓阈值和熔断规则不受手数影响——每手空单仍按各自开仓时的 A 独立判断锁仓,累计 3 锁熔断不变。
|
||||||
|
|
||||||
|
|
||||||
|
### 仓位管理
|
||||||
|
|
||||||
|
**总手数**:$N = \lfloor \text{权益金(万元)} \rfloor$,向下取整。
|
||||||
|
|
||||||
|
**分配规则**:按沉淀资金从大到小取前三个合约(假设FG09 > FG11 > FG01),轮询分配 N 手:
|
||||||
|
|
||||||
|
| N | FG09 | FG11 | FG01 |
|
||||||
|
|:--|:--|:--|:--|
|
||||||
|
| 1 | 1 | — | — |
|
||||||
|
| 2 | 1 | 1 | — |
|
||||||
|
| 3 | 1 | 1 | 1 |
|
||||||
|
| 4 | 2 | 1 | 1 |
|
||||||
|
| 5 | 2 | 2 | 1 |
|
||||||
|
| 6 | 2 | 2 | 2 |
|
||||||
|
| 7 | 3 | 2 | 2 |
|
||||||
|
| … | … | … | … |
|
||||||
|
|
||||||
|
> 合约 p(按沉淀资金降序,p=1,2,3)分配 $\lfloor (N + 3 - p) / 3 \rfloor$ 手。各合约独立运行策略(独立 A、独立锁仓计数器),一轮熔断不影响其他合约。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、每日操作流程
|
||||||
|
|
||||||
|
### 盘前(1 分钟)
|
||||||
|
|
||||||
|
1. 取前 5 个交易日的 (最高 − 最低),计算均值 → **当日振幅阈值 A**
|
||||||
|
2. 检查当前持仓状态:
|
||||||
|
- 无持仓 → 新 轮次 开始
|
||||||
|
- 有活跃空单 → 继续累仓
|
||||||
|
- 有锁仓 → 继续累仓(锁仓不阻止开新空单)
|
||||||
|
|
||||||
|
### 开盘
|
||||||
|
|
||||||
|
**无条件开 N 手空单**(N = 每日手数,默认 1),记录开仓价和该手对应的锁仓阈值(= 当日振幅阈值 A)。
|
||||||
|
|
||||||
|
```
|
||||||
|
第 N 手空单: 开仓价 = 今日开盘价, 锁仓阈值 = 当日 A
|
||||||
|
```
|
||||||
|
|
||||||
|
### 盘中监控
|
||||||
|
|
||||||
|
用日内最高价、最低价判断触发:
|
||||||
|
|
||||||
|
| 条件 | 触发 | 动作 |
|
||||||
|
|------|------|------|
|
||||||
|
| **总浮盈 ≥ N × A 点**(N = 每日手数) | 🟢 止盈 | 全部清仓(活跃空单 + 锁仓对),获利了结,轮次 结束 |
|
||||||
|
| **某空单亏损 ≥ 该手开仓时的振幅阈值** | 🔒 锁仓 | 对该手开多单锁仓,冻结亏损。该手从"活跃"移入"锁仓" |
|
||||||
|
| **累计锁仓 ≥ 3 次** | 🔴 3 锁熔断 | 全部清仓(活跃空单按结算价平仓 + 锁仓对平仓),认输重来 |
|
||||||
|
| 以上均未触发 | — 持仓 | 所有活跃空单和锁仓对继续持有至次日 |
|
||||||
|
|
||||||
|
### 盘中判断细节
|
||||||
|
|
||||||
|
- 如果止盈和锁仓同时可能触发(日内振幅足够大),用**收盘方向**判断哪个先触发:收盘 > 开盘 → 锁仓先触发,反之止盈先触发。
|
||||||
|
- 当天新开的空单和昨天留下的活跃空单**平等对待**——任何一笔活跃空单亏损达到它自己的锁仓阈值,就锁那一笔。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、锁仓机制
|
||||||
|
|
||||||
|
锁仓 = 对亏损达到阈值的空单,**额外开一手多单**,形成锁仓对。
|
||||||
|
|
||||||
|
```
|
||||||
|
锁仓对 = 空单@开仓价 + 多单@(开仓价 + 阈值)
|
||||||
|
锁仓对盈亏 = (开仓价 − 现价) + (现价 − (开仓价 + 阈值))
|
||||||
|
= −阈值 (固定,不再随价格变动)
|
||||||
|
```
|
||||||
|
|
||||||
|
**一旦锁仓,该笔亏损被冻结,不会扩大。** 但锁仓对也不贡献利润——只有活跃空单才能在价格下跌时产生收益。
|
||||||
|
|
||||||
|
锁仓对在两种情况下平仓:
|
||||||
|
1. 后续总浮盈触发止盈 → 和所有头寸一起清仓
|
||||||
|
2. 累计 3 锁触发熔断 → 和所有头寸一起清仓
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、清仓规则
|
||||||
|
|
||||||
|
### 🟢 止盈清仓
|
||||||
|
|
||||||
|
**触发条件**:当日最低价对应的总浮盈 ≥ N × 当日振幅阈值 A(N = 每日手数)
|
||||||
|
|
||||||
|
总浮盈 = 所有活跃空单浮盈 + 所有锁仓对冻结盈亏
|
||||||
|
|
||||||
|
- 清仓所有活跃空单(按 trigger 价格平仓)
|
||||||
|
- 清仓所有锁仓对(空单和多单同时平仓)
|
||||||
|
- 轮次 结束,锁仓计数器归零,次日重新开始
|
||||||
|
|
||||||
|
### 🔴 3 锁熔断
|
||||||
|
|
||||||
|
**触发条件**:累计锁仓次数 ≥ 3(跨日累计,不清零直到 轮次 结束)
|
||||||
|
|
||||||
|
- 活跃空单按当日结算价平仓
|
||||||
|
- 锁仓对同步平仓
|
||||||
|
- 认输出局,轮次 结束,计数器归零
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、适用条件
|
||||||
|
|
||||||
|
| 条件 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| ✅ 长期空头趋势 | 每日默认做空有正期望 |
|
||||||
|
| ✅ 日均振幅 > 15 点 | 阈值足够覆盖交易成本 |
|
||||||
|
| ✅ 无极端单边上涨 | 连续锁仓是最怕的场景 |
|
||||||
|
| ❌ 趋势反转(熊→牛) | 会连续 3 锁出局,建议暂停 |
|
||||||
|
| ❌ 振幅 < 10 点 | 利润不够覆盖滑点 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、操作示例
|
||||||
|
|
||||||
|
> 以下示例均假设每点价值 20 元。
|
||||||
|
|
||||||
|
### 示例 1:顺利止盈(N=1)
|
||||||
|
|
||||||
|
| 时间 | 操作 | 价格 | 说明 |
|
||||||
|
|:---|:---|:---|:---|
|
||||||
|
| 第 1 天开盘 | 开空 1 手 | 1300 | A=18,开仓 |
|
||||||
|
| 第 1 天收盘 | 持仓 | 1285 | 浮盈 15 点 < 18,不触发,留仓 |
|
||||||
|
| 第 2 天开盘 | 开空 1 手 | 1280 | A=20 |
|
||||||
|
| 第 2 天盘中 | — | 1260 | 手1浮盈=40,手2浮盈=20,总浮盈=60 ≥ 20,🟢 **止盈!** |
|
||||||
|
|
||||||
|
**结果**:利润 = 60 点 × 20 = **+1,200 元**。即使没锁仓,两手持仓让总浮盈更快达到阈值。
|
||||||
|
|
||||||
|
### 示例 2:锁仓后止盈
|
||||||
|
|
||||||
|
| 时间 | 操作 | 价格 | 说明 |
|
||||||
|
|:---|:---|:---|:---|
|
||||||
|
| 第 1 天开盘 | 开空 1 手 | 1300 | A=18 |
|
||||||
|
| 第 1 天盘中 | 价格反弹 | 1318 | 空单浮亏 = 18 ≥ 18,🔒 **锁仓**!多单 @1318,冻结亏损 −18 点 |
|
||||||
|
| 第 2 天开盘 | 开空 1 手 | 1315 | A=20,新活跃空单 |
|
||||||
|
| 第 2 天盘中 | — | 1277 | 活跃浮盈 = 1315−1277 = 38,总浮盈 = 38+(−18) = 20 ≥ 20,🟢 **止盈!** |
|
||||||
|
|
||||||
|
**结果**:利润 = 20 点 × 20 = **+400 元**。锁仓冻结了 18 点亏损,活跃空单多跌了 20 点覆盖亏损后触发。
|
||||||
|
|
||||||
|
### 示例 3:3 锁熔断
|
||||||
|
|
||||||
|
| 时间 | 操作 | 价格 | 说明 |
|
||||||
|
|:---|:---|:---|:---|
|
||||||
|
| 第 1 天 | 开空 → 锁仓 | 1300 → 1318 | A=18,锁仓 1,冻结 −18 |
|
||||||
|
| 第 2 天 | 开空 → 锁仓 | 1315 → 1337 | A=22,锁仓 2,冻结 −22 |
|
||||||
|
| 第 3 天 | 开空 → 锁仓 | 1330 → 1350 | A=20,锁仓 3,🔴 **熔断!** |
|
||||||
|
|
||||||
|
**结果**:全部清仓。亏损 = (−18)+(−22)+(−20)+(活跃空单结算亏损) ≈ **−1,200 元以上**。连续 3 天反弹,认输出局。
|
||||||
|
|
||||||
|
### 示例 4:多手缩放(N=2)
|
||||||
|
|
||||||
|
| 时间 | 操作 | 价格 | 说明 |
|
||||||
|
|:---|:---|:---|:---|
|
||||||
|
| 第 1 天开盘 | 开空 2 手 | 1300 | A=18,止盈阈值 = 2×18 = 36 点 |
|
||||||
|
| 第 1 天盘中 | — | 1282 | 每手浮盈 = 18,总浮盈 = 36 ≥ 36,🟢 **止盈!** |
|
||||||
|
|
||||||
|
**结果**:利润 = 36 点 × 20 = **+720 元**。相比 N=1(利润 360 元),翻倍但触发难度不变——每手仍只需跌 18 点。
|
||||||
|
|
||||||
|
### 关键公式速查
|
||||||
|
|
||||||
|
| 场景 | 止盈阈值 | 触发时利润(恰卡阈值) |
|
||||||
|
|:---|:---|:---|
|
||||||
|
| N 手/天,无锁仓 | N × A | N × A × 20 |
|
||||||
|
| N 手/天,K 个锁仓对 | N × A | N × A × 20(锁仓亏损已内嵌在总浮盈中) |
|
||||||
|
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# 玻璃期货 FG 主连 — 四家博弈分析
|
||||||
|
|
||||||
|
> 2026-07-22 | 收盘 913 | 结算 908 | +1.0%
|
||||||
|
|
||||||
|
## 行情回顾(7/2 → 7/22)
|
||||||
|
|
||||||
|
价格从 960 区间一路下跌至 7/20 最低 889,随后两个交易日反弹至 913。
|
||||||
|
|
||||||
|
## 四家机构 7/22 持仓全貌
|
||||||
|
|
||||||
|
| | 多单 | 空单 | 净持仓 | 净盈亏 |
|
||||||
|
|---|-----:|-----:|-----:|-----:|
|
||||||
|
| **中信** | 63,971 @ 988.16 | 193,030 @ 995.26 | 净空 12.9万 | **+2.21亿** |
|
||||||
|
| **高盛** | — | 188,460 @ 997.89 | 裸空 18.8万 | **+3.20亿** |
|
||||||
|
| **国泰** | 144,325 @ 991.07 | 168,449 @ 1018.54 | 净空 2.4万 | **+1.30亿** |
|
||||||
|
| **华泰** | 68,791 @ 1002.62 | 80,564 @ 976.69 | 净空 1.2万 | **-0.21亿** |
|
||||||
|
| **合计** | 27.7万手 | 63.1万手 | **净空 35.3万** | **+6.51亿** |
|
||||||
|
|
||||||
|
## 利润曲线
|
||||||
|
|
||||||
|
```
|
||||||
|
07-02 963 +4.52亿 ← 起点
|
||||||
|
07-07 957 +5.04亿 ← 第一波加空
|
||||||
|
07-16 938 +5.89亿 ← 破位日,大规模加空
|
||||||
|
07-20 900 +8.66亿 ← 利润峰值(价格最低 889)
|
||||||
|
07-21 901 +8.01亿 ← 反弹开始
|
||||||
|
07-22 908 +6.86亿 ← 继续反弹,两天蒸发 1.8 亿
|
||||||
|
```
|
||||||
|
|
||||||
|
## 关键发现
|
||||||
|
|
||||||
|
### 7/22 是转折日
|
||||||
|
|
||||||
|
四家合计净减仓 **77,252 手**,是整个 7 月最大单日撤退。从"加仓做空"切换到"减仓收网"模式。
|
||||||
|
|
||||||
|
### 成本变化(7/2 → 7/22)
|
||||||
|
|
||||||
|
| | 多单成本 | 空单成本 | 解读 |
|
||||||
|
|---|-----:|-----:|------|
|
||||||
|
| 中信 | 1031→988(▼43) | 1026→995(▼31) | 双边操作,多空成本都在降 |
|
||||||
|
| 高盛 | — | 1016→998(▼18) | 纯空头,成本被动跟随 |
|
||||||
|
| 国泰 | 1036→991(▼45) | 1032→1019(▼13) | 主攻多单成本,抄底积极 |
|
||||||
|
| 华泰 | 1030→1003(▼27) | 1017→977(▼41) | 主攻空单成本,做空更强 |
|
||||||
|
|
||||||
|
国泰抄底最积极(多单成本降了 45 点),华泰做空最激进(空单成本降了 41 点)。
|
||||||
|
|
||||||
|
## 博弈格局
|
||||||
|
|
||||||
|
### 角色定位
|
||||||
|
|
||||||
|
- **中信**(净空 12.9 万):空头老大,安全垫最厚(82 点),已经在有条不紊地收网
|
||||||
|
- **高盛**(裸空 18.8 万):最焦虑的玩家,纯裸空没有对冲,每涨 10 点亏 3768 万
|
||||||
|
- **国泰**(净空 2.4 万):最从容,空单成本最高(建仓最早),体量小随时可清
|
||||||
|
- **华泰**(净空 1.2 万):唯一亏损的,多单成本太高拖累整体
|
||||||
|
|
||||||
|
### 三种退场路径
|
||||||
|
|
||||||
|
1. **有序撤退(概率 60%)**:四家在 890-930 区间轮流平仓,2-3 周后净空降到 15 万以下
|
||||||
|
2. **抢跑踩踏(概率 30%)**:高盛扛不住加速减空 → 价格冲 930-950 → 中信跟进 → 冲高回落
|
||||||
|
3. **继续加空(概率 10%)**:地产数据进一步恶化,空头重新加仓
|
||||||
|
|
||||||
|
### 未来 10 日预判
|
||||||
|
|
||||||
|
| 阶段 | 价格区间 | 驱动 |
|
||||||
|
|------|------|------|
|
||||||
|
| 1-3天 | 910-925 | 空头有序减仓推升价格 |
|
||||||
|
| 4-6天 | 920-930 | 华泰砍多 + 中信压盘,遇阻 |
|
||||||
|
| 7-10天 | 895-915 | 减仓节奏稳定后回归基本面 |
|
||||||
|
|
||||||
|
## 每日观察指标
|
||||||
|
|
||||||
|
1. 高盛日减仓量——超过 2 万手 = 抢跑信号
|
||||||
|
2. 四家合计日减仓量——超过 8 万手 = 撤退加速
|
||||||
|
3. 国泰是否开始减空——最从容的人也判断见顶
|
||||||
|
4. 价格到 925+ 时华泰多单是否大幅减少
|
||||||
|
|
||||||
|
## 数据来源
|
||||||
|
|
||||||
|
- 四家机构持仓:用户付费 App(主力合约维度)
|
||||||
|
- 行情数据:CZCE / akshare
|
||||||
|
- 分析日期:2026-07-22
|
||||||
+188
@@ -0,0 +1,188 @@
|
|||||||
|
日期: 持仓, 增减, 持仓均价
|
||||||
|
|
||||||
|
中信期货 - 空单持仓:
|
||||||
|
2026-07-22: 193030, -22032, 995.26
|
||||||
|
2026-07-21: 215062, -1029, 995.26
|
||||||
|
2026-07-20: 216091, -18178, 995.26
|
||||||
|
2026-07-17: 234269, -31, 995.26
|
||||||
|
2026-07-16: 234300, 35258, 995.26
|
||||||
|
2026-07-15: 199042, 5294, 1005.40
|
||||||
|
2026-07-14: 193748, 4117, 1006.94
|
||||||
|
2026-07-13: 189631, 18219, 1008.16
|
||||||
|
2026-07-10: 171412, -17319, 1013.49
|
||||||
|
2026-07-09: 188731, 9860, 1013.49
|
||||||
|
2026-07-08: 178871, -19747, 1016.71
|
||||||
|
2026-07-07: 198618, 25262, 1016.71
|
||||||
|
2026-07-06: 173356, 2998, 1025.41
|
||||||
|
2026-07-03: 170358, -11691, 1026.30
|
||||||
|
2026-07-02: 182049, 1284, 1026.30
|
||||||
|
|
||||||
|
中信期货 - 多单持仓:
|
||||||
|
2026-07-22: 63971, -10850, 988.16
|
||||||
|
2026-07-21: 74821, 2587, 988.16
|
||||||
|
2026-07-20: 72234, -84, 991.28
|
||||||
|
2026-07-17: 72318, 6928, 991.28
|
||||||
|
2026-07-16: 65390, 6950, 999.15
|
||||||
|
2026-07-15: 58440, -1227, 1006.42
|
||||||
|
2026-07-14: 59667, 887, 1006.42
|
||||||
|
2026-07-13: 58780, -4908, 1007.26
|
||||||
|
2026-07-10: 63688, 5795, 1007.26
|
||||||
|
2026-07-09: 57893, -3193, 1011.59
|
||||||
|
2026-07-08: 61086, 2092, 1011.59
|
||||||
|
2026-07-07: 58994, 8116, 1013.45
|
||||||
|
2026-07-06: 50878, -14738, 1022.46
|
||||||
|
2026-07-03: 65616, 9777, 1022.46
|
||||||
|
2026-07-02: 55839, -2024, 1031.30
|
||||||
|
|
||||||
|
高盛期货 - 空单持仓:
|
||||||
|
2026-07-22: 188460, -14927, 997.89
|
||||||
|
2026-07-21: 203387, -15903, 997.89
|
||||||
|
2026-07-20: 219290, 1220, 997.89
|
||||||
|
2026-07-17: 218070, 14452, 998.44
|
||||||
|
2026-07-16: 203618, 12879, 1004.22
|
||||||
|
2026-07-15: 190739, -5701, 1008.69
|
||||||
|
2026-07-14: 196440, 1711, 1008.69
|
||||||
|
2026-07-13: 194729, 418, 1009.20
|
||||||
|
2026-07-10: 194311, -5203, 1009.31
|
||||||
|
2026-07-09: 199514, 7653, 1009.31
|
||||||
|
2026-07-08: 191861, 1394, 1011.47
|
||||||
|
2026-07-07: 190467, 12046, 1011.86
|
||||||
|
2026-07-06: 178421, 578, 1015.56
|
||||||
|
2026-07-03: 177843, 1049, 1015.69
|
||||||
|
2026-07-02: 176794, 19121, 1015.95
|
||||||
|
|
||||||
|
国泰君安期货 - 空单持仓:
|
||||||
|
2026-07-22: 168449, -9289, 1018.54
|
||||||
|
2026-07-21: 177738, -10380, 1018.54
|
||||||
|
2026-07-20: 188118, -731, 1018.54
|
||||||
|
2026-07-17: 188849, -12984, 1018.54
|
||||||
|
2026-07-16: 201833, -5144, 1018.54
|
||||||
|
2026-07-15: 206977, 2787, 1018.54
|
||||||
|
2026-07-14: 204190, -2399, 1019.49
|
||||||
|
2026-07-13: 206589, 24344, 1019.49
|
||||||
|
2026-07-10: 182245, -4391, 1027.70
|
||||||
|
2026-07-09: 186636, -6510, 1027.70
|
||||||
|
2026-07-08: 193146, -6957, 1027.70
|
||||||
|
2026-07-07: 200103, 9696, 1027.70
|
||||||
|
2026-07-06: 190407, 1344, 1031.30
|
||||||
|
2026-07-03: 189063, -1588, 1031.70
|
||||||
|
2026-07-02: 190651, 4656, 1031.70
|
||||||
|
|
||||||
|
国泰君安期货 - 多单持仓:
|
||||||
|
2026-07-22: 144325, -6262, 991.07
|
||||||
|
2026-07-21: 150587, 17790, 991.07
|
||||||
|
2026-07-20: 132797, 3578, 1003.13
|
||||||
|
2026-07-17: 129219, 12066, 1005.99
|
||||||
|
2026-07-16: 117153, 8387, 1015.15
|
||||||
|
2026-07-15: 108766, 5534, 1021.10
|
||||||
|
2026-07-14: 103232, 4815, 1024.97
|
||||||
|
2026-07-13: 98417, 4839, 1028.58
|
||||||
|
2026-07-10: 93578, -1293, 1032.23
|
||||||
|
2026-07-09: 94871, -807, 1032.23
|
||||||
|
2026-07-08: 95678, -5169, 1032.23
|
||||||
|
2026-07-07: 100847, 3412, 1032.23
|
||||||
|
2026-07-06: 97435, -4615, 1034.87
|
||||||
|
2026-07-03: 102050, 2169, 1034.87
|
||||||
|
2026-07-02: 99881, 9801, 1036.23
|
||||||
|
|
||||||
|
华泰期货 - 空单持仓:
|
||||||
|
2026-07-22: 80564, -8762, 976.69
|
||||||
|
2026-07-21: 89326, 9844, 976.69
|
||||||
|
2026-07-20: 79482, 1751, 986.06
|
||||||
|
2026-07-17: 77731, -8367, 988.00
|
||||||
|
2026-07-16: 86098, 11358, 988.00
|
||||||
|
2026-07-15: 74740, 3028, 995.60
|
||||||
|
2026-07-14: 71712, -2199, 997.57
|
||||||
|
2026-07-13: 73911, 10715, 997.57
|
||||||
|
2026-07-10: 63196, -3512, 1004.28
|
||||||
|
2026-07-09: 66708, 3827, 1004.28
|
||||||
|
2026-07-08: 62881, -10077, 1007.27
|
||||||
|
2026-07-07: 72958, 12276, 1007.27
|
||||||
|
2026-07-06: 60682, -6933, 1017.45
|
||||||
|
2026-07-03: 65615, -5963, 1017.45
|
||||||
|
2026-07-02: 71558, -2128, 1017.45
|
||||||
|
|
||||||
|
华泰期货 - 多单持仓:
|
||||||
|
2026-07-22: 68791, -5130, 1002.62
|
||||||
|
2026-07-21: 73921, -274, 1002.62
|
||||||
|
2026-07-20: 74195, 840, 1002.62
|
||||||
|
2026-07-17: 73355, 7012, 1003.79
|
||||||
|
2026-07-16: 66343, 4562, 1012.97
|
||||||
|
2026-07-15: 61781, -1562, 1018.50
|
||||||
|
2026-07-14: 63343, 2364, 1018.50
|
||||||
|
2026-07-13: 60979, -1843, 1021.12
|
||||||
|
2026-07-10: 62822, 1161, 1021.12
|
||||||
|
2026-07-09: 61661, 162, 1022.19
|
||||||
|
2026-07-08: 61499, 82, 1022.37
|
||||||
|
2026-07-07: 61417, 6050, 1022.46
|
||||||
|
2026-07-06: 55367, -2699, 1029.61
|
||||||
|
2026-07-03: 58066, -4006, 1029.61
|
||||||
|
2026-07-02: 62072, -2935, 1029.61
|
||||||
|
|
||||||
|
东证期货 - 空单持仓:
|
||||||
|
2026-07-22: 112614, -18937, 965.67
|
||||||
|
2026-07-21: 131551, -45486, 965.67
|
||||||
|
2026-07-20: 177037, -1629, 965.67
|
||||||
|
2026-07-17: 178666, -4252, 965.67
|
||||||
|
2026-07-16: 182918, 47376, 965.67
|
||||||
|
2026-07-15: 135542, -13505, 975.35
|
||||||
|
2026-07-14: 149047, -1108, 975.35
|
||||||
|
2026-07-13: 150155, 40931, 975.35
|
||||||
|
2026-07-10: 109224, -23556, 981.85
|
||||||
|
2026-07-09: 132780, 25017, 981.85
|
||||||
|
2026-07-08: 107763, -56346, 988.08
|
||||||
|
2026-07-07: 164109, 37956, 988.08
|
||||||
|
2026-07-06: 126153, 20536, 997.43
|
||||||
|
2026-07-03: 105617, -10003, 1001.79
|
||||||
|
2026-07-02: 115620, -16409, 1001.79
|
||||||
|
|
||||||
|
东证期货 - 多单持仓:
|
||||||
|
2026-07-22: 85630, 6685, 965.35
|
||||||
|
2026-07-21: 78945, 8359, 970.20
|
||||||
|
2026-07-20: 70586, 2334, 978.40
|
||||||
|
2026-07-17: 68252, -5918, 981.08
|
||||||
|
2026-07-16: 74170, 10413, 981.08
|
||||||
|
2026-07-15: 63757, 1640, 988.12
|
||||||
|
2026-07-14: 62117, -2136, 989.15
|
||||||
|
2026-07-13: 64253, -14158, 989.15
|
||||||
|
2026-07-10: 78411, 7829, 989.15
|
||||||
|
2026-07-09: 70582, -9231, 991.94
|
||||||
|
2026-07-08: 79813, 11598, 991.94
|
||||||
|
2026-07-07: 68215, 10382, 997.54
|
||||||
|
2026-07-06: 57833, -30335, 1004.82
|
||||||
|
2026-07-03: 88168, 20705, 1004.82
|
||||||
|
2026-07-02: 67463, 3862, 1014.89
|
||||||
|
|
||||||
|
银河期货 - 空单持仓:
|
||||||
|
2026-07-22: 62130, -3771, 1022.69
|
||||||
|
2026-07-21: 65901, -3865, 1022.69
|
||||||
|
2026-07-20: 69766, 1572, 1022.69
|
||||||
|
2026-07-17: 68194, 431, 1025.51
|
||||||
|
2026-07-16: 67763, -936, 1026.20
|
||||||
|
2026-07-15: 68699, 372, 1026.20
|
||||||
|
2026-07-14: 68327, 5179, 1026.62
|
||||||
|
2026-07-13: 63148, -3119, 1032.83
|
||||||
|
2026-07-10: 66267, -431, 1032.83
|
||||||
|
2026-07-09: 66698, 1995, 1032.83
|
||||||
|
2026-07-08: 64703, -5025, 1035.23
|
||||||
|
2026-07-07: 69728, 6103, 1035.23
|
||||||
|
2026-07-06: 63625, -1703, 1042.73
|
||||||
|
2026-07-03: 65328, -3861, 1042.73
|
||||||
|
2026-07-02: 69189, 1875, 1042.73
|
||||||
|
|
||||||
|
银河期货 - 多单持仓:
|
||||||
|
2026-07-22: 57020, -7565, 1016.17
|
||||||
|
2026-07-21: 64585, -4812, 1016.17
|
||||||
|
2026-07-20: 69397, -2877, 1016.17
|
||||||
|
2026-07-17: 72274, -4410, 1016.17
|
||||||
|
2026-07-16: 76684, 4888, 1016.17
|
||||||
|
2026-07-15: 71796, -3235, 1021.49
|
||||||
|
2026-07-14: 75031, -456, 1021.49
|
||||||
|
2026-07-13: 75487, 4361, 1021.49
|
||||||
|
2026-07-10: 71126, -6491, 1025.39
|
||||||
|
2026-07-09: 77617, 376, 1025.39
|
||||||
|
2026-07-08: 77241, -3621, 1025.73
|
||||||
|
2026-07-07: 80862, 10307, 1025.73
|
||||||
|
2026-07-06: 70555, 2439, 1035.77
|
||||||
|
2026-07-03: 68116, -4101, 1037.95
|
||||||
|
2026-07-02: 72217, 490, 1037.95
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
claude --resume 77f85e7b-9dfa-4d47-8a89-a5307361c54f
|
||||||
+144
@@ -0,0 +1,144 @@
|
|||||||
|
# 玻璃期货 FG — 中枢波动交易规则
|
||||||
|
|
||||||
|
> 核心思想:不赌方向,赌波动。锚定价值中枢,上轨卖、下轨买,赚区间回归的钱。
|
||||||
|
|
||||||
|
## 一、交易成本
|
||||||
|
|
||||||
|
| 项目 | 金额 |
|
||||||
|
|------|-----:|
|
||||||
|
| 一手保证金 | 1,600 ~ 1,800 元 |
|
||||||
|
| 开平合计手续费 | 4.02 元 |
|
||||||
|
| 手续费折合点数 | **约 0.2 个点** |
|
||||||
|
| 每点价值 | 20 元 |
|
||||||
|
|
||||||
|
> 0.2 个点覆盖手续费,8 个点的波段净赚 160 元/手。
|
||||||
|
|
||||||
|
## 二、中枢的确定
|
||||||
|
|
||||||
|
### 方法:前一日结算价(最实用)
|
||||||
|
|
||||||
|
每天开盘前,以前一日结算价为当日中枢。结合近 5 日均振幅来设区间。
|
||||||
|
|
||||||
|
```
|
||||||
|
中枢 = 前一日结算价
|
||||||
|
近 5 日均振幅 = 过去 5 个交易日 (最高-最低) 的均值
|
||||||
|
```
|
||||||
|
|
||||||
|
### 中枢调整规则
|
||||||
|
|
||||||
|
| 情形 | 调整 |
|
||||||
|
|------|------|
|
||||||
|
| 连续 5 个波段成功止盈 | 中枢跟随现价上移/下移 |
|
||||||
|
| 连续 2 次触发止损 | 暂停交易,等新中枢形成 |
|
||||||
|
| 价格连续 2 日收盘在中枢 ±20 点以外 | 中枢失效,趋势已形成,退出等收敛 |
|
||||||
|
| 六家主力同时减空 | 中枢偏多 3-5 个点 |
|
||||||
|
| 高盛单日减空超 2 万手 | 上轨放宽 5 点,暂停做空 |
|
||||||
|
|
||||||
|
## 三、区间参数
|
||||||
|
|
||||||
|
### 标准参数(波动率正常时)
|
||||||
|
|
||||||
|
```
|
||||||
|
上轨(卖空区): 中枢 +10 ~ +15 点
|
||||||
|
下轨(买多区): 中枢 -10 ~ -15 点
|
||||||
|
单笔目标利润: 8 ~ 10 个点
|
||||||
|
硬止损线: 中枢 ±25 点
|
||||||
|
```
|
||||||
|
|
||||||
|
### 宽幅参数(波动率放大时,近 5 日均振幅 > 22 点)
|
||||||
|
|
||||||
|
```
|
||||||
|
上轨: 中枢 +15 ~ +22 点
|
||||||
|
下轨: 中枢 -15 ~ -22 点
|
||||||
|
单笔目标利润: 10 ~ 15 点
|
||||||
|
硬止损线: 中枢 ±35 点
|
||||||
|
```
|
||||||
|
|
||||||
|
### 窄幅参数(波动率收缩时,近 5 日均振幅 < 12 点)
|
||||||
|
|
||||||
|
```
|
||||||
|
上轨: 中枢 +6 ~ +10 点
|
||||||
|
下轨: 中枢 -6 ~ -10 点
|
||||||
|
单笔目标利润: 5 ~ 8 点
|
||||||
|
硬止损线: 中枢 ±18 点
|
||||||
|
```
|
||||||
|
|
||||||
|
## 四、仓位管理
|
||||||
|
|
||||||
|
### 分三批入场
|
||||||
|
|
||||||
|
| 批次 | 时机 | 仓位占比 | 举例(总仓位 5 手) |
|
||||||
|
|------|------|:------:|------|
|
||||||
|
| 第一批 | 价格触及区间边界 | 30% | 中枢 908,跌到 898 开 2 手多 |
|
||||||
|
| 第二批 | 偏离中枢 15 点 | 30% | 跌到 893 再开 1 手多 |
|
||||||
|
| 第三批 | 偏离中枢 20 点 | 40% | 跌到 888 再开 2 手多 |
|
||||||
|
|
||||||
|
> 均价被摊薄,反弹到中枢附近即可整体止盈。
|
||||||
|
|
||||||
|
### 仓位约束
|
||||||
|
|
||||||
|
- 单向连续加仓**不超过 3 次**
|
||||||
|
- 加满 3 批后若继续反向,**不再加仓**,到硬止损线全平
|
||||||
|
- 总保证金占用不超过账户的 **50%**,留一半做错时腾挪
|
||||||
|
|
||||||
|
## 五、操作流程
|
||||||
|
|
||||||
|
### 每日盘前(5 分钟)
|
||||||
|
|
||||||
|
1. 算中枢:取前一日结算价
|
||||||
|
2. 算近 5 日均振幅
|
||||||
|
3. 选定参数档位(标准/宽幅/窄幅)
|
||||||
|
4. 写下当日上下轨和止损线
|
||||||
|
5. 看博弈格局(六家持仓变化),微调中枢
|
||||||
|
|
||||||
|
### 盘中
|
||||||
|
|
||||||
|
- 价格到上轨 → 开空单
|
||||||
|
- 价格到下轨 → 开多单
|
||||||
|
- 价格回到中枢 ±3 点 → 止盈平仓
|
||||||
|
- 价格突破中枢 ±25 点 → 全部止损
|
||||||
|
|
||||||
|
### 每日盘后
|
||||||
|
|
||||||
|
- 更新 `info.txt` 的持仓数据
|
||||||
|
- 记录当日交易笔数、盈亏、是否触止损
|
||||||
|
- 触止损则复盘:是中枢定错了还是区间定窄了
|
||||||
|
|
||||||
|
## 六、什么时候不做
|
||||||
|
|
||||||
|
| 情形 | 原因 |
|
||||||
|
|------|------|
|
||||||
|
| 价格连续 2 日收盘在 ±20 点外 | 趋势已形成,中枢失效 |
|
||||||
|
| 近 5 日均振幅 > 30 点 | 波动过大,区间策略被反复打脸 |
|
||||||
|
| 近 5 日均振幅 < 8 点 | 利润不够覆盖滑点风险 |
|
||||||
|
| 重大政策发布日 | 跳空风险不可控 |
|
||||||
|
| 连续止损 2 次 | 当天停手,中枢有问题 |
|
||||||
|
|
||||||
|
## 七、盈亏预期
|
||||||
|
|
||||||
|
假设每天操作 5 手,日均 1-2 个来回:
|
||||||
|
|
||||||
|
| 场景 | 日均盈亏 |
|
||||||
|
|------|------|
|
||||||
|
| 理想(2 个来回,每手 10 点) | +2,000 元 |
|
||||||
|
| 平常(1 个来回,每手 8 点) | +800 元 |
|
||||||
|
| 止损日(-25 点全平,5 手) | -2,500 元 |
|
||||||
|
|
||||||
|
> 关键:止损日控制在小概率(<10%),平常日稳定积累,月胜率 > 70% 就能稳定盈利。
|
||||||
|
|
||||||
|
## 八、博弈数据辅助决策
|
||||||
|
|
||||||
|
结合 `info.txt` 六家机构数据:
|
||||||
|
|
||||||
|
| 博弈信号 | 操作调整 |
|
||||||
|
|------|------|
|
||||||
|
| 高盛单日减空 > 2 万手 | 空头抢跑,暂停做空,上轨放宽 5 点 |
|
||||||
|
| 中信连续 3 天减空 | 空头有序撤退,中枢逐步上移 |
|
||||||
|
| 国泰开始减空 | 最聪明的人在止盈,跟 |
|
||||||
|
| 华泰多单大幅减少 | 反弹接近尾声,准备下移中枢 |
|
||||||
|
| 六家同日双向减仓 > 5 万手 | 变盘前兆,减半仓 |
|
||||||
|
| 东证多空同步扩仓 | 波动率要放大,切宽幅参数 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> 最后更新:2026-07-22
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
# 对话恢复指南
|
||||||
|
|
||||||
|
> 换设备后,在 Claude Code 中输入下方"启动提示词"即可无缝接续。
|
||||||
|
|
||||||
|
## 会话信息
|
||||||
|
|
||||||
|
- **会话 UUID**: `77f85e7b-9dfa-4d47-8a89-a5307361c54f`
|
||||||
|
- **最后更新**: 2026-07-22
|
||||||
|
- **Python 环境**: `.venv/`(Python 3.13 + akshare)
|
||||||
|
|
||||||
|
## 启动提示词
|
||||||
|
|
||||||
|
> 这是之前对话的延续(会话 ID: 77f85e7b)。请先读取 日报/对话恢复指南.md、日报/中枢波动交易规则.md、日报/info.txt 三个文件,理解项目上下文后,告诉我你已经了解了情况,然后我们继续。
|
||||||
|
|
||||||
|
## 我们在做什么
|
||||||
|
|
||||||
|
追踪 **6 家主力机构**(中信、高盛、国泰、华泰、东证、银河)在玻璃期货 FG 主连上的多空持仓,通过持仓成本、仓位变化、博弈格局来判断后续走势和交易机会。
|
||||||
|
|
||||||
|
## 文件说明
|
||||||
|
|
||||||
|
| 文件 | 用途 | 维护方式 |
|
||||||
|
|------|------|------|
|
||||||
|
| `日报/info.txt` | 六家机构每日持仓数据(日期、持仓量、增减仓、持仓均价) | **人工每日更新**,从付费 App 获取 |
|
||||||
|
| `日报/中枢波动交易规则.md` | 基于价值中枢的波段交易策略 | 策略文档,偶尔更新 |
|
||||||
|
| `日报/对话恢复指南.md` | 本文件 | 重要结论更新时同步更新 |
|
||||||
|
| `日报/talk.md` | 会话 UUID 备忘 | 不变 |
|
||||||
|
|
||||||
|
## 当前状态
|
||||||
|
|
||||||
|
### 盘面
|
||||||
|
|
||||||
|
- 玻璃主连从 7/2 的 960+ 一路跌至 7/20 最低 889,近两日反弹至 7/22 收盘 913
|
||||||
|
- 六家机构合计净空 38.6 万手,基本就是全市场前 20 净空的全部
|
||||||
|
- 7/22 六家合计减仓约 7-8 万手,空头主力已从"加仓模式"切换到"撤退模式"
|
||||||
|
|
||||||
|
### 六家持仓(7/22 收盘价 913)
|
||||||
|
|
||||||
|
| | 多单 | 空单 | 净持仓 | 净盈亏 |
|
||||||
|
|---|-----:|-----:|-----:|-----:|
|
||||||
|
| **中信** | 6.4万 @ 988 | 19.3万 @ 995 | 净空 12.9万 | +2.21亿 |
|
||||||
|
| **高盛** | — | 18.8万 @ 998 | 裸空 18.8万 | +3.20亿 |
|
||||||
|
| **国泰** | 14.4万 @ 991 | 16.8万 @ 1019 | 净空 2.4万 | +1.30亿 |
|
||||||
|
| **华泰** | 6.9万 @ 1003 | 8.1万 @ 977 | 净空 1.2万 | -0.21亿 |
|
||||||
|
| **东证** | 8.6万 @ 965 | 11.3万 @ 966 | 净空 2.7万 | +0.29亿 |
|
||||||
|
| **银河** | 5.7万 @ 1016 | 6.2万 @ 1023 | 净空 0.5万 | +0.19亿 |
|
||||||
|
| **合计** | 42.0万 | 80.5万 | 净空 38.6万 | +6.99亿 |
|
||||||
|
|
||||||
|
### 角色定位
|
||||||
|
|
||||||
|
- **中信**:空头老大,安全垫 82 点,正在有序撤退
|
||||||
|
- **高盛**:裸空之王,最焦虑的玩家,每涨 10 点亏 3768 万
|
||||||
|
- **国泰**:最从容,空单建在最高位(1019),操作最漂亮
|
||||||
|
- **华泰**:唯一亏损的,多单买贵了拖累整体
|
||||||
|
- **东证**:完美锁仓(多空成本差 0.32),做波动不做方向。**散户最值得学习**
|
||||||
|
- **银河**:接近中性,躺平型观察者
|
||||||
|
|
||||||
|
### 博弈判断
|
||||||
|
|
||||||
|
- 七家(含中金财富)空头主导市场,多头是散户和中小机构
|
||||||
|
- 空头利润峰值在 7/20(8.66亿),已开始缩水
|
||||||
|
- 未来 10 日:大概率 890-930 区间震荡,中枢逐步上移
|
||||||
|
- 关键观察:高盛减空节奏、中信撤退速度、国泰何时动
|
||||||
|
|
||||||
|
### 策略框架
|
||||||
|
|
||||||
|
已形成"中枢波动交易"策略,见 `日报/中枢波动交易规则.md`。核心:锚定结算价为中枢,上轨卖下轨买,赚区间回归的钱。手续费 0.2 点覆盖,8-10 点波段即可盈利。
|
||||||
|
|
||||||
|
## 每日工作流
|
||||||
|
|
||||||
|
1. 从付费 App 获取六家机构最新持仓数据
|
||||||
|
2. 更新 `日报/info.txt`(在每家机构下方追加新日期行)
|
||||||
|
3. 打开 Claude Code,告诉我"更新了"
|
||||||
|
4. Claude 会:拉行情 → 算盈亏 → 分析博弈 → 输出判断
|
||||||
|
5. 根据分析调整当日交易中枢和区间参数
|
||||||
|
|
||||||
|
## 关键结论备忘
|
||||||
|
|
||||||
|
- 东证双边成本收敛于 965,此价位可能是当前市场价值中枢
|
||||||
|
- 国泰空单成本 1019 是全市场最高,下跌全程未追空而是加多抄底
|
||||||
|
- 7/22 是转折日,六家单日净减仓 7.7 万手,从加仓切换到收网
|
||||||
|
- 银河打破完美对冲的那天是变盘信号
|
||||||
|
- 高盛单日减空超 2 万手 = 抢跑信号
|
||||||
|
|
||||||
|
## 参考命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看玻璃主连日线
|
||||||
|
.venv/bin/python3 -c "import akshare as ak; df=ak.futures_main_sina(symbol='FG0'); print(df.tail(10))"
|
||||||
|
|
||||||
|
# 拉取当日 CZCE 持仓排名
|
||||||
|
.venv/bin/python3 -c "import akshare as ak; r=ak.get_rank_table_czce(date='20260722'); print(r['FG'])"
|
||||||
|
|
||||||
|
# 启动此对话(同一设备)
|
||||||
|
claude --resume 77f85e7b-9dfa-4d47-8a89-a5307361c54f
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user