完善下单表单与 CTP 持仓,requirements 加入 vnpy 并更新部署文档
以损定仓/固定张数分栏下单、限价市价、持仓仅读柜台;DEPLOY 补充 SimNow 与 vnpy 安装说明。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -26,6 +26,11 @@ need_install python3 python3
|
|||||||
need_install python3-venv python3-venv
|
need_install python3-venv python3-venv
|
||||||
need_install git git
|
need_install git git
|
||||||
|
|
||||||
|
# vnpy_ctp 在 Linux 上可能需要编译
|
||||||
|
if ! dpkg -s build-essential &>/dev/null; then
|
||||||
|
apt-get install -y build-essential python3-dev
|
||||||
|
fi
|
||||||
|
|
||||||
if ! command -v pm2 &>/dev/null; then
|
if ! command -v pm2 &>/dev/null; then
|
||||||
echo "==> 安装 PM2..."
|
echo "==> 安装 PM2..."
|
||||||
if ! command -v npm &>/dev/null; then
|
if ! command -v npm &>/dev/null; then
|
||||||
|
|||||||
+84
-13
@@ -20,13 +20,15 @@
|
|||||||
## 环境要求
|
## 环境要求
|
||||||
|
|
||||||
- **系统**:Ubuntu 20.04+(推荐)
|
- **系统**:Ubuntu 20.04+(推荐)
|
||||||
- **Python**:3.10+
|
- **Python**:3.10+(vnpy_ctp 要求 ≥3.10)
|
||||||
- **Node.js + PM2**:进程守护与开机自启
|
- **Node.js + PM2**:进程守护与开机自启
|
||||||
|
- **编译工具**(安装 vnpy_ctp 时需要):`build-essential`、`python3-dev`
|
||||||
- **网络**:
|
- **网络**:
|
||||||
- `hq.sinajs.cn`(新浪行情)
|
- `hq.sinajs.cn`(新浪行情)
|
||||||
- 企业微信 API(若启用推送)
|
- 企业微信 API(若启用推送)
|
||||||
- `git.bz121.com`(拉取代码)
|
- `git.bz121.com`(拉取代码)
|
||||||
- 可选:`pip` 安装 akshare 时需访问 PyPI
|
- `pypi.org`(pip 安装依赖)
|
||||||
|
- SimNow / 期货公司 **CTP 前置地址**(下单与持仓,见下文)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -60,10 +62,12 @@ bash deploy.sh
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
apt update
|
apt update
|
||||||
apt install -y python3 python3-venv python3-pip git nodejs npm
|
apt install -y python3 python3-venv python3-pip python3-dev git nodejs npm build-essential
|
||||||
npm install -g pm2
|
npm install -g pm2
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`build-essential` 与 `python3-dev` 用于编译安装 **vnpy_ctp**(CTP 网关)。若 `pip install vnpy_ctp` 报编译错误,请先确认上述包已安装。
|
||||||
|
|
||||||
### 2. 克隆代码
|
### 2. 克隆代码
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -80,16 +84,13 @@ pip install --upgrade pip
|
|||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. 可选:AKShare(手续费第三方同步)
|
依赖已包含 **vnpy**、**vnpy_ctp**(CTP 报单)、**akshare**(手续费同步)。安装完成后可验证:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
source venv/bin/activate
|
python -c "from vnpy_ctp import CtpGateway; print('vnpy_ctp OK')"
|
||||||
pip install akshare
|
|
||||||
```
|
```
|
||||||
|
|
||||||
不安装也可使用系统内置 `data/fee_rates.json` 默认费率。
|
若提示找不到模块,查看本文「CTP / vnpy 故障排查」一节。
|
||||||
|
|
||||||
### 5. 环境变量
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
@@ -107,6 +108,12 @@ nano .env
|
|||||||
| `WECHAT_WEBHOOK` | 企业微信机器人地址(可选) |
|
| `WECHAT_WEBHOOK` | 企业微信机器人地址(可选) |
|
||||||
| `QUOTE_SOURCE` | `sina`(默认)/ `ths` / `auto` |
|
| `QUOTE_SOURCE` | `sina`(默认)/ `ths` / `auto` |
|
||||||
| `THS_REFRESH_TOKEN` | 同花顺 iFinD token(机构用户) |
|
| `THS_REFRESH_TOKEN` | 同花顺 iFinD token(机构用户) |
|
||||||
|
| `SIMNOW_USER` | SimNow 仿真账号(模拟盘必填) |
|
||||||
|
| `SIMNOW_PASSWORD` | SimNow 密码 |
|
||||||
|
| `SIMNOW_TD_ADDRESS` | SimNow 交易前置(以官网最新为准) |
|
||||||
|
| `SIMNOW_MD_ADDRESS` | SimNow 行情前置 |
|
||||||
|
| `CTP_LIVE_*` | 期货公司实盘 CTP(后期接入,见 `.env.example`) |
|
||||||
|
| `TRADING_MODE` | `simulation`(SimNow)/ `live`(实盘) |
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
|
|
||||||
@@ -119,8 +126,21 @@ ADMIN_PASSWORD=你的强密码
|
|||||||
ADMIN_SYNC_FROM_ENV=false
|
ADMIN_SYNC_FROM_ENV=false
|
||||||
WECHAT_WEBHOOK=
|
WECHAT_WEBHOOK=
|
||||||
QUOTE_SOURCE=sina
|
QUOTE_SOURCE=sina
|
||||||
|
|
||||||
|
# —— SimNow 模拟盘(在 simnow.com.cn 注册)——
|
||||||
|
SIMNOW_USER=你的SimNow账号
|
||||||
|
SIMNOW_PASSWORD=你的密码
|
||||||
|
SIMNOW_BROKER_ID=9999
|
||||||
|
SIMNOW_TD_ADDRESS=tcp://180.168.146.187:10201
|
||||||
|
SIMNOW_MD_ADDRESS=tcp://180.168.146.187:10211
|
||||||
|
SIMNOW_APP_ID=simnow_client_test
|
||||||
|
SIMNOW_AUTH_CODE=0000000000000000
|
||||||
|
SIMNOW_PRODUCT_INFO=simnow_client_test
|
||||||
|
TRADING_MODE=simulation
|
||||||
```
|
```
|
||||||
|
|
||||||
|
SimNow 前置地址会随官网更新,部署前请到 [SimNow 官网](https://www.simnow.com.cn/) 核对 **7×24** 或交易时段地址。
|
||||||
|
|
||||||
### 6. PM2 启动
|
### 6. PM2 启动
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -150,15 +170,25 @@ pip install -r requirements.txt
|
|||||||
pm2 restart qihuo
|
pm2 restart qihuo
|
||||||
```
|
```
|
||||||
|
|
||||||
若新增可选依赖(如 akshare):
|
若 `vnpy_ctp` 安装失败(常见于缺少编译环境):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install akshare
|
apt install -y build-essential python3-dev
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install --no-cache-dir vnpy vnpy_ctp
|
||||||
pm2 restart qihuo
|
pm2 restart qihuo
|
||||||
```
|
```
|
||||||
|
|
||||||
应用启动时会自动执行 SQLite 表结构迁移(`ALTER TABLE` 容错),一般无需手工改库。
|
应用启动时会自动执行 SQLite 表结构迁移(`ALTER TABLE` 容错),一般无需手工改库。
|
||||||
|
|
||||||
|
### 首次启用 CTP 下单
|
||||||
|
|
||||||
|
1. 浏览器登录 → **系统设置** 确认 **模拟盘 · SimNow**
|
||||||
|
2. 打开 **持仓监控** 页 → 点击 **连接 CTP**
|
||||||
|
3. 连接成功后:权益来自柜台、**持仓监控** 显示 CTP 实际持仓、**期货下单** 可报单
|
||||||
|
|
||||||
|
详见 [TRADING.md](./TRADING.md)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## PM2 常用命令
|
## PM2 常用命令
|
||||||
@@ -274,7 +304,11 @@ ufw allow 6600/tcp
|
|||||||
| 现价一直 `--` | 新浪网络不可达 | 检查服务器能否访问 `hq.sinajs.cn` |
|
| 现价一直 `--` | 新浪网络不可达 | 检查服务器能否访问 `hq.sinajs.cn` |
|
||||||
| 关键位 500 | 缺 `sina_code` 列 | `git pull` 重启;或手工 `ALTER TABLE` |
|
| 关键位 500 | 缺 `sina_code` 列 | `git pull` 重启;或手工 `ALTER TABLE` |
|
||||||
| K 线生成失败 | matplotlib 未装 | `pip install matplotlib==3.9.2` |
|
| K 线生成失败 | matplotlib 未装 | `pip install matplotlib==3.9.2` |
|
||||||
| 手续费同步失败 | 未装 akshare | `pip install akshare` 或用「重载 JSON」 |
|
| 手续费同步失败 | akshare 异常 | 使用「重载 JSON」或检查 akshare |
|
||||||
|
| **未安装 vnpy / vnpy_ctp** | 依赖未装或编译失败 | 见下方「CTP / vnpy 故障排查」 |
|
||||||
|
| **CTP 连接超时** | SimNow 地址/账号/非交易时段 | 核对 `.env` 与 SimNow 官网前置 |
|
||||||
|
| **持仓监控为空** | 未连接 CTP 或确实无仓 | 先点「连接 CTP」 |
|
||||||
|
| `database is locked` | SQLite 并发 | `git pull` 最新版后重启 |
|
||||||
| `git pull` 冲突 | 本地有修改 | 备份 `futures.db` 后处理冲突或 `git stash` |
|
| `git pull` 冲突 | 本地有修改 | 备份 `futures.db` 后处理冲突或 `git stash` |
|
||||||
|
|
||||||
查看应用是否在监听:
|
查看应用是否在监听:
|
||||||
@@ -283,6 +317,37 @@ ufw allow 6600/tcp
|
|||||||
ss -tlnp | grep 6600
|
ss -tlnp | grep 6600
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### CTP / vnpy 故障排查
|
||||||
|
|
||||||
|
页面提示 **「未安装 vnpy / vnpy_ctp」** 表示 Python 环境未成功安装 CTP 网关,下单与柜台持仓不可用(看盘、策略、复盘仍可用)。
|
||||||
|
|
||||||
|
**1. 安装依赖**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/qihuo
|
||||||
|
source venv/bin/activate
|
||||||
|
apt install -y build-essential python3-dev # 首次需要
|
||||||
|
pip install -r requirements.txt
|
||||||
|
python -c "from vnpy_ctp import CtpGateway; print('OK')"
|
||||||
|
pm2 restart qihuo
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. 配置 SimNow(`.env`)**
|
||||||
|
|
||||||
|
填写 `SIMNOW_USER`、`SIMNOW_PASSWORD`,前置地址以 SimNow 官网为准。
|
||||||
|
|
||||||
|
**3. 连接**
|
||||||
|
|
||||||
|
登录系统 → **持仓监控** → **连接 CTP**。成功则顶栏显示「CTP 已连接」,权益变为 SimNow 账户资金。
|
||||||
|
|
||||||
|
**4. 常见错误**
|
||||||
|
|
||||||
|
| 日志/现象 | 处理 |
|
||||||
|
|-----------|------|
|
||||||
|
| `pip install vnpy_ctp` 编译失败 | 安装 `build-essential python3-dev` 后重试 |
|
||||||
|
| CTP 连接超时 | 检查前置 IP、端口、SimNow 是否维护、是否在允许连接时段 |
|
||||||
|
| 已连接但下单拒单 | 检查合约代码、价格精度、是否有足够保证金 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 安全建议
|
## 安全建议
|
||||||
@@ -299,6 +364,9 @@ ss -tlnp | grep 6600
|
|||||||
```
|
```
|
||||||
/opt/qihuo/
|
/opt/qihuo/
|
||||||
├── app.py
|
├── app.py
|
||||||
|
├── vnpy_bridge.py # CTP 执行层
|
||||||
|
├── recommend_store.py # 品种推荐缓存
|
||||||
|
├── recommend_stream.py # 品种推荐 SSE 推送
|
||||||
├── venv/
|
├── venv/
|
||||||
├── futures.db
|
├── futures.db
|
||||||
├── .env
|
├── .env
|
||||||
@@ -309,9 +377,11 @@ ss -tlnp | grep 6600
|
|||||||
├── data/fee_rates.json
|
├── data/fee_rates.json
|
||||||
├── ecosystem.config.cjs
|
├── ecosystem.config.cjs
|
||||||
├── deploy.sh
|
├── deploy.sh
|
||||||
|
├── requirements.txt # 含 vnpy、vnpy_ctp
|
||||||
└── docs/
|
└── docs/
|
||||||
├── FEATURES.md
|
├── FEATURES.md
|
||||||
└── DEPLOY.md
|
├── DEPLOY.md
|
||||||
|
└── TRADING.md
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -319,4 +389,5 @@ ss -tlnp | grep 6600
|
|||||||
## 相关文档
|
## 相关文档
|
||||||
|
|
||||||
- [功能说明文档](./FEATURES.md)
|
- [功能说明文档](./FEATURES.md)
|
||||||
|
- [交易与 SimNow 配置](./TRADING.md)
|
||||||
- [README](../README.md)
|
- [README](../README.md)
|
||||||
|
|||||||
+20
-6
@@ -9,14 +9,27 @@
|
|||||||
|
|
||||||
已移除「本地 SQLite 假撮合」;模拟盘与实盘均走 **vnpy_ctp**,仅 `.env` 前置与账号不同。
|
已移除「本地 SQLite 假撮合」;模拟盘与实盘均走 **vnpy_ctp**,仅 `.env` 前置与账号不同。
|
||||||
|
|
||||||
|
## 依赖安装
|
||||||
|
|
||||||
|
`requirements.txt` 已包含 `vnpy`、`vnpy_ctp`。服务器部署:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/qihuo
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
python -c "from vnpy_ctp import CtpGateway; print('OK')"
|
||||||
|
```
|
||||||
|
|
||||||
|
详见 [DEPLOY.md](./DEPLOY.md) 中「CTP / vnpy 故障排查」。
|
||||||
|
|
||||||
## 首次使用 SimNow
|
## 首次使用 SimNow
|
||||||
|
|
||||||
1. 在 [SimNow](https://www.simnow.com.cn/) 注册仿真账号
|
1. 在 [SimNow](https://www.simnow.com.cn/) 注册仿真账号
|
||||||
2. 复制 `.env.example` → `.env`,填写 `SIMNOW_USER`、`SIMNOW_PASSWORD`
|
2. 复制 `.env.example` → `.env`,填写 `SIMNOW_USER`、`SIMNOW_PASSWORD`
|
||||||
3. 核对 SimNow 官网最新的 **7×24 或交易时段** 前置地址
|
3. 核对 SimNow 官网最新的 **7×24 或交易时段** 前置地址
|
||||||
4. `pip install vnpy vnpy_ctp`
|
4. `pip install -r requirements.txt`(含 vnpy、vnpy_ctp)
|
||||||
5. 启动程序 → **期货下单** → 点击 **连接 CTP**
|
5. 启动程序 → **持仓监控** → 点击 **连接 CTP**
|
||||||
6. 连接成功后,权益、持仓、下单均来自 SimNow
|
6. 连接成功后,权益、持仓、下单均来自 SimNow 柜台
|
||||||
|
|
||||||
## 参考资金
|
## 参考资金
|
||||||
|
|
||||||
@@ -26,8 +39,7 @@
|
|||||||
|
|
||||||
| 页面 | 路径 |
|
| 页面 | 路径 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 品种推荐 | `/recommend` |
|
| 持仓监控(含下单、推荐) | `/positions` |
|
||||||
| 期货下单 | `/trade` |
|
|
||||||
| 策略交易 | `/strategy` |
|
| 策略交易 | `/strategy` |
|
||||||
| 策略记录 | `/strategy/records` |
|
| 策略记录 | `/strategy/records` |
|
||||||
|
|
||||||
@@ -37,4 +49,6 @@
|
|||||||
|------|------|
|
|------|------|
|
||||||
| `POST /api/ctp/connect` | 按当前模式连接 SimNow 或实盘 CTP |
|
| `POST /api/ctp/connect` | 按当前模式连接 SimNow 或实盘 CTP |
|
||||||
| `GET /api/ctp/status` | 连接状态与缺失配置项 |
|
| `GET /api/ctp/status` | 连接状态与缺失配置项 |
|
||||||
| `POST /api/trade/order` | 限价报单(需已连接 CTP) |
|
| `POST /api/trade/order` | 报单(限价/市价,需已连接 CTP) |
|
||||||
|
| `GET /api/trading/live` | 持仓(CTP 柜台数据) |
|
||||||
|
| `GET /api/recommend/stream` | 品种推荐 SSE 推送 |
|
||||||
|
|||||||
+37
-133
@@ -112,167 +112,62 @@ def install_trading(app, *, login_required, get_db, get_setting, set_setting, fe
|
|||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
tz = ZoneInfo("Asia/Shanghai")
|
tz = ZoneInfo("Asia/Shanghai")
|
||||||
now_iso = datetime.now(tz).strftime("%Y-%m-%dT%H:%M")
|
now_iso = datetime.now(tz).strftime("%Y-%m-%dT%H:%M")
|
||||||
capital = _capital(conn)
|
|
||||||
mode = get_trading_mode(get_setting)
|
mode = get_trading_mode(get_setting)
|
||||||
ctp_st = ctp_status(mode)
|
ctp_st = ctp_status(mode)
|
||||||
rows: list[dict] = []
|
rows: list[dict] = []
|
||||||
seen: set[str] = set()
|
|
||||||
|
|
||||||
ctp_pairs: list[tuple[str, str]] = []
|
if not ctp_st.get("connected"):
|
||||||
|
return rows
|
||||||
|
|
||||||
|
# 程序监控仅用于补充止损/止盈,持仓以 CTP 柜台为准
|
||||||
|
monitor_map: dict[tuple[str, str], dict] = {}
|
||||||
|
for r in conn.execute(
|
||||||
|
"SELECT * FROM trade_order_monitors WHERE status='active'"
|
||||||
|
).fetchall():
|
||||||
|
key = (r["symbol"].lower(), r["direction"])
|
||||||
|
monitor_map[key] = dict(r)
|
||||||
|
|
||||||
if ctp_st.get("connected"):
|
|
||||||
for p in _ctp_positions(mode):
|
for p in _ctp_positions(mode):
|
||||||
sym = (p.get("symbol") or "").strip()
|
sym = (p.get("symbol") or "").strip()
|
||||||
direction = p.get("direction") or "long"
|
direction = p.get("direction") or "long"
|
||||||
lots = int(p.get("lots") or 0)
|
lots = int(p.get("lots") or 0)
|
||||||
if lots <= 0:
|
if lots <= 0:
|
||||||
continue
|
continue
|
||||||
ctp_pairs.append((sym, direction))
|
|
||||||
key = f"ctp:{sym.lower()}:{direction}"
|
|
||||||
seen.add(key)
|
|
||||||
entry = float(p.get("avg_price") or 0)
|
entry = float(p.get("avg_price") or 0)
|
||||||
codes = ths_to_codes(sym)
|
|
||||||
mark = fetch_price(
|
|
||||||
sym,
|
|
||||||
codes.get("market_code", "") if codes else "",
|
|
||||||
codes.get("sina_code", "") if codes else "",
|
|
||||||
)
|
|
||||||
spec = get_contract_spec(sym)
|
|
||||||
mult = spec["mult"]
|
|
||||||
float_pnl = p.get("pnl")
|
float_pnl = p.get("pnl")
|
||||||
if mark is not None and entry > 0:
|
if float_pnl is not None:
|
||||||
if direction == "long":
|
float_pnl = round(float(float_pnl), 2)
|
||||||
float_pnl = round((mark - entry) * mult * lots, 2)
|
codes = ths_to_codes(sym)
|
||||||
else:
|
tick = calc_order_tick_metrics(sym, lots, entry)
|
||||||
float_pnl = round((entry - mark) * mult * lots, 2)
|
mon = None
|
||||||
tick = calc_order_tick_metrics(sym, lots, mark or entry)
|
for (ms, md), mv in monitor_map.items():
|
||||||
|
if md != direction:
|
||||||
|
continue
|
||||||
|
if ms == sym.lower() or _match_ctp_symbol(sym, ms):
|
||||||
|
mon = mv
|
||||||
|
break
|
||||||
|
sl = float(mon["stop_loss"]) if mon and mon.get("stop_loss") is not None else None
|
||||||
|
tp = float(mon["take_profit"]) if mon and mon.get("take_profit") is not None else None
|
||||||
rows.append({
|
rows.append({
|
||||||
"key": key,
|
"key": f"ctp:{sym.lower()}:{direction}",
|
||||||
"source": "ctp",
|
"source": "ctp",
|
||||||
"source_label": "CTP 柜台",
|
"source_label": "CTP 柜台",
|
||||||
|
"monitor_id": mon["id"] if mon else None,
|
||||||
"symbol": codes.get("name", sym) if codes else sym,
|
"symbol": codes.get("name", sym) if codes else sym,
|
||||||
"symbol_code": sym,
|
"symbol_code": sym,
|
||||||
"direction": direction,
|
"direction": direction,
|
||||||
"direction_label": "做多" if direction == "long" else "做空",
|
"direction_label": "做多" if direction == "long" else "做空",
|
||||||
"lots": lots,
|
"lots": lots,
|
||||||
"entry_price": entry,
|
"entry_price": entry,
|
||||||
"stop_loss": None,
|
"stop_loss": sl,
|
||||||
"take_profit": None,
|
"take_profit": tp,
|
||||||
"mark_price": mark,
|
"mark_price": None,
|
||||||
"float_pnl": float_pnl,
|
"float_pnl": float_pnl,
|
||||||
"tick_value_total": tick.get("tick_value_total"),
|
"tick_value_total": tick.get("tick_value_total"),
|
||||||
"price_precision": tick.get("price_precision"),
|
"price_precision": tick.get("price_precision"),
|
||||||
"tick_size": tick.get("tick_size"),
|
"tick_size": tick.get("tick_size"),
|
||||||
"can_close": True,
|
"can_close": True,
|
||||||
})
|
})
|
||||||
|
|
||||||
def _dup_ctp(ths_sym: str, direction: str) -> bool:
|
|
||||||
for cs, d in ctp_pairs:
|
|
||||||
if d != direction:
|
|
||||||
continue
|
|
||||||
if cs.lower() == ths_sym.lower() or _match_ctp_symbol(cs, ths_sym):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
monitors = conn.execute(
|
|
||||||
"SELECT * FROM trade_order_monitors WHERE status='active' ORDER BY id DESC"
|
|
||||||
).fetchall()
|
|
||||||
for r in monitors:
|
|
||||||
sym = r["symbol"]
|
|
||||||
direction = r["direction"]
|
|
||||||
if _dup_ctp(sym, direction):
|
|
||||||
continue
|
|
||||||
key = f"mon:{sym.lower()}:{direction}"
|
|
||||||
entry = float(r["entry_price"] or 0)
|
|
||||||
sl = float(r["stop_loss"]) if r["stop_loss"] is not None else None
|
|
||||||
tp = float(r["take_profit"]) if r["take_profit"] is not None else None
|
|
||||||
lots = float(r["lots"] or 1)
|
|
||||||
codes = ths_to_codes(sym)
|
|
||||||
market = r["market_code"] or (codes.get("market_code", "") if codes else "") or ""
|
|
||||||
sina = codes.get("sina_code", "") if codes else ""
|
|
||||||
mark = fetch_price(sym, market, sina)
|
|
||||||
metrics = calc_position_metrics(direction, entry, sl or entry, tp or entry, lots, mark, capital, sym)
|
|
||||||
fee_info = calc_fee_breakdown(sym, entry, mark or entry, lots, r["open_time"] or "", now_iso)
|
|
||||||
est_net = None
|
|
||||||
if metrics.get("float_pnl") is not None:
|
|
||||||
est_net = round(metrics["float_pnl"] - fee_info["total_fee"], 2)
|
|
||||||
rows.append({
|
|
||||||
"key": key,
|
|
||||||
"source": "program",
|
|
||||||
"source_label": r["monitor_type"] or "程序监控",
|
|
||||||
"monitor_id": r["id"],
|
|
||||||
"symbol": r["symbol_name"] or sym,
|
|
||||||
"symbol_code": sym,
|
|
||||||
"direction": direction,
|
|
||||||
"direction_label": "做多" if direction == "long" else "做空",
|
|
||||||
"lots": lots,
|
|
||||||
"entry_price": entry,
|
|
||||||
"stop_loss": sl,
|
|
||||||
"take_profit": tp,
|
|
||||||
"mark_price": mark,
|
|
||||||
"open_time": r["open_time"],
|
|
||||||
"holding_duration": _holding_duration(r["open_time"] or "", now_iso),
|
|
||||||
"float_pnl": metrics.get("float_pnl"),
|
|
||||||
"float_pct": metrics.get("float_pct"),
|
|
||||||
"risk_pct": metrics.get("risk_pct"),
|
|
||||||
"risk_amount": metrics.get("risk_amount"),
|
|
||||||
"rr_ratio": metrics.get("rr_ratio"),
|
|
||||||
"margin": metrics.get("margin"),
|
|
||||||
"position_pct": metrics.get("position_pct"),
|
|
||||||
"est_fee": fee_info["total_fee"],
|
|
||||||
"est_pnl_net": est_net,
|
|
||||||
"can_close": ctp_st.get("connected"),
|
|
||||||
})
|
|
||||||
|
|
||||||
legacy = conn.execute(
|
|
||||||
"SELECT * FROM position_monitors WHERE status='active' ORDER BY id DESC"
|
|
||||||
).fetchall()
|
|
||||||
for r in legacy:
|
|
||||||
sym = r["symbol"]
|
|
||||||
direction = r["direction"]
|
|
||||||
key = f"leg:{sym.lower()}:{direction}"
|
|
||||||
if any(x.get("symbol_code", "").lower() == sym.lower() and x.get("direction") == direction for x in rows):
|
|
||||||
continue
|
|
||||||
entry = float(r["entry_price"])
|
|
||||||
sl = float(r["stop_loss"])
|
|
||||||
tp = float(r["take_profit"])
|
|
||||||
lots = float(r["lots"] or 1)
|
|
||||||
market = r["market_code"] or ""
|
|
||||||
sina = r["sina_code"] or ""
|
|
||||||
mark = fetch_price(sym, market, sina)
|
|
||||||
metrics = calc_position_metrics(direction, entry, sl, tp, lots, mark, capital, sym)
|
|
||||||
fee_info = calc_fee_breakdown(sym, entry, mark or entry, lots, r["open_time"] or "", now_iso)
|
|
||||||
est_net = None
|
|
||||||
if metrics.get("float_pnl") is not None:
|
|
||||||
est_net = round(metrics["float_pnl"] - fee_info["total_fee"], 2)
|
|
||||||
rows.append({
|
|
||||||
"key": key,
|
|
||||||
"source": "legacy",
|
|
||||||
"source_label": "历史录入",
|
|
||||||
"legacy_id": r["id"],
|
|
||||||
"symbol": r["symbol_name"] or sym,
|
|
||||||
"symbol_code": sym,
|
|
||||||
"direction": direction,
|
|
||||||
"direction_label": "做多" if direction == "long" else "做空",
|
|
||||||
"lots": lots,
|
|
||||||
"entry_price": entry,
|
|
||||||
"stop_loss": sl,
|
|
||||||
"take_profit": tp,
|
|
||||||
"mark_price": mark,
|
|
||||||
"open_time": r["open_time"],
|
|
||||||
"holding_duration": _holding_duration(r["open_time"] or "", now_iso),
|
|
||||||
"float_pnl": metrics.get("float_pnl"),
|
|
||||||
"float_pct": metrics.get("float_pct"),
|
|
||||||
"risk_pct": metrics.get("risk_pct"),
|
|
||||||
"risk_amount": metrics.get("risk_amount"),
|
|
||||||
"rr_ratio": metrics.get("rr_ratio"),
|
|
||||||
"margin": metrics.get("margin"),
|
|
||||||
"position_pct": metrics.get("position_pct"),
|
|
||||||
"est_fee": fee_info["total_fee"],
|
|
||||||
"est_pnl_net": est_net,
|
|
||||||
"can_close": True,
|
|
||||||
"close_url": f"/close_position/{r['id']}",
|
|
||||||
})
|
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
@app.route("/trade")
|
@app.route("/trade")
|
||||||
@@ -516,6 +411,14 @@ def install_trading(app, *, login_required, get_db, get_setting, set_setting, fe
|
|||||||
price = float(d.get("price") or 0)
|
price = float(d.get("price") or 0)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return jsonify({"ok": False, "error": "手数或价格无效"}), 400
|
return jsonify({"ok": False, "error": "手数或价格无效"}), 400
|
||||||
|
order_type = (d.get("order_type") or d.get("price_type") or "limit").strip().lower()
|
||||||
|
if order_type == "market" and price <= 0:
|
||||||
|
codes = ths_to_codes(sym)
|
||||||
|
price = fetch_price(
|
||||||
|
sym,
|
||||||
|
codes.get("market_code", "") if codes else "",
|
||||||
|
codes.get("sina_code", "") if codes else "",
|
||||||
|
) or 0
|
||||||
if not sym or price <= 0:
|
if not sym or price <= 0:
|
||||||
return jsonify({"ok": False, "error": "品种或价格无效"}), 400
|
return jsonify({"ok": False, "error": "品种或价格无效"}), 400
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
@@ -547,6 +450,7 @@ def install_trading(app, *, login_required, get_db, get_setting, set_setting, fe
|
|||||||
lots=lots,
|
lots=lots,
|
||||||
price=price,
|
price=price,
|
||||||
settings=_settings_dict(),
|
settings=_settings_dict(),
|
||||||
|
order_type=order_type,
|
||||||
)
|
)
|
||||||
if offset.startswith("open"):
|
if offset.startswith("open"):
|
||||||
sl = d.get("stop_loss")
|
sl = d.get("stop_loss")
|
||||||
|
|||||||
+4
-2
@@ -4,5 +4,7 @@ python-dotenv==1.0.1
|
|||||||
Werkzeug==3.0.3
|
Werkzeug==3.0.3
|
||||||
matplotlib==3.9.2
|
matplotlib==3.9.2
|
||||||
akshare==1.18.64
|
akshare==1.18.64
|
||||||
# 实盘 / 模拟 CTP(SimNow + 期货公司)
|
|
||||||
# pip install vnpy vnpy_ctp
|
# CTP 下单:SimNow 模拟盘 / 期货公司实盘(见 docs/DEPLOY.md)
|
||||||
|
vnpy>=3.9.0
|
||||||
|
vnpy_ctp>=6.7.11.4
|
||||||
|
|||||||
+17
-12
@@ -3,30 +3,35 @@
|
|||||||
.trade-dashboard{display:flex;flex-direction:column;gap:1.25rem}
|
.trade-dashboard{display:flex;flex-direction:column;gap:1.25rem}
|
||||||
.trade-row-split{display:grid;grid-template-columns:1fr 1fr;gap:1.25rem;align-items:stretch}
|
.trade-row-split{display:grid;grid-template-columns:1fr 1fr;gap:1.25rem;align-items:stretch}
|
||||||
.trade-card{margin-bottom:0;height:100%;display:flex;flex-direction:column}
|
.trade-card{margin-bottom:0;height:100%;display:flex;flex-direction:column}
|
||||||
.trade-card h2{margin-bottom:.65rem;flex-shrink:0}
|
.trade-card h2{margin-bottom:.35rem;flex-shrink:0}
|
||||||
.trade-card .card-body{flex:1;min-height:0}
|
.trade-card .card-body{flex:1;min-height:0}
|
||||||
.trade-card-full{margin-bottom:0}
|
.trade-card-full{margin-bottom:0}
|
||||||
|
.pos-hint{font-size:.75rem;margin:-.15rem 0 .5rem .25rem;color:var(--text-muted)}
|
||||||
.trade-order-status{display:grid;gap:.55rem;margin:.5rem 0 .75rem;padding:.65rem .85rem;background:var(--card-inner);border:1px solid var(--card-border);border-radius:8px;font-size:.82rem}
|
.trade-order-status{display:grid;gap:.55rem;margin:.5rem 0 .75rem;padding:.65rem .85rem;background:var(--card-inner);border:1px solid var(--card-border);border-radius:8px;font-size:.82rem}
|
||||||
.trade-order-status-compact{margin-top:0}
|
.trade-order-status-compact{margin-top:0}
|
||||||
.trade-order-status .status-row{display:flex;flex-wrap:wrap;align-items:center;gap:.35rem .65rem}
|
.trade-order-status .status-row{display:flex;flex-wrap:wrap;align-items:center;gap:.35rem .65rem}
|
||||||
.trade-input-row,.trade-risk-row{display:grid;grid-template-columns:2fr 1fr 1fr;gap:.65rem;margin-bottom:.75rem}
|
.trade-form-grid{display:grid;grid-template-columns:1fr 1fr;gap:.65rem;margin-bottom:.75rem}
|
||||||
|
.trade-form-grid .span-2{grid-column:span 2}
|
||||||
.trade-field label{display:block;font-size:.72rem;margin-bottom:.25rem;color:var(--text-label)}
|
.trade-field label{display:block;font-size:.72rem;margin-bottom:.25rem;color:var(--text-label)}
|
||||||
.trade-btn-row{display:grid;grid-template-columns:repeat(4,1fr);gap:.5rem;margin:.75rem 0}
|
.trade-field select,.trade-field input{width:100%}
|
||||||
.trade-btn{border:none;border-radius:8px;padding:.65rem .3rem;cursor:pointer;display:flex;flex-direction:column;align-items:center;gap:.12rem;color:#fff;font-weight:600}
|
.price-type-tabs{display:flex;gap:.35rem;margin-bottom:.35rem}
|
||||||
.trade-btn .btn-price{font-size:1rem}
|
.price-tab{border:1px solid var(--card-border);background:var(--card-inner);color:var(--text-muted);padding:.25rem .65rem;border-radius:6px;font-size:.75rem;cursor:pointer}
|
||||||
.trade-btn .btn-label{font-size:.82rem}
|
.price-tab.active{border-color:var(--accent);color:var(--accent);font-weight:600}
|
||||||
.trade-btn .btn-sub{font-size:.66rem;opacity:.85;font-weight:400}
|
.market-hint{font-size:.7rem;margin-top:.25rem}
|
||||||
.trade-btn.long{background:linear-gradient(180deg,#e74c3c,#c0392b)}
|
.calc-lots-row{display:flex;gap:.4rem}
|
||||||
.trade-btn.lock{background:linear-gradient(180deg,#27ae60,#1e8449)}
|
.calc-lots-row input{flex:1}
|
||||||
.trade-btn.close{background:linear-gradient(180deg,#3498db,#2980b9)}
|
.calc-lots-row .btn-secondary{padding:.35rem .6rem;font-size:.75rem;white-space:nowrap}
|
||||||
|
.trade-action-row{display:flex;gap:.65rem;margin:.75rem 0 .5rem}
|
||||||
|
.trade-action-row .btn-open{flex:1;padding:.65rem}
|
||||||
.trade-footer{background:var(--card-inner);border-radius:8px;padding:.65rem .85rem;font-size:.78rem;line-height:1.5;border:1px solid var(--card-border);margin-top:.5rem}
|
.trade-footer{background:var(--card-inner);border-radius:8px;padding:.65rem .85rem;font-size:.78rem;line-height:1.5;border:1px solid var(--card-border);margin-top:.5rem}
|
||||||
.trade-footer strong{color:var(--accent)}
|
.trade-footer strong{color:var(--accent)}
|
||||||
.rec-blocked td{opacity:.55}
|
.rec-blocked td{opacity:.55}
|
||||||
.rec-ok td:first-child{font-weight:600}
|
.rec-ok td:first-child{font-weight:600}
|
||||||
#positions .card-body{max-height:480px;overflow-y:auto}
|
#positions .card-body{max-height:460px;overflow-y:auto}
|
||||||
|
|
||||||
@media (max-width:900px){
|
@media (max-width:900px){
|
||||||
.trade-row-split{grid-template-columns:1fr}
|
.trade-row-split{grid-template-columns:1fr}
|
||||||
#positions .card-body{max-height:360px}
|
#positions .card-body{max-height:360px}
|
||||||
.trade-btn-row{grid-template-columns:repeat(2,1fr)}
|
.trade-form-grid{grid-template-columns:1fr}
|
||||||
|
.trade-form-grid .span-2{grid-column:span 1}
|
||||||
}
|
}
|
||||||
|
|||||||
+150
-90
@@ -1,15 +1,21 @@
|
|||||||
(function () {
|
(function () {
|
||||||
|
var sizingMode = window.TRADE_SIZING_MODE || 'risk';
|
||||||
var list = document.getElementById('position-live-list');
|
var list = document.getElementById('position-live-list');
|
||||||
var recommendList = document.getElementById('recommend-list');
|
var recommendList = document.getElementById('recommend-list');
|
||||||
var symInput = document.getElementById('trade-symbol');
|
var symInput = document.getElementById('trade-symbol');
|
||||||
|
var dirSelect = document.getElementById('trade-direction');
|
||||||
var lotsInput = document.getElementById('trade-lots');
|
var lotsInput = document.getElementById('trade-lots');
|
||||||
|
var lotsCalc = document.getElementById('trade-lots-calc');
|
||||||
var priceInput = document.getElementById('trade-price');
|
var priceInput = document.getElementById('trade-price');
|
||||||
var footer = document.getElementById('trade-footer');
|
|
||||||
var slInput = document.getElementById('trade-sl');
|
var slInput = document.getElementById('trade-sl');
|
||||||
var tpInput = document.getElementById('trade-tp');
|
var tpInput = document.getElementById('trade-tp');
|
||||||
|
var marketHint = document.getElementById('market-hint');
|
||||||
|
var metricsHint = document.getElementById('trade-metrics-hint');
|
||||||
var pollTimer = null;
|
var pollTimer = null;
|
||||||
var recommendSource = null;
|
var recommendSource = null;
|
||||||
var quoteTimer = null;
|
var quoteTimer = null;
|
||||||
|
var lastQuotePrice = null;
|
||||||
|
var priceType = 'limit';
|
||||||
|
|
||||||
function runWhenReady(fn) {
|
function runWhenReady(fn) {
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
@@ -28,37 +34,54 @@
|
|||||||
return (symInput && symInput.value || '').trim();
|
return (symInput && symInput.value || '').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isRiskMode() {
|
||||||
|
return sizingMode === 'risk';
|
||||||
|
}
|
||||||
|
|
||||||
|
function effectiveLots() {
|
||||||
|
if (isRiskMode()) {
|
||||||
|
var v = parseInt(lotsCalc && lotsCalc.value, 10);
|
||||||
|
return v > 0 ? v : 0;
|
||||||
|
}
|
||||||
|
return parseInt(lotsInput && lotsInput.value, 10) || 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function entryPrice() {
|
||||||
|
if (priceType === 'market') return lastQuotePrice;
|
||||||
|
return parseFloat(priceInput && priceInput.value) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPriceType(type) {
|
||||||
|
priceType = type === 'market' ? 'market' : 'limit';
|
||||||
|
document.querySelectorAll('.price-tab').forEach(function (btn) {
|
||||||
|
btn.classList.toggle('active', btn.getAttribute('data-type') === priceType);
|
||||||
|
});
|
||||||
|
if (priceInput) {
|
||||||
|
priceInput.disabled = priceType === 'market';
|
||||||
|
if (priceType === 'market' && lastQuotePrice) priceInput.value = lastQuotePrice;
|
||||||
|
}
|
||||||
|
if (marketHint) marketHint.hidden = priceType !== 'market';
|
||||||
|
}
|
||||||
|
|
||||||
function refreshQuote() {
|
function refreshQuote() {
|
||||||
var sym = selectedSymbol();
|
var sym = selectedSymbol();
|
||||||
var lots = lotsInput ? lotsInput.value : '1';
|
var lots = isRiskMode() ? (effectiveLots() || 1) : (lotsInput ? lotsInput.value : '1');
|
||||||
if (!sym) return;
|
if (!sym) return;
|
||||||
fetch('/api/trade/quote?symbol=' + encodeURIComponent(sym) + '&lots=' + encodeURIComponent(lots))
|
fetch('/api/trade/quote?symbol=' + encodeURIComponent(sym) + '&lots=' + encodeURIComponent(lots))
|
||||||
.then(function (r) { return r.json(); })
|
.then(function (r) { return r.json(); })
|
||||||
.then(function (data) {
|
.then(function (data) {
|
||||||
if (!data.ok) return;
|
if (!data.ok) return;
|
||||||
if (priceInput && !priceInput.dataset.manual && data.price) {
|
lastQuotePrice = data.price;
|
||||||
|
if (priceType === 'market' && priceInput && data.price) {
|
||||||
|
priceInput.value = data.price;
|
||||||
|
} else if (priceInput && !priceInput.dataset.manual && data.price) {
|
||||||
priceInput.value = data.price;
|
priceInput.value = data.price;
|
||||||
}
|
}
|
||||||
var px = data.price != null ? data.price : '—';
|
if (metricsHint && data.metrics) {
|
||||||
['px-long', 'px-short'].forEach(function (id) {
|
|
||||||
var el = document.getElementById(id);
|
|
||||||
if (el) el.textContent = px;
|
|
||||||
});
|
|
||||||
var pl = document.getElementById('pos-long');
|
|
||||||
var ps = document.getElementById('pos-short');
|
|
||||||
if (pl) pl.textContent = '≤' + (data.pos_long || 0);
|
|
||||||
if (ps) ps.textContent = '≤' + (data.pos_short || 0);
|
|
||||||
if (footer && data.metrics) {
|
|
||||||
var m = data.metrics;
|
var m = data.metrics;
|
||||||
var hint = footer.querySelector('.hint');
|
metricsHint.innerHTML =
|
||||||
var extra =
|
'<strong>' + (data.name || sym) + '</strong> 精度 ' + m.price_precision +
|
||||||
'<p><strong>' + (data.name || sym) + '</strong> 精度 <strong>' + m.price_precision +
|
' 位 · 每跳 <strong class="text-accent">' + m.tick_value_total + '</strong> 元(' + lots + ' 手)';
|
||||||
'</strong> 位 · 每跳 <strong class="text-accent">' + m.tick_value_total + '</strong> 元(' + lots + ' 手)</p>';
|
|
||||||
if (hint) {
|
|
||||||
hint.insertAdjacentHTML('afterend', extra);
|
|
||||||
var olds = footer.querySelectorAll('p:not(.hint):not(.text-loss)');
|
|
||||||
for (var i = 0; i < olds.length - 1; i++) olds[i].remove();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}).catch(function () {});
|
}).catch(function () {});
|
||||||
}
|
}
|
||||||
@@ -68,17 +91,63 @@
|
|||||||
quoteTimer = setTimeout(refreshQuote, 400);
|
quoteTimer = setTimeout(refreshQuote, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
function postOrder(offset, direction) {
|
function calcLotsPreview() {
|
||||||
|
var sym = selectedSymbol();
|
||||||
|
var entry = entryPrice() || parseFloat(priceInput && priceInput.value) || 0;
|
||||||
|
var sl = parseFloat(slInput && slInput.value) || 0;
|
||||||
|
if (!sym || !entry || !sl) {
|
||||||
|
alert('请填写品种、入场价与止损');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetch('/api/trade/preview', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
symbol: sym,
|
||||||
|
direction: dirSelect ? dirSelect.value : 'long',
|
||||||
|
entry: entry,
|
||||||
|
price: entry,
|
||||||
|
stop_loss: sl,
|
||||||
|
take_profit: parseFloat(tpInput && tpInput.value) || 0
|
||||||
|
})
|
||||||
|
}).then(function (r) { return r.json(); }).then(function (data) {
|
||||||
|
if (!data.ok) { alert(data.error || '计算失败'); return; }
|
||||||
|
if (lotsCalc) lotsCalc.value = data.lots;
|
||||||
|
scheduleQuote();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function postOrder(offset) {
|
||||||
var sym = selectedSymbol();
|
var sym = selectedSymbol();
|
||||||
if (!sym) { alert('请选择品种'); return; }
|
if (!sym) { alert('请选择品种'); return; }
|
||||||
|
var direction = dirSelect ? dirSelect.value : 'long';
|
||||||
|
var price = entryPrice();
|
||||||
|
if (!price || price <= 0) {
|
||||||
|
alert('无法获取有效价格,请先填写或刷新行情');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var lots = effectiveLots();
|
||||||
|
if (offset === 'open') {
|
||||||
|
if (isRiskMode() && lots <= 0) {
|
||||||
|
alert('请先点击「计算」得到手数');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isRiskMode() && lots <= 0) {
|
||||||
|
alert('请填写手数');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lots = parseInt(lotsInput && lotsInput.value, 10) || 1;
|
||||||
|
}
|
||||||
var body = {
|
var body = {
|
||||||
symbol: sym,
|
symbol: sym,
|
||||||
offset: offset,
|
offset: offset,
|
||||||
direction: direction,
|
direction: direction,
|
||||||
lots: parseInt(lotsInput.value, 10) || 1,
|
lots: lots,
|
||||||
price: parseFloat(priceInput.value) || 0,
|
price: price,
|
||||||
stop_loss: slInput ? parseFloat(slInput.value) : null,
|
order_type: priceType,
|
||||||
take_profit: tpInput ? parseFloat(tpInput.value) : null
|
stop_loss: slInput && slInput.value ? parseFloat(slInput.value) : null,
|
||||||
|
take_profit: tpInput && tpInput.value ? parseFloat(tpInput.value) : null
|
||||||
};
|
};
|
||||||
fetch('/api/trade/order', {
|
fetch('/api/trade/order', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -86,7 +155,7 @@
|
|||||||
body: JSON.stringify(body)
|
body: JSON.stringify(body)
|
||||||
}).then(function (r) { return r.json(); }).then(function (data) {
|
}).then(function (r) { return r.json(); }).then(function (data) {
|
||||||
if (!data.ok) { alert(data.error || '下单失败'); return; }
|
if (!data.ok) { alert(data.error || '下单失败'); return; }
|
||||||
alert('已提交 ' + (data.lots || '') + ' 手');
|
alert((offset === 'open' ? '开仓' : '平仓') + '已提交 ' + (data.lots || lots) + ' 手');
|
||||||
pollPositions();
|
pollPositions();
|
||||||
refreshQuote();
|
refreshQuote();
|
||||||
});
|
});
|
||||||
@@ -94,51 +163,48 @@
|
|||||||
|
|
||||||
function buildPosCard(row) {
|
function buildPosCard(row) {
|
||||||
var pnlClass = row.float_pnl > 0 ? 'pnl-pos' : (row.float_pnl < 0 ? 'pnl-neg' : '');
|
var pnlClass = row.float_pnl > 0 ? 'pnl-pos' : (row.float_pnl < 0 ? 'pnl-neg' : '');
|
||||||
var pnlText = '--';
|
var pnlText = row.float_pnl != null ? ((row.float_pnl >= 0 ? '+' : '') + fmtNum(row.float_pnl) + ' 元') : '--';
|
||||||
if (row.float_pnl != null) {
|
|
||||||
var sign = row.float_pnl >= 0 ? '+' : '';
|
|
||||||
pnlText = sign + fmtNum(row.float_pnl) + '元';
|
|
||||||
if (row.float_pct != null) pnlText += ' (' + sign + fmtNum(row.float_pct) + '%)';
|
|
||||||
}
|
|
||||||
var rr = row.rr_ratio != null ? row.rr_ratio + ':1' : '--';
|
|
||||||
var openT = (row.open_time || '').replace('T', ' ').slice(0, 16);
|
|
||||||
var dirBadge = row.direction_label || (row.direction === 'long' ? '做多' : '做空');
|
var dirBadge = row.direction_label || (row.direction === 'long' ? '做多' : '做空');
|
||||||
var closeBtn = '';
|
var closeBtn = row.can_close ?
|
||||||
if (row.close_url) {
|
'<button type="button" class="btn-del pos-del" data-close=\'' + JSON.stringify({
|
||||||
closeBtn = '<form method="post" action="' + row.close_url + '" style="display:inline" onsubmit="return confirm(\'确认平仓?\')">' +
|
|
||||||
'<button type="submit" class="btn-del pos-del">平仓</button></form>';
|
|
||||||
} else if (row.can_close) {
|
|
||||||
closeBtn = '<button type="button" class="btn-del pos-del" data-close=\'' + JSON.stringify({
|
|
||||||
source: row.source, symbol_code: row.symbol_code, direction: row.direction,
|
source: row.source, symbol_code: row.symbol_code, direction: row.direction,
|
||||||
lots: row.lots, mark_price: row.mark_price, monitor_id: row.monitor_id || null
|
lots: row.lots, mark_price: row.mark_price, monitor_id: row.monitor_id || null
|
||||||
}) + '\'>平仓</button>';
|
}) + '\'>平仓</button>' : '';
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
'<div class="pos-card" data-key="' + (row.key || '') + '">' +
|
'<div class="pos-card">' +
|
||||||
'<div class="pos-card-head"><div><div class="title">' + row.symbol + ' <span class="badge dir">' + dirBadge + '</span></div></div>' + closeBtn + '</div>' +
|
'<div class="pos-card-head"><div><div class="title">' + row.symbol + ' <span class="badge dir">' + dirBadge + '</span></div>' +
|
||||||
'<div class="pos-card-meta">来源 <strong>' + (row.source_label || row.source) + '</strong></div>' +
|
'<div class="text-muted" style="font-size:.72rem">' + (row.symbol_code || '') + '</div></div>' + closeBtn + '</div>' +
|
||||||
|
'<div class="pos-card-meta">来源 <strong>' + (row.source_label || 'CTP') + '</strong> · 柜台浮盈</div>' +
|
||||||
'<div class="pos-metrics">' +
|
'<div class="pos-metrics">' +
|
||||||
'<div class="cell"><label>成交价</label><div>' + fmtNum(row.entry_price) + '</div></div>' +
|
'<div class="cell"><label>持仓均价</label><div>' + fmtNum(row.entry_price) + '</div></div>' +
|
||||||
'<div class="cell"><label>止损</label><div>' + (row.stop_loss != null ? fmtNum(row.stop_loss) : '--') + '</div></div>' +
|
'<div class="cell"><label>止损</label><div>' + (row.stop_loss != null ? fmtNum(row.stop_loss) : '--') + '</div></div>' +
|
||||||
'<div class="cell"><label>止盈</label><div>' + (row.take_profit != null ? fmtNum(row.take_profit) : '--') + '</div></div>' +
|
'<div class="cell"><label>止盈</label><div>' + (row.take_profit != null ? fmtNum(row.take_profit) : '--') + '</div></div>' +
|
||||||
'<div class="cell"><label>浮盈亏</label><div class="' + pnlClass + '">' + pnlText + '</div></div>' +
|
'<div class="cell ' + pnlClass + '"><label>浮盈亏</label><div>' + pnlText + '</div></div>' +
|
||||||
'</div><div class="pos-footer"><span>张数 ' + row.lots + '</span></div></div>'
|
'</div><div class="pos-footer"><span>' + row.lots + ' 手</span></div></div>'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function closePosition(payload) {
|
function closePosition(payload) {
|
||||||
var price = payload.mark_price;
|
function doClose(price) {
|
||||||
if (!price || price <= 0) { alert('无法获取现价'); return; }
|
if (!price || price <= 0) { alert('无法获取现价'); return; }
|
||||||
if (!confirm('确认以 ' + price + ' 限价平仓 ' + payload.lots + ' 手?')) return;
|
if (!confirm('确认平仓 ' + payload.lots + ' 手?')) return;
|
||||||
fetch('/api/trading/close', {
|
fetch('/api/trading/close', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(Object.assign({}, payload, { price: price }))
|
||||||
}).then(function (r) { return r.json(); }).then(function (d) {
|
}).then(function (r) { return r.json(); }).then(function (d) {
|
||||||
if (!d.ok) { alert(d.error || '平仓失败'); return; }
|
if (!d.ok) { alert(d.error || '平仓失败'); return; }
|
||||||
pollPositions();
|
pollPositions();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (payload.mark_price > 0) {
|
||||||
|
doClose(payload.mark_price);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetch('/api/trade/quote?symbol=' + encodeURIComponent(payload.symbol_code) + '&lots=' + payload.lots)
|
||||||
|
.then(function (r) { return r.json(); })
|
||||||
|
.then(function (d) { doClose(d.price); });
|
||||||
|
}
|
||||||
|
|
||||||
function pollPositions() {
|
function pollPositions() {
|
||||||
if (!list) return;
|
if (!list) return;
|
||||||
@@ -150,21 +216,18 @@
|
|||||||
.then(function (data) {
|
.then(function (data) {
|
||||||
var cap = document.getElementById('cap-display');
|
var cap = document.getElementById('cap-display');
|
||||||
if (cap && data.capital != null) cap.textContent = Number(data.capital).toFixed(2);
|
if (cap && data.capital != null) cap.textContent = Number(data.capital).toFixed(2);
|
||||||
var recCap = document.getElementById('rec-capital');
|
|
||||||
if (recCap && data.capital != null) recCap.textContent = Number(data.capital).toFixed(2);
|
|
||||||
var riskBadge = document.getElementById('risk-badge');
|
|
||||||
if (riskBadge && data.risk_status) {
|
|
||||||
riskBadge.textContent = data.risk_status.status_label;
|
|
||||||
riskBadge.className = 'badge ' + (data.risk_status.can_trade ? 'profit' : 'loss');
|
|
||||||
}
|
|
||||||
var ctpBadge = document.getElementById('ctp-badge');
|
var ctpBadge = document.getElementById('ctp-badge');
|
||||||
if (ctpBadge && data.ctp_status) {
|
if (ctpBadge && data.ctp_status) {
|
||||||
ctpBadge.textContent = data.ctp_status.connected ? 'CTP 已连接' : 'CTP 未连接';
|
ctpBadge.textContent = data.ctp_status.connected ? 'CTP 已连接' : 'CTP 未连接';
|
||||||
ctpBadge.className = 'badge ' + (data.ctp_status.connected ? 'profit' : 'planned');
|
ctpBadge.className = 'badge ' + (data.ctp_status.connected ? 'profit' : 'planned');
|
||||||
}
|
}
|
||||||
var rows = data.rows || [];
|
var rows = data.rows || [];
|
||||||
|
if (!data.ctp_status || !data.ctp_status.connected) {
|
||||||
|
list.innerHTML = '<div class="empty-hint">请先连接 CTP,持仓将显示柜台实际数据。</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!rows.length) {
|
if (!rows.length) {
|
||||||
list.innerHTML = '<div class="empty-hint">暂无持仓。可在左侧下单,或通过策略交易开仓。</div>';
|
list.innerHTML = '<div class="empty-hint">柜台暂无持仓。</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
list.innerHTML = rows.map(buildPosCard).join('');
|
list.innerHTML = rows.map(buildPosCard).join('');
|
||||||
@@ -181,17 +244,10 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function badgeClass(status) {
|
|
||||||
if (status === 'ok') return 'profit';
|
|
||||||
return 'planned';
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderRecommendations(data) {
|
function renderRecommendations(data) {
|
||||||
if (!recommendList || !data) return;
|
if (!recommendList || !data) return;
|
||||||
var recCap = document.getElementById('rec-capital');
|
var recCap = document.getElementById('rec-capital');
|
||||||
if (recCap && data.capital != null) recCap.textContent = Number(data.capital).toFixed(2);
|
if (recCap && data.capital != null) recCap.textContent = Number(data.capital).toFixed(2);
|
||||||
var recUpd = document.getElementById('rec-updated');
|
|
||||||
if (recUpd && data.updated_at) recUpd.textContent = '更新 ' + data.updated_at;
|
|
||||||
var rows = data.rows || [];
|
var rows = data.rows || [];
|
||||||
if (!rows.length) {
|
if (!rows.length) {
|
||||||
recommendList.innerHTML = '<tr><td colspan="6" class="empty-hint">当前资金下暂无推荐品种</td></tr>';
|
recommendList.innerHTML = '<tr><td colspan="6" class="empty-hint">当前资金下暂无推荐品种</td></tr>';
|
||||||
@@ -202,49 +258,52 @@
|
|||||||
'<tr class="rec-' + (r.status || '') + '">' +
|
'<tr class="rec-' + (r.status || '') + '">' +
|
||||||
'<td><strong>' + (r.name || '') + '</strong> <span class="text-muted">' + (r.ths || '') + '</span></td>' +
|
'<td><strong>' + (r.name || '') + '</strong> <span class="text-muted">' + (r.ths || '') + '</span></td>' +
|
||||||
'<td>' + (r.exchange || '') + '</td>' +
|
'<td>' + (r.exchange || '') + '</td>' +
|
||||||
'<td class="rec-price">' + (r.price != null ? r.price : '—') + '</td>' +
|
'<td>' + (r.price != null ? r.price : '—') + '</td>' +
|
||||||
'<td>' + (r.margin_one_lot != null ? r.margin_one_lot : '—') + '</td>' +
|
'<td>' + (r.margin_one_lot != null ? r.margin_one_lot : '—') + '</td>' +
|
||||||
'<td>' + (r.min_capital_one_lot != null ? r.min_capital_one_lot : '—') + '</td>' +
|
'<td>' + (r.min_capital_one_lot != null ? r.min_capital_one_lot : '—') + '</td>' +
|
||||||
'<td><span class="badge ' + badgeClass(r.status) + '">' + (r.status_label || '') + '</span></td>' +
|
'<td><span class="badge ' + (r.status === 'ok' ? 'profit' : 'planned') + '">' + (r.status_label || '') + '</span></td>' +
|
||||||
'</tr>'
|
'</tr>'
|
||||||
);
|
);
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
function connectRecommendStream() {
|
function connectRecommendStream() {
|
||||||
if (recommendSource) {
|
if (recommendSource) { recommendSource.close(); recommendSource = null; }
|
||||||
recommendSource.close();
|
|
||||||
recommendSource = null;
|
|
||||||
}
|
|
||||||
recommendSource = new EventSource('/api/recommend/stream');
|
recommendSource = new EventSource('/api/recommend/stream');
|
||||||
recommendSource.addEventListener('recommend', function (ev) {
|
recommendSource.addEventListener('recommend', function (ev) {
|
||||||
try {
|
try { renderRecommendations(JSON.parse(ev.data)); } catch (e) { /* ignore */ }
|
||||||
renderRecommendations(JSON.parse(ev.data));
|
|
||||||
} catch (e) { /* ignore */ }
|
|
||||||
});
|
});
|
||||||
recommendSource.onerror = function () {
|
recommendSource.onerror = function () {
|
||||||
if (recommendSource) {
|
if (recommendSource) { recommendSource.close(); recommendSource = null; }
|
||||||
recommendSource.close();
|
|
||||||
recommendSource = null;
|
|
||||||
}
|
|
||||||
setTimeout(connectRecommendStream, 5000);
|
setTimeout(connectRecommendStream, 5000);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('.price-tab').forEach(function (btn) {
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
setPriceType(btn.getAttribute('data-type'));
|
||||||
|
scheduleQuote();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
if (symInput) symInput.addEventListener('input', scheduleQuote);
|
if (symInput) symInput.addEventListener('input', scheduleQuote);
|
||||||
if (lotsInput) lotsInput.addEventListener('input', scheduleQuote);
|
if (lotsInput) lotsInput.addEventListener('input', scheduleQuote);
|
||||||
|
if (slInput) slInput.addEventListener('input', function () {
|
||||||
|
if (isRiskMode() && lotsCalc) lotsCalc.value = '';
|
||||||
|
});
|
||||||
if (priceInput) {
|
if (priceInput) {
|
||||||
priceInput.addEventListener('input', function () { priceInput.dataset.manual = '1'; });
|
priceInput.addEventListener('input', function () {
|
||||||
|
if (priceType === 'limit') priceInput.dataset.manual = '1';
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var btnLong = document.getElementById('btn-open-long');
|
var btnCalc = document.getElementById('btn-calc-lots');
|
||||||
var btnShort = document.getElementById('btn-open-short');
|
if (btnCalc) btnCalc.addEventListener('click', calcLotsPreview);
|
||||||
var btnCloseL = document.getElementById('btn-close-long');
|
|
||||||
var btnCloseS = document.getElementById('btn-close-short');
|
var btnOpen = document.getElementById('btn-open');
|
||||||
if (btnLong) btnLong.addEventListener('click', function () { postOrder('open', 'long'); });
|
var btnClose = document.getElementById('btn-close-pos');
|
||||||
if (btnShort) btnShort.addEventListener('click', function () { postOrder('open', 'short'); });
|
if (btnOpen) btnOpen.addEventListener('click', function () { postOrder('open'); });
|
||||||
if (btnCloseL) btnCloseL.addEventListener('click', function () { postOrder('close', 'long'); });
|
if (btnClose) btnClose.addEventListener('click', function () { postOrder('close'); });
|
||||||
if (btnCloseS) btnCloseS.addEventListener('click', function () { postOrder('close', 'short'); });
|
|
||||||
|
|
||||||
var btnConnect = document.getElementById('btn-ctp-connect');
|
var btnConnect = document.getElementById('btn-ctp-connect');
|
||||||
if (btnConnect) {
|
if (btnConnect) {
|
||||||
@@ -265,6 +324,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
runWhenReady(function () {
|
runWhenReady(function () {
|
||||||
|
setPriceType('limit');
|
||||||
pollPositions();
|
pollPositions();
|
||||||
connectRecommendStream();
|
connectRecommendStream();
|
||||||
pollTimer = setInterval(pollPositions, 3000);
|
pollTimer = setInterval(pollPositions, 3000);
|
||||||
|
|||||||
+49
-33
@@ -26,64 +26,80 @@
|
|||||||
<div class="trade-order-status trade-order-status-compact">
|
<div class="trade-order-status trade-order-status-compact">
|
||||||
<div class="status-row">
|
<div class="status-row">
|
||||||
<span class="text-muted">计仓</span>
|
<span class="text-muted">计仓</span>
|
||||||
<strong>{{ sizing_mode_label }}</strong>
|
<strong id="sizing-label">{{ sizing_mode_label }}</strong>
|
||||||
{% if sizing_mode == 'risk' %}<span class="text-muted">· {{ risk_percent }}%</span>{% endif %}
|
{% if sizing_mode == 'risk' %}<span class="text-muted">· 单笔风险 {{ risk_percent }}%</span>{% endif %}
|
||||||
<span class="text-muted">· 监控 {{ monitor_count }} 笔</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="trade-input-row">
|
<div class="trade-form-grid">
|
||||||
<div class="symbol-wrap trade-field">
|
<div class="symbol-wrap trade-field span-2">
|
||||||
<label class="text-label">品种</label>
|
<label class="text-label">品种</label>
|
||||||
<input type="text" id="trade-symbol" class="symbol-input" placeholder="主力合约 rb2610" autocomplete="off">
|
<input type="text" id="trade-symbol" class="symbol-input" placeholder="主力合约 rb2610" autocomplete="off">
|
||||||
<div class="symbol-dropdown"></div>
|
<div class="symbol-dropdown"></div>
|
||||||
<div class="symbol-selected" id="sym-selected"></div>
|
<div class="symbol-selected" id="sym-selected"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="trade-field">
|
<div class="trade-field">
|
||||||
|
<label class="text-label">方向</label>
|
||||||
|
<select id="trade-direction">
|
||||||
|
<option value="long">做多</option>
|
||||||
|
<option value="short">做空</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="trade-field" id="field-lots" {% if sizing_mode == 'risk' %}hidden{% endif %}>
|
||||||
<label class="text-label">手数</label>
|
<label class="text-label">手数</label>
|
||||||
<input type="number" id="trade-lots" min="1" step="1" value="1">
|
<input type="number" id="trade-lots" min="1" step="1" value="1">
|
||||||
</div>
|
</div>
|
||||||
<div class="trade-field">
|
|
||||||
<label class="text-label">价格</label>
|
<div class="trade-field span-2">
|
||||||
|
<label class="text-label">入场价</label>
|
||||||
|
<div class="price-type-tabs">
|
||||||
|
<button type="button" class="price-tab active" data-type="limit">限价</button>
|
||||||
|
<button type="button" class="price-tab" data-type="market">市价</button>
|
||||||
|
</div>
|
||||||
<input type="number" id="trade-price" step="any" placeholder="限价">
|
<input type="number" id="trade-price" step="any" placeholder="限价">
|
||||||
|
<p class="hint market-hint" id="market-hint" hidden>市价将按最新行情价报单</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="trade-field">
|
||||||
|
<label class="text-label">止损</label>
|
||||||
|
<input type="number" id="trade-sl" step="any">
|
||||||
|
</div>
|
||||||
|
<div class="trade-field">
|
||||||
|
<label class="text-label">止盈</label>
|
||||||
|
<input type="number" id="trade-tp" step="any">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="trade-field" id="field-calc-lots" {% if sizing_mode != 'risk' %}hidden{% endif %}>
|
||||||
|
<label class="text-label">计算手数</label>
|
||||||
|
<div class="calc-lots-row">
|
||||||
|
<input type="text" id="trade-lots-calc" readonly placeholder="填写止损后计算">
|
||||||
|
<button type="button" class="btn-secondary" id="btn-calc-lots">计算</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="risk-fields" class="trade-risk-row" {% if sizing_mode != 'risk' %}hidden{% endif %}>
|
<div class="trade-action-row">
|
||||||
<div class="trade-field"><label class="text-label">止损</label><input type="number" id="trade-sl" step="any"></div>
|
<button type="button" class="btn-primary btn-open" id="btn-open">开仓</button>
|
||||||
<div class="trade-field"><label class="text-label">止盈</label><input type="number" id="trade-tp" step="any"></div>
|
<button type="button" class="btn-secondary" id="btn-close-pos">平仓</button>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="trade-btn-row">
|
|
||||||
<button type="button" class="trade-btn long" id="btn-open-long">
|
|
||||||
<span class="btn-price" id="px-long">—</span>
|
|
||||||
<span class="btn-label">加多</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="trade-btn lock" id="btn-open-short">
|
|
||||||
<span class="btn-price" id="px-short">—</span>
|
|
||||||
<span class="btn-label">加空</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="trade-btn close" id="btn-close-long">
|
|
||||||
<span class="btn-label">平多</span>
|
|
||||||
<span class="btn-sub" id="pos-long">≤0</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="trade-btn close" id="btn-close-short">
|
|
||||||
<span class="btn-label">平空</span>
|
|
||||||
<span class="btn-sub" id="pos-short">≤0</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="trade-footer" id="trade-footer">
|
<div class="trade-footer" id="trade-footer">
|
||||||
<p class="hint">程序报单经 CTP 进入柜台;策略自动化请用 <a href="{{ url_for('strategy_page') }}">策略交易</a>。</p>
|
<p class="hint" id="trade-metrics-hint">填写品种后显示精度与每跳价值;策略自动化请用 <a href="{{ url_for('strategy_page') }}">策略交易</a>。</p>
|
||||||
{% if ctp_status.last_error %}<p class="text-loss" style="font-size:.78rem;margin-top:.35rem">{{ ctp_status.last_error }}</p>{% endif %}
|
{% if ctp_status.last_error %}
|
||||||
|
<p class="text-loss ctp-install-hint" style="font-size:.78rem;margin-top:.35rem">{{ ctp_status.last_error }}</p>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted ctp-install-hint" style="font-size:.72rem;margin-top:.35rem">报单需安装 vnpy 并连接 CTP(SimNow 模拟盘)。</p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card trade-card" id="positions">
|
<div class="card trade-card" id="positions">
|
||||||
<h2>持仓监控</h2>
|
<h2>持仓监控</h2>
|
||||||
|
<p class="hint pos-hint">数据来自 CTP 柜台(交易所回报),浮盈等为柜台实际值。</p>
|
||||||
<div class="card-body card-scroll" id="position-live-list">
|
<div class="card-body card-scroll" id="position-live-list">
|
||||||
<div class="empty-hint">加载中…</div>
|
<div class="empty-hint">{% if ctp_status.connected %}加载中…{% else %}请先连接 CTP 查看柜台持仓{% endif %}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -107,7 +123,7 @@
|
|||||||
<tr class="rec-{{ r.status }}">
|
<tr class="rec-{{ r.status }}">
|
||||||
<td><strong>{{ r.name }}</strong> <span class="text-muted">{{ r.ths }}</span></td>
|
<td><strong>{{ r.name }}</strong> <span class="text-muted">{{ r.ths }}</span></td>
|
||||||
<td>{{ r.exchange }}</td>
|
<td>{{ r.exchange }}</td>
|
||||||
<td class="rec-price" data-ths="{{ r.ths }}">{% if r.price %}{{ r.price }}{% else %}—{% endif %}</td>
|
<td>{% if r.price %}{{ r.price }}{% else %}—{% endif %}</td>
|
||||||
<td>{% if r.margin_one_lot %}{{ r.margin_one_lot }}{% else %}—{% endif %}</td>
|
<td>{% if r.margin_one_lot %}{{ r.margin_one_lot }}{% else %}—{% endif %}</td>
|
||||||
<td>{% if r.min_capital_one_lot %}{{ r.min_capital_one_lot }}{% else %}—{% endif %}</td>
|
<td>{% if r.min_capital_one_lot %}{{ r.min_capital_one_lot }}{% else %}—{% endif %}</td>
|
||||||
<td><span class="badge {% if r.status=='ok' %}profit{% else %}planned{% endif %}">{{ r.status_label }}</span></td>
|
<td><span class="badge {% if r.status=='ok' %}profit{% else %}planned{% endif %}">{{ r.status_label }}</span></td>
|
||||||
|
|||||||
+10
-1
@@ -173,12 +173,16 @@ class CtpBridge:
|
|||||||
elif direction is not None and "空" in str(direction):
|
elif direction is not None and "空" in str(direction):
|
||||||
d = "short"
|
d = "short"
|
||||||
sym = getattr(pos, "symbol", "") or ""
|
sym = getattr(pos, "symbol", "") or ""
|
||||||
|
exchange = getattr(pos, "exchange", None)
|
||||||
|
ex_name = str(exchange.value if hasattr(exchange, "value") else exchange or "")
|
||||||
out.append({
|
out.append({
|
||||||
"symbol": sym,
|
"symbol": sym,
|
||||||
|
"exchange": ex_name,
|
||||||
"direction": d,
|
"direction": d,
|
||||||
"lots": vol,
|
"lots": vol,
|
||||||
"avg_price": float(getattr(pos, "price", 0) or 0),
|
"avg_price": float(getattr(pos, "price", 0) or 0),
|
||||||
"pnl": float(getattr(pos, "pnl", 0) or 0),
|
"pnl": float(getattr(pos, "pnl", 0) or 0),
|
||||||
|
"frozen": int(getattr(pos, "frozen", 0) or 0),
|
||||||
})
|
})
|
||||||
return out
|
return out
|
||||||
|
|
||||||
@@ -190,6 +194,7 @@ class CtpBridge:
|
|||||||
direction: str,
|
direction: str,
|
||||||
lots: int,
|
lots: int,
|
||||||
price: float,
|
price: float,
|
||||||
|
order_type: str = "limit",
|
||||||
) -> str:
|
) -> str:
|
||||||
from vnpy.trader.constant import Direction, Offset, OrderType
|
from vnpy.trader.constant import Direction, Offset, OrderType
|
||||||
from vnpy.trader.object import OrderRequest
|
from vnpy.trader.object import OrderRequest
|
||||||
@@ -217,11 +222,13 @@ class CtpBridge:
|
|||||||
else:
|
else:
|
||||||
raise ValueError(f"未知开平: {offset}")
|
raise ValueError(f"未知开平: {offset}")
|
||||||
|
|
||||||
|
ot = OrderType.MARKET if (order_type or "limit").lower() == "market" else OrderType.LIMIT
|
||||||
|
|
||||||
req = OrderRequest(
|
req = OrderRequest(
|
||||||
symbol=sym,
|
symbol=sym,
|
||||||
exchange=exchange,
|
exchange=exchange,
|
||||||
direction=d,
|
direction=d,
|
||||||
type=OrderType.LIMIT,
|
type=ot,
|
||||||
volume=lots,
|
volume=lots,
|
||||||
price=price,
|
price=price,
|
||||||
offset=off,
|
offset=off,
|
||||||
@@ -290,6 +297,7 @@ def execute_order(
|
|||||||
lots: int,
|
lots: int,
|
||||||
price: float,
|
price: float,
|
||||||
settings: dict | None = None,
|
settings: dict | None = None,
|
||||||
|
order_type: str = "limit",
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""统一下单:simulation=SimNow,live=期货公司 CTP。"""
|
"""统一下单:simulation=SimNow,live=期货公司 CTP。"""
|
||||||
del conn, settings
|
del conn, settings
|
||||||
@@ -308,6 +316,7 @@ def execute_order(
|
|||||||
direction=direction,
|
direction=direction,
|
||||||
lots=lots,
|
lots=lots,
|
||||||
price=price,
|
price=price,
|
||||||
|
order_type=order_type,
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
"order_id": order_id,
|
"order_id": order_id,
|
||||||
|
|||||||
Reference in New Issue
Block a user