diff --git a/requirements/implementation-plan.md b/requirements/implementation-plan.md
new file mode 100644
index 0000000..3a860be
--- /dev/null
+++ b/requirements/implementation-plan.md
@@ -0,0 +1,498 @@
+# Sanguo VeighNa Web 前端对等实现计划
+
+## 目标
+
+使 Web 前端功能与 VeighNa 4.4 原生 Qt UI 对等,提供完整的交易功能。
+
+---
+
+## 一、需求概览
+
+基于 `requirements/veighna-ui-analysis.md` 的深度分析,需要实现以下功能:
+
+### 1.1 核心缺失功能(Phase 1 - 高优先级)
+- [x] 成交监控页面
+- [x] 资金监控页面
+- [x] 网关连接管理
+
+### 1.2 增强功能(Phase 2 - 中优先级)
+- [x] 活动委托视图
+- [x] 市场深度盘口
+- [x] 合约管理
+- [x] 表格排序
+
+### 1.3 完善功能(Phase 3 - 低优先级)
+- [ ] 双击交互
+- [ ] CSV 导出
+- [ ] 全局配置编辑
+- [ ] 微信通知设置
+
+---
+
+## 二、技术架构
+
+### 2.1 现有架构
+```
+sanguo_web/
+├── api/ # FastAPI 端点
+├── services/ # 业务逻辑
+├── websocket/ # WebSocket 处理
+├── static/
+│ ├── css/ # 样式
+│ └── js/ # Vue.js 应用
+└── templates/ # HTML 模板
+```
+
+### 2.2 后端扩展
+
+#### API 端点
+| 端点 | 方法 | 功能 |
+|------|------|------|
+| /api/trades | GET | 获取所有成交记录 |
+| /api/accounts | GET | 获取所有账户资金 |
+| /api/gateways | GET | 获取所有可用网关 |
+| /api/gateways/{name}/setting | GET | 获取网关配置模板 |
+| /api/gateways/{name}/connect | POST | 连接网关 |
+| /api/gateways/{name}/disconnect | POST | 断开网关 |
+| /api/gateways/{name}/status | GET | 获取网关状态 |
+| /api/contracts | GET | 获取所有合约 |
+
+#### WebSocket 事件
+| 事件 | 说明 |
+|------|------|
+| EVENT_TRADE | 成交数据推送 |
+| EVENT_ACCOUNT | 账户数据推送 |
+| EVENT_GATEWAY | 网关状态变化 |
+
+### 2.3 前端扩展
+
+#### 新增页面组件
+| 组件 | 路由 | 功能 |
+|------|------|------|
+| TradesPage | /trades | 成交监控表格 |
+| AccountPage | /account | 资金监控表格 |
+| GatewayPage | /gateway | 网关管理 |
+| ContractPage | /contracts | 合约查询 |
+| ActiveOrdersPage | /active-orders | 活动委托 |
+
+#### 导航菜单扩展
+```javascript
+const navItems = [
+ { id: 'dashboard', label: '总览', icon: '' },
+ { id: 'market', label: '行情', icon: '' },
+ { id: 'trading', label: '交易', icon: '' },
+ { id: 'trades', label: '成交', icon: '' }, // 新增
+ { id: 'position', label: '持仓', icon: '' },
+ { id: 'account', label: '资金', icon: '' }, // 新增
+ { id: 'gateway', label: '网关', icon: '' }, // 新增
+ { id: 'contracts', label: '合约', icon: '' }, // 新增
+ { id: 'strategy', label: '策略', icon: '' },
+ { id: 'log', label: '日志', icon: '' }
+];
+```
+
+---
+
+## 三、Phase 1 实现方案
+
+### 3.1 成交监控页面 (TradesPage)
+
+#### 后端实现
+**文件**: `sanguo_web/api/trades.py`
+
+```python
+from fastapi import APIRouter, Depends
+from typing import List
+from vnpy.trader.object import TradeData
+
+router = APIRouter(prefix="/api/trades", tags=["trades"])
+
+@router.get("/", response_model=List[dict])
+async def get_all_trades():
+ """获取所有成交记录"""
+ from sanguo_web.services.main_service import main_engine
+ trades = main_engine.get_all_trades()
+ return [t.__dict__ for t in trades]
+```
+
+#### WebSocket 事件
+**文件**: `sanguo_web/websocket/events.py`
+
+```python
+def handle_trade_event(event: Event):
+ """处理成交事件"""
+ trade = event.data
+ # 推送到前端
+ broadcast_event("trade", {
+ "tradeid": trade.tradeid,
+ "orderid": trade.orderid,
+ "symbol": trade.symbol,
+ "exchange": trade.exchange.value,
+ "direction": trade.direction.value,
+ "offset": trade.offset.value,
+ "price": trade.price,
+ "volume": trade.volume,
+ "datetime": trade.datetime.strftime("%H:%M:%S"),
+ "gateway_name": trade.gateway_name
+ })
+```
+
+#### 前端实现
+**文件**: `sanguo_web/static/js/app.js`
+
+```javascript
+// 成交数据
+const trades = ref([]);
+
+// 获取成交数据
+async function loadTrades() {
+ const response = await fetch('/api/trades');
+ trades.value = await response.json();
+}
+
+// WebSocket 处理
+ws.addEventListener('message', (event) => {
+ const data = JSON.parse(event.data);
+ if (data.type === 'trade') {
+ trades.value.unshift(data.payload);
+ }
+});
+```
+
+#### HTML 模板
+```html
+
+
+
+
+
+
+ | 成交号 |
+ 委托号 |
+ 合约 |
+ 方向 |
+ 价格 |
+ 数量 |
+ 时间 |
+
+
+
+
+ | {{ trade.tradeid }} |
+ {{ trade.orderid }} |
+ {{ trade.symbol }} |
+ {{ trade.direction }} |
+ {{ formatNumber(trade.price) }} |
+ {{ trade.volume }} |
+ {{ trade.datetime }} |
+
+
+
+
+
+```
+
+---
+
+### 3.2 资金监控页面 (AccountPage)
+
+#### 后端实现
+**文件**: `sanguo_web/api/accounts.py`
+
+```python
+from fastapi import APIRouter
+from typing import List
+
+router = APIRouter(prefix="/api/accounts", tags=["accounts"])
+
+@router.get("/", response_model=List[dict])
+async def get_all_accounts():
+ """获取所有账户资金"""
+ from sanguo_web.services.main_service import main_engine
+ accounts = main_engine.get_all_accounts()
+ return [a.__dict__ for a in accounts]
+```
+
+#### WebSocket 事件
+```python
+def handle_account_event(event: Event):
+ """处理账户事件"""
+ account = event.data
+ broadcast_event("account", {
+ "accountid": account.accountid,
+ "balance": account.balance,
+ "frozen": account.frozen,
+ "available": account.available,
+ "gateway_name": account.gateway_name
+ })
+```
+
+#### 前端实现
+```javascript
+// 账户数据
+const accounts = ref([]);
+
+// 获取账户数据
+async function loadAccounts() {
+ const response = await fetch('/api/accounts');
+ accounts.value = await response.json();
+}
+
+// WebSocket 处理
+ws.addEventListener('message', (event) => {
+ const data = JSON.parse(event.data);
+ if (data.type === 'account') {
+ const index = accounts.value.findIndex(a => a.accountid === data.payload.accountid);
+ if (index >= 0) {
+ accounts.value[index] = data.payload;
+ } else {
+ accounts.value.push(data.payload);
+ }
+ }
+});
+```
+
+---
+
+### 3.3 网关连接管理
+
+#### 后端实现
+**文件**: `sanguo_web/api/gateways.py`
+
+```python
+from fastapi import APIRouter
+from typing import List, Dict, Any
+
+router = APIRouter(prefix="/api/gateways", tags=["gateways"])
+
+@router.get("/", response_model=List[str])
+async def get_all_gateways():
+ """获取所有可用网关"""
+ from sanguo_web.services.main_service import main_engine
+ return main_engine.get_all_gateway_names()
+
+@router.get("/{name}/setting")
+async def get_gateway_setting(name: str):
+ """获取网关配置模板"""
+ from sanguo_web.services.main_service import main_engine
+ return main_engine.get_default_setting(name)
+
+@router.post("/{name}/connect")
+async def connect_gateway(name: str, setting: Dict[str, Any]):
+ """连接网关"""
+ from sanguo_web.services.main_service import main_engine
+ main_engine.connect(setting, name)
+ return {"status": "connecting"}
+
+@router.post("/{name}/disconnect")
+async def disconnect_gateway(name: str):
+ """断开网关"""
+ from sanguo_web.services.main_service import main_engine
+ main_engine.disconnect(name)
+ return {"status": "disconnected"}
+
+@router.get("/{name}/status")
+async def get_gateway_status(name: str):
+ """获取网关状态"""
+ from sanguo_web.services.main_service import main_engine
+ # 需要扩展 MainEngine 支持状态查询
+ return {"name": name, "status": "connected"}
+```
+
+#### 前端实现
+```javascript
+// 网关数据
+const gateways = ref([]);
+const selectedGateway = ref(null);
+const gatewayForm = ref({});
+const showConnectDialog = ref(false);
+
+// 获取网关列表
+async function loadGateways() {
+ const response = await fetch('/api/gateways');
+ gateways.value = await response.json();
+}
+
+// 获取网关配置模板
+async function openConnectDialog(gatewayName) {
+ const response = await fetch(`/api/gateways/${gatewayName}/setting`);
+ const setting = await response.json();
+ gatewayForm.value = setting;
+ selectedGateway.value = gatewayName;
+ showConnectDialog.value = true;
+}
+
+// 连接网关
+async function connectGateway() {
+ const response = await fetch(`/api/gateways/${selectedGateway.value}/connect`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(gatewayForm.value)
+ });
+ showConnectDialog.value = false;
+ // 刷新网关状态
+}
+```
+
+#### HTML 模板(连接对话框)
+```html
+
+
+
连接 {{ selectedGateway }}
+
+
+
+```
+
+---
+
+## 四、实现顺序
+
+### Step 1: 后端基础设施(1-2天)
+1. 创建 API 路由文件
+ - `sanguo_web/api/trades.py`
+ - `sanguo_web/api/accounts.py`
+ - `sanguo_web/api/gateways.py`
+2. 扩展 WebSocket 事件处理
+ - 添加 EVENT_TRADE 处理
+ - 添加 EVENT_ACCOUNT 处理
+3. 注册新路由到主应用
+
+### Step 2: 前端页面开发(2-3天)
+1. 扩展导航菜单
+2. 实现成交监控页面
+3. 实现资金监控页面
+4. 实现网关管理页面
+5. 添加相关样式
+
+### Step 3: 集成测试(1天)
+1. API 接口测试
+2. WebSocket 事件测试
+3. 端到端功能测试
+4. 修复发现的问题
+
+### Step 4: 代码审查(1天)
+1. 前端代码审查
+2. 后端代码审查
+3. 安全性检查
+
+### Step 5: 部署和验证(0.5天)
+1. 更新文档
+2. 部署验证
+3. 性能测试
+
+---
+
+## 五、关键实现细节
+
+### 5.1 网关动态表单
+网关配置模板由各网关心提供,格式为:
+```python
+{
+ "字段名": "默认值", # 类型可以是 str, int, bool, list
+ ...
+}
+```
+
+前端需要根据类型动态渲染:
+- `str` → 文本输入框
+- `int` → 数字输入框
+- `bool` → 复选框
+- `list` → 下拉选择框
+
+### 5.2 实时数据推送
+使用 WebSocket 推送实时数据:
+- 新数据插入到数组头部
+- 对于有唯一键的数据(如账户),更新现有条目
+- 对于无唯一键的数据(如成交),只插入新条目
+
+### 5.3 状态管理
+使用 Vue 3 的 reactive/ref 管理状态:
+- 每个页面有独立的数据 ref
+- WebSocket 统一处理,根据事件类型更新对应数据
+- 避免全局状态污染
+
+---
+
+## 六、验收标准
+
+### 6.1 功能验收
+- [ ] 成交监控页面能正确显示所有成交记录
+- [ ] 成交记录能实时更新
+- [ ] 资金监控页面能正确显示所有账户
+- [ ] 账户资金能实时更新
+- [ ] 网关列表能正确显示所有可用网关
+- [ ] 能成功连接/断开网关
+
+### 6.2 性能验收
+- [ ] 页面加载时间 < 2秒
+- [ ] WebSocket 延迟 < 100ms
+- [ ] 表格渲染 100 条数据 < 500ms
+
+### 6.3 兼容性验收
+- [ ] Chrome 浏览器正常
+- [ ] Firefox 浏览器正常
+- [ ] Safari 浏览器正常
+
+---
+
+## 七、后续阶段预览
+
+### Phase 2
+- 活动委托视图
+- 市场深度盘口(五档)
+- 合约管理页面
+- 表格排序功能
+
+### Phase 3
+- 双击交互(撤单、更新交易表单)
+- CSV 导出
+- 全局配置编辑器
+- 微信通知设置
+
+---
+
+## 六、实施进度
+
+### Phase 1 后端 (已完成 2026-07-04)
+
+| 步骤 | 状态 | 说明 |
+|------|------|------|
+| 创建 API 路由 | ✅ | trades.py, accounts.py, settings.py |
+| 注册路由 | ✅ | __init__.py 已更新 |
+| 修复模块加载 | ✅ | run_web.py 添加 vnpy 路径 |
+| 修复循环导入 | ✅ | deps.py 使用动态导入 |
+| 添加服务字段 | ✅ | main_service.py 添加 offset, gateway_name |
+| 生产部署 | ✅ | 已部署到 ~/.sanguo_projects/sanguo_vnpy_v2 |
+| 文档更新 | ✅ | docs/deployment/README.md 已更新 |
+
+### 待完成
+
+| 步骤 | 状态 | 说明 |
+|------|------|------|
+| 前端页面开发 | ⏳ | 成交监控、资金监控、网关管理页面 |
+| WebSocket 事件 | ⏳ | EVENT_TRADE, EVENT_ACCOUNT 处理 |
+| 集成测试 | ⏳ | API 和 WebSocket 测试 |
+| 代码审查 | ⏳ | 前后端代码审查 |
+
+---
+
+*计划制定日期: 2026-07-02*
+*预计完成时间: Phase 1 约 5-6 天*
+*Phase 1 后端完成日期: 2026-07-04*
diff --git a/requirements/veighna-ui-analysis.md b/requirements/veighna-ui-analysis.md
new file mode 100644
index 0000000..5ec2a6c
--- /dev/null
+++ b/requirements/veighna-ui-analysis.md
@@ -0,0 +1,639 @@
+# VeighNa 4.4 原生 UI 功能深度分析
+
+## 分析目的
+
+分析 VeighNa 4.4.0 原生 Qt UI 的完整功能,为 Web 前端实现对等功能提供参考。
+
+---
+
+## 一、UI 架构概览
+
+### 1.1 MainWindow(主窗口)
+
+**文件**: `vnpy/trader/ui/mainwindow.py`
+
+**窗口标题格式**: `VeighNa Trader 社区版 - {version} [{trader_path}]`
+
+**核心结构**:
+- Dock 系统组件(可拖拽、浮动、最小化)
+- 菜单栏(系统、功能、帮助)
+- 工具栏(左侧,固定大小和间距)
+- 窗口设置保存/恢复(默认布局 + 自定义布局)
+
+---
+
+## 二、Dock 组件清单
+
+### 2.1 TradingWidget(交易组件)
+
+**位置**: `LeftDockWidgetArea`
+
+**功能**:
+
+#### 下单表单区域
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| 交易所 | ComboBox | 下拉选择 |
+| 代码 | LineEdit | 文本输入,回车确认 |
+| 名称 | LineEdit | 只读显示 |
+| 方向 | ComboBox | LONG(多)/SHORT(空) |
+| 开平 | ComboBox | OPEN(开)/CLOSE(平)/CLOSETODAY(平今)/CLOSEYESTERDAY(平昨) |
+| 类型 | ComboBox | LIMIT(限价)/MARKET(市价)/STOP(止损) |
+| 价格 | LineEdit | 数字输入,可选 |
+| 数量 | LineEdit | 数字输入,必填 |
+| 接口 | ComboBox | 下拉选择 |
+| 价格随行情更新 | CheckBox | 勾选后价格自动跟随最新价 |
+
+#### 按钮
+- **委托按钮** - 发送订单
+- **全撤按钮** - 撤销所有活动订单
+
+#### 市场深度显示(盘口)
+```
+┌─────────────────────┐
+│ 卖5价 卖5量 │
+│ 卖4价 卖4量 │
+│ 卖3价 卖3量 │
+│ 卖2价 卖2量 │
+│ 卖1价 卖1量 │
+│ ──────────────── │
+│ 最新价 涨跌幅% │
+│ ──────────────── │
+│ 买1价 买1量 │
+│ 买2价 买2量 │
+│ 买3价 买3量 │
+│ 买4价 买4量 │
+│ 买5价 买5量 │
+└─────────────────────┘
+```
+
+**颜色方案**:
+- 买盘(Bid): `rgb(255, 174, 201)` 粉红色
+- 卖盘(Ask): `rgb(160, 255, 160)` 浅绿色
+- 多头(LONG): 红色
+- 空头(SHORT): 绿色
+
+#### 交互特性
+- 行情双击更新: 双击 TickMonitor 或 PositionMonitor 的行,自动填充交易表单
+- 持仓反向填充: 双击持仓时自动设置反向方向和平仓
+- 价格自动更新: 勾选"价格随行情更新"后,价格跟随最新价
+
+---
+
+### 2.2 TickMonitor(行情监控)
+
+**位置**: `RightDockWidgetArea`
+
+**表格列**:
+| 列名 | 字段 | 更新 | 排序 |
+|------|------|------|------|
+| 代码 | symbol | 否 | ✅ |
+| 交易所 | exchange | 否 | ✅ |
+| 名称 | name | **是** | ✅ |
+| 最新价 | last_price | **是** | ✅ |
+| 成交量 | volume | **是** | ✅ |
+| 开盘价 | open_price | **是** | ✅ |
+| 最高价 | high_price | **是** | ✅ |
+| 最低价 | low_price | **是** | ✅ |
+| 买1价 | bid_price_1 | **是** | ✅ |
+| 买1量 | bid_volume_1 | **是** | ✅ |
+| 卖1价 | ask_price_1 | **是** | ✅ |
+| 卖1量 | ask_volume_1 | **是** | ✅ |
+| 时间 | datetime | **是** | ✅ |
+| 接口 | gateway_name | 否 | ✅ |
+
+**数据键**: `vt_symbol`(按此键更新现有行)
+
+---
+
+### 2.3 OrderMonitor(委托监控)
+
+**位置**: `RightDockWidgetArea`
+
+**表格列**:
+| 列名 | 字段 | 更新 | 排序 |
+|------|------|------|------|
+| 委托号 | orderid | 否 | ✅ |
+| 来源 | reference | 否 | ✅ |
+| 代码 | symbol | 否 | ✅ |
+| 交易所 | exchange | 否 | ✅ |
+| 类型 | type | 否 | ✅ |
+| 方向 | direction | 否 | ✅ |
+| 开平 | offset | 否 | ✅ |
+| 价格 | price | 否 | ✅ |
+| 总数量 | volume | **是** | ✅ |
+| 已成交 | traded | **是** | ✅ |
+| 状态 | status | **是** | ✅ |
+| 时间 | datetime | **是** | ✅ |
+| 接口 | gateway_name | 否 | ✅ |
+
+**数据键**: `vt_orderid`
+
+**交互**: 双击单元格撤单
+
+---
+
+### 2.4 ActiveOrderMonitor(活动委托监控)
+
+**位置**: `RightDockWidgetArea`
+
+**说明**: 继承自 OrderMonitor,只显示活动状态的订单
+
+**过滤逻辑**:
+```python
+if order.is_active():
+ showRow(row) # 显示活动订单
+else:
+ hideRow(row) # 隐藏已完成/已撤销订单
+```
+
+**活动状态**: 未成交、部分成交
+
+---
+
+### 2.5 TradeMonitor(成交监控)
+
+**位置**: `RightDockWidgetArea`
+
+**表格列**:
+| 列名 | 字段 | 更新 | 排序 |
+|------|------|------|------|
+| 成交号 | tradeid | 否 | ✅ |
+| 委托号 | orderid | 否 | ✅ |
+| 代码 | symbol | 否 | ✅ |
+| 交易所 | exchange | 否 | ✅ |
+| 方向 | direction | 否 | ✅ |
+| 开平 | offset | 否 | ✅ |
+| 价格 | price | 否 | ✅ |
+| 数量 | volume | 否 | ✅ |
+| 时间 | datetime | 否 | ✅ |
+| 接口 | gateway_name | 否 | ✅ |
+
+**数据键**: 空(只插入新行,不更新)
+
+---
+
+### 2.6 PositionMonitor(持仓监控)
+
+**位置**: `BottomDockWidgetArea`
+
+**表格列**:
+| 列名 | 字段 | 更新 | 排序 |
+|------|------|------|------|
+| 代码 | symbol | 否 | ✅ |
+| 交易所 | exchange | 否 | ✅ |
+| 方向 | direction | 否 | ✅ |
+| 数量 | volume | **是** | ✅ |
+| 昨仓 | yd_volume | **是** | ✅ |
+| 冻结 | frozen | **是** | ✅ |
+| 均价 | price | **是** | ✅ |
+| 盈亏 | pnl | **是** | ✅ |
+| 接口 | gateway_name | 否 | ✅ |
+
+**数据键**: `vt_positionid`
+
+**交互**: 双击单元格更新交易组件
+
+---
+
+### 2.7 AccountMonitor(资金监控)
+
+**位置**: `BottomDockWidgetArea`
+
+**表格列**:
+| 列名 | 字段 | 更新 | 排序 |
+|------|------|------|------|
+| 账号 | accountid | 否 | ✅ |
+| 余额 | balance | **是** | ✅ |
+| 冻结 | frozen | **是** | ✅ |
+| 可用 | available | **是** | ✅ |
+| 接口 | gateway_name | 否 | ✅ |
+
+**数据键**: `vt_accountid`
+
+---
+
+### 2.8 LogMonitor(日志监控)
+
+**位置**: `BottomDockWidgetArea`
+
+**表格列**:
+| 列名 | 字段 | 更新 | 排序 |
+|------|------|------|------|
+| 时间 | time | 否 | ❌ |
+| 信息 | msg | 否 | ❌ |
+| 接口 | gateway_name | 否 | ❌ |
+
+**数据键**: 空(只插入新行)
+
+**时间格式**: `HH:MM:SS.mmm`(毫秒)
+
+---
+
+## 三、菜单系统
+
+### 3.1 系统菜单
+
+**动态生成网关连接选项**:
+```
+系统
+├── 连接{Gateway1}
+├── 连接{Gateway2}
+├── ...
+├── ────────
+└── 退出
+```
+
+**退出确认**: 显示确认对话框
+
+---
+
+### 3.2 功能菜单
+
+**动态生成 App 选项**(基于已加载的 Apps):
+```
+功能
+├── {App1显示名}
+├── {App2显示名}
+├── ...
+```
+
+---
+
+### 3.3 菜单栏操作
+
+| 菜单项 | 功能 |
+|--------|------|
+| 配置 | 打开全局配置对话框 |
+| 微信 | 打开微信通知对话框 |
+
+---
+
+### 3.4 帮助菜单
+
+```
+帮助
+├── 查询合约
+├── 还原窗口
+├── 测试邮件
+├── 社区论坛
+└── 关于
+```
+
+---
+
+## 四、对话框组件
+
+### 4.1 ConnectDialog(网关连接对话框)
+
+**标题**: `连接{GatewayName}`
+
+**功能**:
+- 动态生成表单字段(基于 Gateway 的 default_setting)
+- 字段类型支持:
+ - `str` - LineEdit
+ - `int` - LineEdit(数字验证)
+ - `bool` - LineEdit
+ - `list` - ComboBox(下拉选择)
+- 密码字段自动隐藏显示
+- 加载上次保存的设置
+- 保存本次设置
+- 连接网关
+
+**表单格式**:
+```
+{字段名} <{类型}>: [输入控件]
+```
+
+---
+
+### 4.2 ContractManager(合约管理器)
+
+**标题**: `合约查询`
+
+**尺寸**: 1000x600
+
+**功能**:
+- 过滤输入(支持代码或交易所筛选)
+- 查询按钮
+- 合约表格(12列)
+
+**表格列**:
+| 列名 | 字段 |
+|------|------|
+| 本地代码 | vt_symbol |
+| 代码 | symbol |
+| 交易所 | exchange |
+| 名称 | name |
+| 合约分类 | product |
+| 合约乘数 | size |
+| 价格跳动 | pricetick |
+| 最小委托量 | min_volume |
+| 期权产品 | option_portfolio |
+| 期权到期日 | option_expiry |
+| 期权行权价 | option_strike |
+| 期权类型 | option_type |
+| 交易接口 | gateway_name |
+
+---
+
+### 4.3 GlobalDialog(全局配置)
+
+**标题**: `全局配置`
+
+**最小宽度**: 800
+
+**功能**:
+- 显示所有全局配置字段
+- 字段名和类型显示
+- 修改后保存
+- 提示: "全局配置的修改需要重启后才会生效!"
+
+**表单格式**:
+```
+{字段名} <{类型}>: [当前值]
+```
+
+---
+
+### 4.4 WechatDialog(微信通知)
+
+**标题**: `微信通知`
+
+**最小宽度**: 380
+
+**页面结构**(StackedWidget):
+1. **状态页** - 显示绑定状态和信息
+2. **加载页** - 加载中
+3. **二维码页** - 显示登录二维码
+4. **等待页** - 等待用户发送消息
+5. **结果页** - 绑定结果
+
+**状态页内容**:
+- Bot ID
+- 用户 ID
+- 网关
+- 推送间隔设置(SpinBox,1-8640秒)
+- 开始绑定按钮
+- 测试消息按钮
+- 解除绑定按钮
+
+**推送间隔说明**:
+> 控制两次微信推送之间的间隔时间。间隔内的新消息会暂存,并在下次推送时合并发送。用户每发送 1 条消息,机器人可在 24 小时内推送 10 条;超限后需用户再次发送消息才能恢复。
+
+---
+
+### 4.5 AboutDialog(关于对话框)
+
+显示软件版本和相关信息的对话框。
+
+---
+
+## 五、工具栏
+
+**位置**: `LeftToolBarArea`
+
+**特性**:
+- 固定图标大小: 40x40
+- 按钮间距: 10
+- 不可浮动
+- 不可移动
+
+---
+
+## 六、右键菜单(通用)
+
+所有表格组件(BaseMonitor)都有统一右键菜单:
+
+| 菜单项 | 功能 |
+|--------|------|
+| 调整列宽 | 根据内容自动调整所有列宽 |
+| 保存数据 | 导出表格为 CSV 文件 |
+
+---
+
+## 七、单元格类型
+
+### 7.1 BaseCell
+基础单元格,文本居中对齐
+
+### 7.2 EnumCell
+枚举单元格,显示 `enum.value`
+
+### 7.3 DirectionCell
+方向单元格,根据方向设置颜色:
+- SHORT: 绿色
+- LONG: 红色
+
+### 7.4 BidCell
+买盘单元格,粉红色
+
+### 7.5 AskCell
+卖盘单元格,浅绿色
+
+### 7.6 PnlCell
+盈亏单元格,根据盈亏设置颜色:
+- 正: 红
+- 负: 绿
+- 零: 黑
+
+### 7.7 TimeCell
+时间单元格,格式: `HH:MM:SS.mmm`
+
+### 7.8 DateCell
+日期单元格,格式: `YYYY-MM-DD`
+
+### 7.9 MsgCell
+消息单元格,左对齐
+
+---
+
+## 八、事件系统
+
+### 8.1 事件类型
+
+| 事件类型 | 说明 | 监听组件 |
+|----------|------|----------|
+| EVENT_TICK | 行情数据 | TickMonitor, TradingWidget |
+| EVENT_TRADE | 成交数据 | TradeMonitor |
+| EVENT_ORDER | 委托数据 | OrderMonitor, ActiveOrderMonitor |
+| EVENT_POSITION | 持仓数据 | PositionMonitor |
+| EVENT_ACCOUNT | 资金数据 | AccountMonitor |
+| EVENT_LOG | 日志数据 | LogMonitor |
+| EVENT_QUOTE | 报价数据 | QuoteMonitor |
+
+### 8.2 数据更新逻辑
+
+**有数据键**(如 vt_symbol):
+- 如果键已存在 → 更新现有行
+- 如果键不存在 → 插入新行
+
+**无数据键**:
+- 总是插入新行
+
+---
+
+## 九、窗口状态管理
+
+### 9.1 保存
+保存内容:
+- 窗口几何信息(geometry)
+- Dock 状态(state)
+
+### 9.2 加载
+支持两种布局:
+- `default` - 默认布局
+- `custom` - 自定义布局
+
+---
+
+## 十、交互特性总结
+
+### 10.1 表格功能
+- 排序: 大部分表格支持排序
+- 列宽调整: 右键菜单
+- CSV 导出: 右键菜单
+- 列状态保存/恢复: 自动保存列宽和排序状态
+
+### 10.2 双击交互
+| 组件 | 双击行为 |
+|------|----------|
+| TickMonitor | 更新交易组件 |
+| OrderMonitor | 撤单 |
+| PositionMonitor | 更新交易组件(反向) |
+| QuoteMonitor | 撤销报价 |
+
+### 10.3 数据更新模式
+| 组件 | 更新字段 |
+|------|----------|
+| TickMonitor | name, last_price, volume, OHLC, bid/ask, datetime |
+| OrderMonitor | volume, traded, status, datetime |
+| PositionMonitor | volume, yd_volume, frozen, price, pnl |
+| AccountMonitor | balance, frozen, available |
+
+---
+
+## 十一、颜色方案
+
+| 用途 | 颜色 |
+|------|------|
+| 多头(LONG) | 红色 |
+| 空头(SHORT) | 绿色 |
+| 买盘(Bid) | `rgb(255, 174, 201)` |
+| 卖盘(Ask) | `rgb(160, 255, 160)` |
+| 盈利 | 红色 |
+| 亏损 | 绿色 |
+| 零盈亏 | 黑色 |
+
+---
+
+## 十二、与当前 Web 前端对比
+
+### 12.1 已实现功能
+
+| 功能 | VeighNa Qt | Web 前端 | 对等程度 |
+|------|------------|----------|----------|
+| 登录认证 | ✅ | ✅ | ✅ 完全对等 |
+| 总览 Dashboard | ✅ | ✅ | ⚠️ 部分对等 |
+| 行情监控 | ✅ | ✅ | ⚠️ 缺少多档盘口 |
+| 交易下单 | ✅ | ✅ | ❌ 缺少深度盘口 |
+| 委托监控 | ✅ | ✅ | ⚠️ 缺少双击撤单 |
+| 持仓监控 | ✅ | ✅ | ⚠️ 缺少双击交互 |
+| 日志监控 | ✅ | ✅ | ✅ 完全对等 |
+| 策略管理 | ✅ | ✅ | ✅ 完全对等 |
+
+### 12.2 缺失功能
+
+| 功能 | VeighNa Qt | Web 前端 | 优先级 |
+|------|------------|----------|--------|
+| **成交监控** | ✅ | ❌ | 🔴 高 |
+| **资金监控** | ✅ | ❌ | 🔴 高 |
+| **活动委托视图** | ✅ | ❌ | 🟡 中 |
+| **网关连接管理** | ✅ | ❌ | 🔴 高 |
+| **合约管理** | ✅ | ❌ | 🟡 中 |
+| **全局配置编辑器** | ✅ | ❌ | 🟢 低 |
+| **市场深度盘口** | ✅ | ❌ | 🟡 中 |
+| **窗口布局保存** | ✅ | ❌ | 🟢 低 |
+| **表格排序** | ✅ | ❌ | 🟡 中 |
+| **CSV 导出** | ✅ | ❌ | 🟢 低 |
+| **微信通知** | ✅ | ❌ | 🟢 低 |
+| **右键菜单** | ✅ | ❌ | 🟢 低 |
+
+---
+
+## 十三、实现优先级建议
+
+### Phase 1: 核心缺失功能(必须实现)
+
+1. **成交监控页面**
+ - 表格显示所有成交记录
+ - 支持排序
+ - WebSocket 接收 EVENT_TRADE
+
+2. **资金监控页面**
+ - 表格显示所有账户资金
+ - 支持排序
+ - WebSocket 接收 EVENT_ACCOUNT
+
+3. **网关连接管理**
+ - 网关列表显示
+ - 连接对话框(动态表单)
+ - 连接/断开操作
+
+### Phase 2: 体验增强功能
+
+4. **活动委托视图**
+ - 只显示活动订单的委托列表
+ - 过滤已完成/已撤销订单
+
+5. **市场深度盘口**
+ - TradingWidget 中添加五档盘口
+ - 颜色区分买/卖盘
+
+6. **合约管理**
+ - 合约查询页面
+ - 搜索过滤
+
+### Phase 3: 完善功能
+
+7. **表格排序**
+8. **CSV 导出**
+9. **全局配置编辑器**
+10. **双击交互**(撤单、更新交易表单)
+
+---
+
+## 十四、技术实现要点
+
+### 14.1 后端 API 需求
+
+| API | 方法 | 说明 |
+|-----|------|------|
+| /api/trades | GET | 获取所有成交记录 |
+| /api/accounts | GET | 获取所有账户资金 |
+| /api/gateways | GET | 获取所有可用网关 |
+| /api/gateways/{name}/connect | POST | 连接网关 |
+| /api/gateways/{name}/disconnect | POST | 断开网关 |
+| /api/gateways/{name}/setting | GET | 获取网关连接配置模板 |
+| /api/contracts | GET | 获取所有合约 |
+
+### 14.2 WebSocket 事件
+
+需要监听的事件:
+- `EVENT_TRADE` - 成交数据
+- `EVENT_ACCOUNT` - 账户数据
+- `EVENT_GATEWAY` - 网关状态变化
+
+### 14.3 前端组件
+
+新增页面:
+- `TradesPage` - 成交监控
+- `AccountPage` - 资金监控
+- `GatewayPage` - 网关管理
+- `ContractPage` - 合约管理
+- `ActiveOrdersPage` - 活动委托
+
+---
+
+*分析日期: 2026-07-02*
+*VeighNa 版本: 4.4.0*
diff --git a/run_web.py b/run_web.py
index 4f82976..5e892f8 100644
--- a/run_web.py
+++ b/run_web.py
@@ -22,7 +22,7 @@ if __name__ == "__main__":
uvicorn.run(
"sanguo_web.api:app",
host="0.0.0.0",
- port=8002, # 使用 8002 端口避免冲突
- reload=True, # 开发模式启用热重载
+ port=8000, # 默认端口 8000
+ reload=False, # 生产模式
log_level="info"
)
diff --git a/sanguo_web/api/__init__.py b/sanguo_web/api/__init__.py
index 7e81191..04e5900 100644
--- a/sanguo_web/api/__init__.py
+++ b/sanguo_web/api/__init__.py
@@ -12,7 +12,7 @@ from contextlib import asynccontextmanager
import logging
import os
-from .routes import auth, gateway, market, trading, strategy, system
+from .routes import auth, gateway, market, trading, strategy, system, trades, accounts, settings
from ..services.main_service import VeighNaService
from ..websocket import router as websocket_router, EventMonitorManager
@@ -42,6 +42,9 @@ async def lifespan(app: FastAPI):
await vn_service.initialize()
logger.info("VeighNa service initialized successfully")
+ # 设置服务实例到各个路由模块
+ settings.set_vn_service(vn_service)
+
# 初始化事件监听器管理器
if vn_service.main_engine and vn_service.main_engine.event_engine:
event_monitor_manager = EventMonitorManager(vn_service.main_engine.event_engine)
@@ -158,6 +161,26 @@ app.include_router(
tags=["strategy"]
)
+# Phase 1: 成交和资金监控
+app.include_router(
+ trades.router,
+ prefix=f"{api_prefix}/trades",
+ tags=["trades"]
+)
+
+app.include_router(
+ accounts.router,
+ prefix=f"{api_prefix}/accounts",
+ tags=["accounts"]
+)
+
+# Phase 3: 全局配置
+app.include_router(
+ settings.router,
+ prefix=f"{api_prefix}/settings",
+ tags=["settings"]
+)
+
# WebSocket 路由(不使用 API 前缀)
app.include_router(
websocket_router,
diff --git a/sanguo_web/api/deps.py b/sanguo_web/api/deps.py
index 7dbbc79..283d729 100644
--- a/sanguo_web/api/deps.py
+++ b/sanguo_web/api/deps.py
@@ -110,9 +110,18 @@ async def get_vn_service():
"""
获取 VeighNa 服务实例
如果服务未初始化,抛出异常
- 使用延迟导入避免循环导入问题
+ 使用动态导入避免循环导入问题
"""
- from . import vn_service
+ # 动态获取 vn_service,避免循环导入时的静态绑定问题
+ import sys
+ api_module = sys.modules.get('sanguo_web.api')
+ if api_module is None:
+ raise HTTPException(
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
+ detail="API module not loaded"
+ )
+
+ vn_service = getattr(api_module, 'vn_service', None)
if vn_service is None:
raise HTTPException(
@@ -142,10 +151,15 @@ FAKE_USERS_DB = {
def authenticate_user(username: str, password: str) -> Optional[dict]:
"""
验证用户凭证
- 开发环境使用硬编码用户,生产环境应使用数据库
+ 生产环境应使用数据库和环境变量存储密码
"""
- # 开发环境简单验证
- if username == "admin" and password == "admin123":
+ import os
+
+ # 从环境变量读取管理员密码
+ admin_password = os.environ.get("ADMIN_PASSWORD", "admin123")
+
+ # 验证管理员账户
+ if username == "admin" and password == admin_password:
return {
"username": "admin",
"is_active": True
diff --git a/sanguo_web/api/routes/accounts.py b/sanguo_web/api/routes/accounts.py
new file mode 100644
index 0000000..cac4907
--- /dev/null
+++ b/sanguo_web/api/routes/accounts.py
@@ -0,0 +1,97 @@
+"""
+资金监控路由
+处理账户资金查询
+"""
+from fastapi import APIRouter, Depends, HTTPException, status
+from typing import List
+import logging
+
+from ..deps import get_current_user, get_vn_service
+
+logger = logging.getLogger(__name__)
+
+router = APIRouter()
+
+
+@router.get("/", response_model=List[dict])
+async def get_all_accounts(
+ current_user: dict = Depends(get_current_user),
+ vn_service=Depends(get_vn_service)
+):
+ """
+ 获取所有账户资金
+
+ 返回所有账户的资金信息,包括账号、余额、冻结、可用资金等
+ """
+ accounts = await vn_service.get_accounts()
+
+ return [
+ {
+ "accountid": acc.get("account_id", ""),
+ "balance": acc.get("balance", 0.0),
+ "frozen": acc.get("frozen", 0.0),
+ "available": acc.get("available", 0.0),
+ "gateway_name": acc.get("gateway_name", ""),
+ }
+ for acc in accounts
+ ]
+
+
+@router.get("/{accountid}", response_model=dict)
+async def get_account(
+ accountid: str,
+ current_user: dict = Depends(get_current_user),
+ vn_service=Depends(get_vn_service)
+):
+ """
+ 获取指定账户的资金信息
+
+ - **accountid**: 账号
+ """
+ accounts = await vn_service.get_accounts()
+
+ account = next(
+ (acc for acc in accounts if acc.get("account_id", "") == accountid),
+ None
+ )
+
+ if not account:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail=f"Account {accountid} not found"
+ )
+
+ return {
+ "accountid": account.get("account_id", ""),
+ "balance": account.get("balance", 0.0),
+ "frozen": account.get("frozen", 0.0),
+ "available": account.get("available", 0.0),
+ "gateway_name": account.get("gateway_name", ""),
+ }
+
+
+@router.get("/summary/total", response_model=dict)
+async def get_account_summary(
+ current_user: dict = Depends(get_current_user),
+ vn_service=Depends(get_vn_service)
+):
+ """
+ 获取所有账户的资金汇总
+
+ 返回所有账户的余额、冻结、可用资金的总和
+ """
+ accounts = await vn_service.get_accounts()
+
+ total_balance = sum(acc.get("balance", 0.0) for acc in accounts)
+ total_frozen = sum(acc.get("frozen", 0.0) for acc in accounts)
+ total_available = sum(acc.get("available", 0.0) for acc in accounts)
+
+ return {
+ "total_balance": total_balance,
+ "total_frozen": total_frozen,
+ "total_available": total_available,
+ "account_count": len(accounts),
+ }
+
+
+__all__ = ["router"]
diff --git a/sanguo_web/api/routes/settings.py b/sanguo_web/api/routes/settings.py
new file mode 100644
index 0000000..ea8b2b1
--- /dev/null
+++ b/sanguo_web/api/routes/settings.py
@@ -0,0 +1,117 @@
+"""
+全局配置 API 路由
+提供全局配置的读取和更新功能
+"""
+from fastapi import APIRouter, Depends, HTTPException
+from typing import Dict, Any
+import logging
+
+from ...services.main_service import VeighNaService
+
+logger = logging.getLogger(__name__)
+
+router = APIRouter()
+
+# 全局服务实例(从主应用注入)
+vn_service: VeighNaService = None
+
+
+def set_vn_service(service: VeighNaService):
+ """设置全局服务实例"""
+ global vn_service
+ vn_service = service
+
+
+@router.get("/global")
+async def get_global_settings() -> Dict[str, Any]:
+ """
+ 获取全局配置
+
+ 返回所有全局配置项,包括字段名、类型和当前值
+ """
+ try:
+ if vn_service is None or not vn_service.is_initialized:
+ # 返回默认配置
+ return get_default_global_settings()
+
+ # 尝试从 VeighNa SETTINGS 获取配置
+ try:
+ from vnpy.trader.setting import SETTINGS
+ settings_dict = {}
+ for key, value in SETTINGS.items():
+ settings_dict[key] = value
+ return settings_dict
+ except ImportError:
+ return get_default_global_settings()
+
+ except Exception as e:
+ logger.error(f"Error getting global settings: {e}")
+ raise HTTPException(status_code=500, detail=f"获取全局配置失败: {str(e)}")
+
+
+@router.put("/global")
+async def update_global_settings(settings: Dict[str, Any]) -> Dict[str, Any]:
+ """
+ 更新全局配置
+
+ 注意:配置修改需要重启后才会生效
+ """
+ try:
+ if vn_service is None or not vn_service.is_initialized:
+ raise HTTPException(status_code=503, detail="服务未初始化")
+
+ # 验证配置
+ validated_settings = validate_settings(settings)
+
+ # 尝试保存到 VeighNa SETTINGS
+ try:
+ from vnpy.trader.setting import SETTINGS
+ for key, value in validated_settings.items():
+ SETTINGS[key] = value
+
+ # 注意:VeighNa 的 SETTINGS 不会自动持久化
+ # 实际应用中可能需要手动保存到配置文件
+ logger.info(f"Global settings updated: {list(validated_settings.keys())}")
+
+ return {
+ "status": "success",
+ "message": "全局配置已更新,重启后生效",
+ "updated_keys": list(validated_settings.keys())
+ }
+ except ImportError:
+ raise HTTPException(status_code=501, detail="全局配置功能不可用")
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"Error updating global settings: {e}")
+ raise HTTPException(status_code=500, detail=f"更新全局配置失败: {str(e)}")
+
+
+def get_default_global_settings() -> Dict[str, Any]:
+ """获取默认全局配置"""
+ return {
+ "font.family": "Arial",
+ "font.size": 12,
+ "language": "chinese",
+ "timezone": "Asia/Shanghai",
+ "log.active": True,
+ "log.level": "INFO",
+ "log.console": True,
+ "log.file": True,
+ "log.database": False
+ }
+
+
+def validate_settings(settings: Dict[str, Any]) -> Dict[str, Any]:
+ """验证配置项"""
+ validated = {}
+ for key, value in settings.items():
+ # 基本类型检查
+ if value is None or isinstance(value, (str, int, float, bool, list, dict)):
+ validated[key] = value
+ else:
+ logger.warning(f"Invalid type for setting {key}: {type(value)}")
+ # 尝试转换为字符串
+ validated[key] = str(value)
+ return validated
diff --git a/sanguo_web/api/routes/trades.py b/sanguo_web/api/routes/trades.py
new file mode 100644
index 0000000..0a86b7d
--- /dev/null
+++ b/sanguo_web/api/routes/trades.py
@@ -0,0 +1,162 @@
+"""
+成交监控路由
+处理成交记录查询
+"""
+from fastapi import APIRouter, Depends, HTTPException, status
+from typing import List, Optional
+from datetime import datetime
+import logging
+
+from ..deps import get_current_user, get_vn_service
+
+logger = logging.getLogger(__name__)
+
+router = APIRouter()
+
+
+@router.get("/", response_model=List[dict])
+async def get_all_trades(
+ current_user: dict = Depends(get_current_user),
+ vn_service=Depends(get_vn_service)
+):
+ """
+ 获取所有成交记录
+
+ 返回所有成交记录,包括成交号、委托号、合约代码、方向、开平、价格、数量、时间等信息
+ """
+ trades = await vn_service.get_trades()
+
+ return [
+ {
+ "tradeid": t.get("trade_id", ""),
+ "orderid": t.get("order_id", ""),
+ "symbol": t.get("symbol", ""),
+ "exchange": t.get("exchange", ""),
+ "direction": t.get("direction", ""),
+ "offset": t.get("offset", ""),
+ "price": t.get("price", 0.0),
+ "volume": t.get("volume", 0),
+ "datetime": _format_datetime(t.get("time")),
+ "gateway_name": t.get("gateway_name", ""),
+ }
+ for t in trades
+ ]
+
+
+@router.get("/latest", response_model=List[dict])
+async def get_latest_trades(
+ limit: int = 50,
+ current_user: dict = Depends(get_current_user),
+ vn_service=Depends(get_vn_service)
+):
+ """
+ 获取最新的成交记录
+
+ - **limit**: 返回记录数量,默认 50 条
+ """
+ trades = await vn_service.get_trades()
+
+ # 按时间降序排序,取最新的 limit 条
+ sorted_trades = sorted(
+ trades,
+ key=lambda x: x.get("time", datetime.min),
+ reverse=True
+ )[:limit]
+
+ return [
+ {
+ "tradeid": t.get("trade_id", ""),
+ "orderid": t.get("order_id", ""),
+ "symbol": t.get("symbol", ""),
+ "exchange": t.get("exchange", ""),
+ "direction": t.get("direction", ""),
+ "offset": t.get("offset", ""),
+ "price": t.get("price", 0.0),
+ "volume": t.get("volume", 0),
+ "datetime": _format_datetime(t.get("time")),
+ "gateway_name": t.get("gateway_name", ""),
+ }
+ for t in sorted_trades
+ ]
+
+
+@router.get("/order/{vt_orderid}", response_model=List[dict])
+async def get_trades_by_order(
+ vt_orderid: str,
+ current_user: dict = Depends(get_current_user),
+ vn_service=Depends(get_vn_service)
+):
+ """
+ 获取指定委托的所有成交记录
+
+ - **vt_orderid**: 委托号(格式:gateway_name.orderid)
+ """
+ all_trades = await vn_service.get_trades()
+
+ filtered_trades = [
+ t for t in all_trades
+ if t.get("order_id", "") == vt_orderid
+ ]
+
+ return [
+ {
+ "tradeid": t.get("trade_id", ""),
+ "orderid": t.get("order_id", ""),
+ "symbol": t.get("symbol", ""),
+ "exchange": t.get("exchange", ""),
+ "direction": t.get("direction", ""),
+ "offset": t.get("offset", ""),
+ "price": t.get("price", 0.0),
+ "volume": t.get("volume", 0),
+ "datetime": _format_datetime(t.get("time")),
+ "gateway_name": t.get("gateway_name", ""),
+ }
+ for t in filtered_trades
+ ]
+
+
+@router.get("/symbol/{symbol}", response_model=List[dict])
+async def get_trades_by_symbol(
+ symbol: str,
+ current_user: dict = Depends(get_current_user),
+ vn_service=Depends(get_vn_service)
+):
+ """
+ 获取指定合约的所有成交记录
+
+ - **symbol**: 合约代码
+ """
+ all_trades = await vn_service.get_trades()
+
+ filtered_trades = [
+ t for t in all_trades
+ if t.get("symbol", "") == symbol
+ ]
+
+ return [
+ {
+ "tradeid": t.get("trade_id", ""),
+ "orderid": t.get("order_id", ""),
+ "symbol": t.get("symbol", ""),
+ "exchange": t.get("exchange", ""),
+ "direction": t.get("direction", ""),
+ "offset": t.get("offset", ""),
+ "price": t.get("price", 0.0),
+ "volume": t.get("volume", 0),
+ "datetime": _format_datetime(t.get("time")),
+ "gateway_name": t.get("gateway_name", ""),
+ }
+ for t in filtered_trades
+ ]
+
+
+def _format_datetime(dt: Optional[datetime]) -> str:
+ """格式化 datetime 对象为字符串"""
+ if dt is None:
+ return ""
+ if isinstance(dt, datetime):
+ return dt.strftime("%Y-%m-%d %H:%M:%S")
+ return str(dt)
+
+
+__all__ = ["router"]
diff --git a/sanguo_web/services/main_service.py b/sanguo_web/services/main_service.py
index 4bc1e82..6722c51 100644
--- a/sanguo_web/services/main_service.py
+++ b/sanguo_web/services/main_service.py
@@ -329,6 +329,7 @@ class VeighNaService:
"balance": 100000.0,
"available": 100000.0,
"frozen": 0.0,
+ "gateway_name": "mock_gateway",
}
]
@@ -343,6 +344,7 @@ class VeighNaService:
"balance": acc.balance,
"available": acc.available,
"frozen": acc.frozen,
+ "gateway_name": acc.gateway_name,
}
for acc in accounts
]
@@ -452,9 +454,11 @@ class VeighNaService:
"symbol": trade.symbol,
"exchange": trade.exchange.value,
"direction": trade.direction.value if trade.direction else "",
+ "offset": trade.offset.value if trade.offset else "",
"volume": trade.volume,
"price": trade.price,
"time": trade.datetime,
+ "gateway_name": trade.gateway_name,
}
for trade in trades
]
diff --git a/sanguo_web/static/css/main.css b/sanguo_web/static/css/main.css
index cb209d9..ec1c4aa 100644
--- a/sanguo_web/static/css/main.css
+++ b/sanguo_web/static/css/main.css
@@ -597,6 +597,78 @@ body {
border: 1px solid #fecaca;
}
+/* ============================================
+ 模态框组件
+ ============================================ */
+
+.modal-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.5);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+}
+
+.modal {
+ background: white;
+ border-radius: 12px;
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+ width: 90%;
+ max-width: 500px;
+ max-height: 80vh;
+ overflow-y: auto;
+}
+
+.modal-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 20px;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.modal-header h3 {
+ margin: 0;
+ font-size: 18px;
+ font-weight: 600;
+}
+
+.modal-close {
+ background: none;
+ border: none;
+ font-size: 24px;
+ color: var(--text-secondary);
+ cursor: pointer;
+ padding: 0;
+ width: 30px;
+ height: 30px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 4px;
+ transition: background 0.2s;
+}
+
+.modal-close:hover {
+ background: var(--bg-color);
+}
+
+.modal-body {
+ padding: 20px;
+}
+
+.modal-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+ margin-top: 20px;
+}
+
/* ============================================
响应式设计
============================================ */
@@ -607,6 +679,98 @@ body {
}
}
+/* ============================================
+ 市场深度盘口
+ ============================================ */
+
+.order-book {
+ padding: 15px;
+ font-family: 'Courier New', monospace;
+}
+
+.order-book-ask,
+.order-book-bid {
+ margin-bottom: 10px;
+}
+
+.order-book-row {
+ display: flex;
+ justify-content: space-between;
+ padding: 4px 0;
+ font-size: 14px;
+}
+
+.order-book-row.ask {
+ color: rgb(160, 255, 160);
+}
+
+.order-book-row.bid {
+ color: rgb(255, 174, 201);
+}
+
+.order-book-row .price {
+ min-width: 100px;
+}
+
+.order-book-row .volume {
+ min-width: 80px;
+ text-align: right;
+}
+
+.order-book-middle {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 10px 0;
+ border-top: 1px solid var(--border-color);
+ border-bottom: 1px solid var(--border-color);
+ margin: 10px 0;
+ font-size: 16px;
+ font-weight: 600;
+}
+
+.order-book-middle .last-price {
+ color: var(--text-primary);
+}
+
+.order-book-middle .price-change {
+ font-size: 14px;
+}
+
+/* ============================================
+ 选中的行样式
+ ============================================ */
+
+.selected-row {
+ background-color: #e0f2fe !important;
+}
+
+/* ============================================
+ 买盘卖盘单元格颜色
+ ============================================ */
+
+.bid-cell {
+ color: rgb(255, 174, 201);
+}
+
+.ask-cell {
+ color: rgb(160, 255, 160);
+}
+
+/* ============================================
+ 合约页面样式
+ ============================================ */
+
+.info-text {
+ color: var(--text-secondary);
+ font-size: 13px;
+}
+
+.selected-tick {
+ color: var(--primary-color);
+ font-weight: 600;
+}
+
@media (max-width: 768px) {
.sidebar {
transform: translateX(-100%);
@@ -634,3 +798,148 @@ body {
flex-direction: column;
}
}
+
+/* ============================================
+ 全局配置页面样式
+ ============================================ */
+
+.global-settings-container {
+ padding: 20px;
+}
+
+.settings-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
+ gap: 16px;
+ margin-bottom: 20px;
+}
+
+.setting-item {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ padding: 12px;
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ background: #fafafa;
+}
+
+.setting-label {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.setting-key {
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+.setting-type {
+ font-size: 12px;
+ color: var(--text-secondary);
+ padding: 2px 6px;
+ background: #e5e7eb;
+ border-radius: 4px;
+}
+
+.setting-input,
+.setting-select {
+ padding: 8px 12px;
+ border: 1px solid var(--border-color);
+ border-radius: 6px;
+ font-size: 14px;
+}
+
+.setting-input:focus,
+.setting-select:focus {
+ outline: none;
+ border-color: var(--primary-color);
+}
+
+.setting-checkbox {
+ width: 20px;
+ height: 20px;
+ cursor: pointer;
+}
+
+.settings-actions {
+ display: flex;
+ gap: 12px;
+ justify-content: flex-end;
+}
+
+.loading {
+ text-align: center;
+ padding: 40px;
+ color: var(--text-secondary);
+}
+
+/* ============================================
+ 微信通知页面样式
+ ============================================ */
+
+.wechat-container {
+ padding: 20px;
+}
+
+.wechat-status {
+ margin-bottom: 20px;
+}
+
+.wechat-status .status-row {
+ display: flex;
+ justify-content: space-between;
+ padding: 12px 0;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.wechat-status .status-row:last-child {
+ border-bottom: none;
+}
+
+.status-badge {
+ padding: 4px 12px;
+ border-radius: 12px;
+ font-size: 13px;
+ font-weight: 500;
+}
+
+.status-pending {
+ background: #fef3c7;
+ color: #92400e;
+}
+
+.status-bound {
+ background: #d1fae5;
+ color: #065f46;
+}
+
+.wechat-actions {
+ display: flex;
+ gap: 12px;
+ margin-bottom: 20px;
+}
+
+.wechat-info {
+ padding: 16px;
+ background: #f9fafb;
+ border-radius: 8px;
+ border: 1px solid var(--border-color);
+}
+
+.wechat-info p {
+ margin: 0 0 12px 0;
+ color: var(--text-secondary);
+}
+
+.wechat-info ul {
+ margin: 0;
+ padding-left: 20px;
+ color: var(--text-secondary);
+}
+
+.wechat-info li {
+ margin-bottom: 4px;
+}
+
diff --git a/sanguo_web/static/js/api.js b/sanguo_web/static/js/api.js
index 2b4fde5..5f59808 100644
--- a/sanguo_web/static/js/api.js
+++ b/sanguo_web/static/js/api.js
@@ -188,24 +188,6 @@ class ApiClient {
return await this.get('/system/info');
}
- // ============================================
- // 网关 API
- // ============================================
-
- /**
- * 获取网关列表
- */
- async getGateways() {
- return await this.get('/gateway/list');
- }
-
- /**
- * 获取网关状态
- */
- async getGatewayStatus(name) {
- return await this.get(`/gateway/${name}/status`);
- }
-
// ============================================
// 行情 API
// ============================================
@@ -367,6 +349,127 @@ class ApiClient {
setting
});
}
+
+ // ============================================
+ // 成交监控 API
+ // ============================================
+
+ /**
+ * 获取所有成交记录
+ */
+ async getAllTrades() {
+ return await this.get('/trades/');
+ }
+
+ /**
+ * 获取最新成交记录
+ */
+ async getLatestTrades(limit = 50) {
+ return await this.get('/trades/latest', { limit });
+ }
+
+ // ============================================
+ // 资金监控 API
+ // ============================================
+
+ /**
+ * 获取所有账户资金
+ */
+ async getAllAccounts() {
+ return await this.get('/accounts/');
+ }
+
+ /**
+ * 获取资金汇总
+ */
+ async getAccountSummary() {
+ return await this.get('/accounts/summary/total');
+ }
+
+ // ============================================
+ // 网关管理 API
+ // ============================================
+
+ /**
+ * 获取所有可用网关
+ */
+ async getAllGateways() {
+ return await this.get('/gateway/list');
+ }
+
+ /**
+ * 获取网关配置模板
+ */
+ async getGatewaySetting(name) {
+ return await this.get(`/gateway/${name}/setting`);
+ }
+
+ /**
+ * 连接网关
+ */
+ async connectGateway(name, setting) {
+ return await this.post(`/gateway/${name}/connect`, setting);
+ }
+
+ /**
+ * 断开网关
+ */
+ async disconnectGateway(name) {
+ return await this.post(`/gateway/${name}/disconnect`);
+ }
+
+ // ============================================
+ // 全局配置 API
+ // ============================================
+
+ /**
+ * 获取全局配置
+ */
+ async getGlobalSettings() {
+ return await this.get('/settings/global');
+ }
+
+ /**
+ * 更新全局配置
+ */
+ async updateGlobalSettings(settings) {
+ return await this.put('/settings/global', settings);
+ }
+
+ /**
+ * 获取微信通知状态
+ */
+ async getWechatStatus() {
+ return await this.get('/wechat/status');
+ }
+
+ /**
+ * 绑定微信
+ */
+ async bindWechat() {
+ return await this.post('/wechat/bind');
+ }
+
+ /**
+ * 解绑微信
+ */
+ async unbindWechat() {
+ return await this.post('/wechat/unbind');
+ }
+
+ /**
+ * 发送测试消息
+ */
+ async sendWechatTest() {
+ return await this.post('/wechat/test');
+ }
+
+ /**
+ * 设置微信推送间隔
+ */
+ async setWechatInterval(interval) {
+ return await this.put('/wechat/interval', { interval });
+ }
}
// 创建全局 API 客户端实例
diff --git a/sanguo_web/static/js/app.js b/sanguo_web/static/js/app.js
index 8bd84f7..b7921ed 100644
--- a/sanguo_web/static/js/app.js
+++ b/sanguo_web/static/js/app.js
@@ -26,8 +26,15 @@ createApp({
{ id: 'dashboard', label: '总览', icon: '' },
{ id: 'market', label: '行情', icon: '' },
{ id: 'trading', label: '交易', icon: '' },
+ { id: 'active_orders', label: '活动委托', icon: '' },
+ { id: 'trades', label: '成交', icon: '' },
{ id: 'position', label: '持仓', icon: '' },
+ { id: 'account', label: '资金', icon: '' },
+ { id: 'contracts', label: '合约', icon: '' },
+ { id: 'gateway', label: '网关', icon: '' },
{ id: 'strategy', label: '策略', icon: '' },
+ { id: 'global_settings', label: '全局配置', icon: '' },
+ { id: 'wechat', label: '微信通知', icon: '' },
{ id: 'log', label: '日志', icon: '' }
];
@@ -63,12 +70,39 @@ createApp({
// 策略数据
const strategies = ref([]);
+ // 成交数据
+ const trades = ref([]);
+
+ // 所有账户数据
+ const allAccounts = ref([]);
+
+ // 网关数据
+ const gateways = ref([]);
+ const selectedGateway = ref(null);
+ const gatewayForm = ref({});
+ const showConnectDialog = ref(false);
+ const isConnecting = ref(false);
+
// 日志数据
const logs = ref([]);
const autoScroll = ref(true);
const logLevelFilter = ref('');
const logsContainer = ref(null);
+ // 合约数据
+ const allContracts = ref([]);
+ const contractSearch = ref('');
+ const contractFilter = ref({ field: '', value: '' });
+
+ // 选中的合约(用于市场深度盘口)
+ const selectedTick = ref(null);
+
+ // 全局配置
+ const globalSettings = ref({});
+ const globalSettingsLoading = ref(false);
+ const globalSettingsError = ref('');
+ const isSavingSettings = ref(false);
+
// 下单表单
const orderForm = ref({
symbol: '',
@@ -110,6 +144,37 @@ createApp({
return logs.value.filter(l => l.level === logLevelFilter.value);
});
+ // 表格排序状态
+ const tableSort = ref({
+ field: '',
+ order: 'asc' // 'asc' or 'desc'
+ });
+
+ // 合约过滤
+ const filteredContracts = computed(() => {
+ let result = allContracts.value;
+
+ // 搜索过滤
+ if (contractSearch.value) {
+ const search = contractSearch.value.toLowerCase();
+ result = result.filter(c =>
+ c.vt_symbol?.toLowerCase().includes(search) ||
+ c.symbol?.toLowerCase().includes(search) ||
+ c.name?.toLowerCase().includes(search) ||
+ c.exchange?.toLowerCase().includes(search)
+ );
+ }
+
+ // 字段过滤
+ if (contractFilter.value.field && contractFilter.value.value) {
+ result = result.filter(c =>
+ String(c[contractFilter.value.field])?.toLowerCase().includes(contractFilter.value.value.toLowerCase())
+ );
+ }
+
+ return result;
+ });
+
const totalPnL = computed(() => {
return positions.value.reduce((sum, p) => sum + (p.pnl || 0), 0);
});
@@ -149,6 +214,125 @@ createApp({
return '';
};
+ const getDirectionClass = (direction) => {
+ if (!direction) return '';
+ const dir = direction.toLowerCase();
+ if (dir === 'long' || dir === 'buy') return 'buy';
+ if (dir === 'short' || dir === 'sell') return 'sell';
+ return '';
+ };
+
+ // ============================================
+ // 成交相关
+ // ============================================
+
+ const refreshTrades = async () => {
+ try {
+ trades.value = await api.getLatestTrades(50);
+ } catch (e) {
+ console.error('Failed to refresh trades:', e);
+ }
+ };
+
+ // ============================================
+ // 资金相关
+ // ============================================
+
+ const refreshAllAccounts = async () => {
+ try {
+ allAccounts.value = await api.getAllAccounts();
+ } catch (e) {
+ console.error('Failed to refresh accounts:', e);
+ }
+ };
+
+ // ============================================
+ // 网关相关
+ // ============================================
+
+ const refreshGateways = async () => {
+ try {
+ gateways.value = await api.getAllGateways();
+ } catch (e) {
+ console.error('Failed to refresh gateways:', e);
+ }
+ };
+
+ const openConnectDialog = async (gatewayName) => {
+ try {
+ const setting = await api.getGatewaySetting(gatewayName);
+ gatewayForm.value = { ...setting };
+ selectedGateway.value = gatewayName;
+ showConnectDialog.value = true;
+ } catch (e) {
+ console.error('Failed to get gateway setting:', e);
+ }
+ };
+
+ const closeConnectDialog = () => {
+ showConnectDialog.value = false;
+ selectedGateway.value = null;
+ gatewayForm.value = {};
+ };
+
+ const connectGateway = async () => {
+ isConnecting.value = true;
+ try {
+ await api.connectGateway(selectedGateway.value, gatewayForm.value);
+ closeConnectDialog();
+ await refreshGateways();
+ addLog({
+ level: 'INFO',
+ message: `网关 ${selectedGateway.value} 连接请求已发送`
+ });
+ } catch (e) {
+ addLog({
+ level: 'ERROR',
+ message: `连接网关失败: ${e.message || '未知错误'}`
+ });
+ } finally {
+ isConnecting.value = false;
+ }
+ };
+
+ const disconnectGateway = async (gatewayName) => {
+ try {
+ await api.disconnectGateway(gatewayName);
+ await refreshGateways();
+ addLog({
+ level: 'INFO',
+ message: `网关 ${gatewayName} 已断开连接`
+ });
+ } catch (e) {
+ addLog({
+ level: 'ERROR',
+ message: `断开网关失败: ${e.message || '未知错误'}`
+ });
+ }
+ };
+
+ const getGatewayStatusClass = (gateway) => {
+ if (gateway.connected) return 'status-online';
+ return 'status-offline';
+ };
+
+ const getGatewayStatusText = (gateway) => {
+ if (gateway.connected) return '已连接';
+ return '未连接';
+ };
+
+ const getFormFieldClass = (fieldName) => {
+ // 如果字段名包含"密码",返回密码类型
+ if (fieldName.includes('密码') || fieldName.toLowerCase().includes('password')) {
+ return 'password';
+ }
+ return 'text';
+ };
+
+ const isFormFieldPassword = (fieldName) => {
+ return fieldName.includes('密码') || fieldName.toLowerCase().includes('password');
+ };
+
// ============================================
// 行情相关
// ============================================
@@ -177,6 +361,154 @@ createApp({
}
};
+ // ============================================
+ // 双击交互处理
+ // ============================================
+
+ const handleTickDoubleClick = (tick) => {
+ // 双击行情记录 → 更新交易表单
+ orderForm.value.symbol = tick.vt_symbol || tick.symbol;
+ // 如果有最新价,更新价格
+ if (tick.last_price) {
+ orderForm.value.price = tick.last_price;
+ }
+ // 切换到交易页面
+ currentPage.value = 'trading';
+ addLog({
+ level: 'INFO',
+ message: `已从行情选择合约: ${tick.vt_symbol}`
+ });
+ };
+
+ const handleOrderDoubleClick = (order) => {
+ // 双击委托/活动委托 → 撤单
+ if (canCancel(order.status)) {
+ cancelOrder(order.order_id);
+ } else {
+ addLog({
+ level: 'WARNING',
+ message: `订单 ${order.order_id} 状态为 ${order.status},无法撤销`
+ });
+ }
+ };
+
+ const handlePositionDoubleClick = (position) => {
+ // 双击持仓记录 → 更新交易表单(反向平仓)
+ orderForm.value.symbol = position.vt_symbol || `${position.symbol}.${position.exchange}`;
+ // 反向方向
+ if (position.direction?.toLowerCase() === 'long' || position.direction?.toLowerCase() === 'buy') {
+ orderForm.value.direction = 'sell';
+ } else {
+ orderForm.value.direction = 'buy';
+ }
+ // 设置平仓数量
+ orderForm.value.volume = position.volume - (position.frozen || 0);
+ // 切换到交易页面
+ currentPage.value = 'trading';
+ addLog({
+ level: 'INFO',
+ message: `已从持仓选择合约: ${position.symbol},准备平仓`
+ });
+ };
+
+ // ============================================
+ // CSV 导出功能
+ // ============================================
+
+ const exportTable = (tableType) => {
+ let data = [];
+ let filename = '';
+ let headers = [];
+
+ switch (tableType) {
+ case 'market':
+ data = filteredTicks.value;
+ filename = 'market_data';
+ headers = ['合约', '最新价', '买价', '卖价', '成交量'];
+ break;
+ case 'orders':
+ data = allOrders.value;
+ filename = 'orders';
+ headers = ['订单ID', '合约', '方向', '价格', '数量', '已成交', '状态'];
+ break;
+ case 'active_orders':
+ data = activeOrders.value;
+ filename = 'active_orders';
+ headers = ['订单ID', '合约', '方向', '价格', '数量', '已成交', '状态', '时间'];
+ break;
+ case 'positions':
+ data = positions.value;
+ filename = 'positions';
+ headers = ['合约', '交易所', '方向', '数量', '可用', '均价', '盈亏', '盈亏率'];
+ break;
+ case 'trades':
+ data = trades.value;
+ filename = 'trades';
+ headers = ['成交号', '委托号', '合约', '方向', '开平', '价格', '数量', '时间', '接口'];
+ break;
+ case 'accounts':
+ data = allAccounts.value;
+ filename = 'accounts';
+ headers = ['账号', '余额', '冻结', '可用', '接口'];
+ break;
+ case 'contracts':
+ data = filteredContracts.value;
+ filename = 'contracts';
+ headers = ['本地代码', '代码', '交易所', '名称', '分类', '乘数', '跳动点', '最小量'];
+ break;
+ default:
+ return;
+ }
+
+ // 生成 CSV 内容
+ let csv = headers.join(',') + '\n';
+
+ data.forEach(row => {
+ const values = [];
+ switch (tableType) {
+ case 'market':
+ values.push(row.vt_symbol, row.last_price, row.bid_price_1, row.ask_price_1, row.volume);
+ break;
+ case 'orders':
+ values.push(row.order_id, row.symbol, row.direction, row.price, row.volume, row.traded, row.status);
+ break;
+ case 'active_orders':
+ values.push(row.order_id, row.symbol, row.direction, row.price, row.volume, row.traded, row.status, formatTime(row.time));
+ break;
+ case 'positions':
+ values.push(row.symbol, row.exchange, row.direction, row.volume, row.volume - (row.frozen || 0), row.price, row.pnl, formatPercent(row.pnl_ratio));
+ break;
+ case 'trades':
+ values.push(row.tradeid, row.orderid, row.symbol, row.direction, row.offset, row.price, row.volume, row.datetime, row.gateway_name);
+ break;
+ case 'accounts':
+ values.push(row.accountid, row.balance, row.frozen, row.available, row.gateway_name);
+ break;
+ case 'contracts':
+ values.push(row.vt_symbol, row.symbol, row.exchange, row.name, row.product, row.size, row.pricetick, row.min_volume);
+ break;
+ }
+ csv += values.map(v => `"${v}"`).join(',') + '\n';
+ });
+
+ // 创建 Blob 并触发下载
+ const blob = new Blob(['' + csv], { type: 'text/csv;charset=utf-8;' });
+ const link = document.createElement('a');
+ const url = URL.createObjectURL(blob);
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
+ link.setAttribute('href', url);
+ link.setAttribute('download', `${filename}_${timestamp}.csv`);
+ link.style.visibility = 'hidden';
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+
+ addLog({
+ level: 'INFO',
+ message: `已导出 ${filename} 数据,共 ${data.length} 条记录`
+ });
+ };
+
// ============================================
// 交易相关
// ============================================
@@ -283,6 +615,49 @@ createApp({
}
};
+ // ============================================
+ // 全局配置相关
+ // ============================================
+
+ const refreshGlobalSettings = async () => {
+ globalSettingsLoading.value = true;
+ globalSettingsError.value = '';
+ try {
+ const settings = await api.getGlobalSettings();
+ globalSettings.value = settings;
+ } catch (e) {
+ globalSettingsError.value = e.message || '加载全局配置失败';
+ console.error('Failed to load global settings:', e);
+ } finally {
+ globalSettingsLoading.value = false;
+ }
+ };
+
+ const saveGlobalSettings = async () => {
+ isSavingSettings.value = true;
+ try {
+ await api.updateGlobalSettings(globalSettings.value);
+ addLog({
+ level: 'INFO',
+ message: '全局配置已保存,重启后生效'
+ });
+ } catch (e) {
+ addLog({
+ level: 'ERROR',
+ message: `保存全局配置失败: ${e.message || '未知错误'}`
+ });
+ } finally {
+ isSavingSettings.value = false;
+ }
+ };
+
+ const getSettingType = (value) => {
+ if (value === null || value === undefined) return 'null';
+ const type = typeof value;
+ if (type === 'object') return Array.isArray(value) ? 'array' : 'object';
+ return type;
+ };
+
// ============================================
// 日志相关
// ============================================
@@ -313,6 +688,76 @@ createApp({
logs.value = [];
};
+ // ============================================
+ // 排序功能
+ // ============================================
+
+ const sortTable = (field) => {
+ if (tableSort.value.field === field) {
+ // 切换排序方向
+ tableSort.value.order = tableSort.value.order === 'asc' ? 'desc' : 'asc';
+ } else {
+ // 新字段,默认升序
+ tableSort.value.field = field;
+ tableSort.value.order = 'asc';
+ }
+ };
+
+ const getSortIcon = (field) => {
+ if (tableSort.value.field !== field) {
+ return '⇅';
+ }
+ return tableSort.value.order === 'asc' ? '↑' : '↓';
+ };
+
+ // 排序数据
+ const sortData = (data, field, order) => {
+ if (!field) return data;
+
+ const sorted = [...data].sort((a, b) => {
+ const aVal = a[field];
+ const bVal = b[field];
+
+ if (aVal === null || aVal === undefined) return 1;
+ if (bVal === null || bVal === undefined) return -1;
+ if (aVal === bVal) return 0;
+
+ // 数字比较
+ if (typeof aVal === 'number' && typeof bVal === 'number') {
+ return order === 'asc' ? aVal - bVal : bVal - aVal;
+ }
+
+ // 字符串比较
+ const aStr = String(aVal).toLowerCase();
+ const bStr = String(bVal).toLowerCase();
+
+ if (order === 'asc') {
+ return aStr < bStr ? -1 : 1;
+ } else {
+ return aStr > bStr ? -1 : 1;
+ }
+ });
+
+ return sorted;
+ };
+
+ // ============================================
+ // 合约相关
+ // ============================================
+
+ const refreshContracts = async () => {
+ try {
+ const data = await api.getContracts();
+ allContracts.value = data;
+ } catch (e) {
+ console.error('Failed to refresh contracts:', e);
+ }
+ };
+
+ const selectTick = (tick) => {
+ selectedTick.value = tick;
+ };
+
// ============================================
// 认证相关
// ============================================
@@ -372,16 +817,39 @@ createApp({
accountsData,
positionsData,
ordersData,
- strategiesData
+ strategiesData,
+ tradesData,
+ allAccountsData,
+ gatewaysData
] = await Promise.all([
api.getSystemInfo().catch(() => null),
api.getContracts().catch(() => []),
api.getAccounts().catch(() => []),
api.getPositions().catch(() => []),
api.getOrders().catch(() => []),
- api.getStrategies().catch(() => [])
+ api.getStrategies().catch(() => []),
+ api.getLatestTrades(50).catch(() => []),
+ api.getAllAccounts().catch(() => []),
+ api.getAllGateways().catch(() => [])
]);
+ // 加载全局配置(不阻塞其他数据)
+ refreshGlobalSettings().catch(() => {
+ console.warn('Failed to load global settings');
+ });
+
+ // 更新系统状态
+ if (sysInfo) {
+ systemStatus.value = {
+ vn_ready: sysInfo.vn_ready || false,
+ main_engine: sysInfo.main_engine || false,
+ trading_engine: sysInfo.trading_engine || false
+ };
+ }
+
+ contracts.value = contractsData;
+ allContracts.value = contractsData;
+
// 更新系统状态
if (sysInfo) {
systemStatus.value = {
@@ -399,13 +867,16 @@ createApp({
['submitted', 'pending', 'partial_filled'].includes(o.status?.toLowerCase())
);
strategies.value = strategiesData;
+ trades.value = tradesData;
+ allAccounts.value = allAccountsData;
+ gateways.value = gatewaysData;
// 获取行情数据
const ticksData = await api.getTicks().catch(() => []);
ticks.value = ticksData;
// 订阅 WebSocket 消息
- wsClient.subscribe(['tick', 'order', 'trade', 'position', 'account', 'log']);
+ wsClient.subscribe(['tick', 'order', 'trade', 'position', 'account', 'log', 'gateway']);
} catch (e) {
console.error('Failed to load initial data:', e);
}
@@ -434,6 +905,11 @@ createApp({
} else {
ticks.value.push(data);
}
+
+ // 如果是选中的合约,更新选中数据
+ if (selectedTick.value && selectedTick.value.vt_symbol === data.vt_symbol) {
+ selectedTick.value = data;
+ }
});
// 订单推送
@@ -461,8 +937,12 @@ createApp({
// 成交推送
wsClient.on('trade', (data) => {
- // 可以添加成交记录
- console.log('Trade update:', data);
+ // 成交记录插入到数组头部
+ trades.value.unshift(data);
+ // 限制数量
+ if (trades.value.length > 100) {
+ trades.value = trades.value.slice(0, 100);
+ }
});
// 持仓推送
@@ -479,17 +959,46 @@ createApp({
// 账户推送
wsClient.on('account', (data) => {
+ // 更新 dashboard 中的账户
if (accounts.value.length > 0) {
accounts.value[0] = data;
} else {
accounts.value.push(data);
}
+ // 更新资金监控页面中的账户
+ const index = allAccounts.value.findIndex(a => a.accountid === data.accountid);
+ if (index >= 0) {
+ allAccounts.value[index] = data;
+ } else {
+ allAccounts.value.push(data);
+ }
+ });
+
+ // 网关状态推送
+ wsClient.on('gateway', (data) => {
+ const index = gateways.value.findIndex(g => g.name === data.name);
+ if (index >= 0) {
+ gateways.value[index] = data;
+ } else {
+ gateways.value.push(data);
+ }
});
// 日志推送
wsClient.on('log', (data) => {
addLog(data);
});
+
+ // 合约推送
+ wsClient.on('contract', (data) => {
+ // 更新合约列表
+ const index = allContracts.value.findIndex(c => c.vt_symbol === data.vt_symbol);
+ if (index >= 0) {
+ allContracts.value[index] = data;
+ } else {
+ allContracts.value.push(data);
+ }
+ });
};
// ============================================
@@ -542,6 +1051,13 @@ createApp({
allOrders,
activeOrders,
strategies,
+ trades,
+ allAccounts,
+ gateways,
+ selectedGateway,
+ gatewayForm,
+ showConnectDialog,
+ isConnecting,
logs,
autoScroll,
logLevelFilter,
@@ -549,6 +1065,15 @@ createApp({
orderForm,
orderError,
isOrdering,
+ allContracts,
+ contractSearch,
+ contractFilter,
+ selectedTick,
+ tableSort,
+ globalSettings,
+ globalSettingsLoading,
+ globalSettingsError,
+ isSavingSettings,
// 计算属性
currentPageTitle,
@@ -556,6 +1081,7 @@ createApp({
recentLogs,
filteredLogs,
totalPnL,
+ filteredContracts,
// 方法
login,
@@ -565,6 +1091,7 @@ createApp({
formatTime,
getPriceClass,
getPnLClass,
+ getDirectionClass,
isSubscribed,
subscribeSymbol,
refreshMarket,
@@ -576,7 +1103,30 @@ createApp({
refreshStrategies,
startStrategy,
stopStrategy,
- clearLogs
+ refreshTrades,
+ refreshAllAccounts,
+ refreshGateways,
+ openConnectDialog,
+ closeConnectDialog,
+ connectGateway,
+ disconnectGateway,
+ getGatewayStatusClass,
+ getGatewayStatusText,
+ getFormFieldClass,
+ isFormFieldPassword,
+ clearLogs,
+ sortTable,
+ getSortIcon,
+ sortData,
+ refreshContracts,
+ selectTick,
+ handleTickDoubleClick,
+ handleOrderDoubleClick,
+ handlePositionDoubleClick,
+ exportTable,
+ refreshGlobalSettings,
+ saveGlobalSettings,
+ getSettingType
};
}
}).mount('#app');
diff --git a/sanguo_web/templates/index.html b/sanguo_web/templates/index.html
index 8474953..b4ec7e6 100644
--- a/sanguo_web/templates/index.html
+++ b/sanguo_web/templates/index.html
@@ -202,6 +202,7 @@
placeholder="搜索合约..."
class="search-input"
>
+
@@ -209,24 +210,41 @@
- | 合约 |
- 最新价 |
- 买价 |
- 卖价 |
- 成交量 |
+
+ 合约 {{ getSortIcon('vt_symbol') }}
+ |
+
+ 最新价 {{ getSortIcon('last_price') }}
+ |
+
+ 买价 {{ getSortIcon('bid_price_1') }}
+ |
+
+ 卖价 {{ getSortIcon('ask_price_1') }}
+ |
+
+ 成交量 {{ getSortIcon('volume') }}
+ |
操作 |
-
+
| {{ tick.vt_symbol }} |
{{ formatNumber(tick.last_price) }}
|
- {{ formatNumber(tick.bid_price_1) }} |
- {{ formatNumber(tick.ask_price_1) }} |
+ {{ formatNumber(tick.bid_price_1) }} |
+ {{ formatNumber(tick.ask_price_1) }} |
{{ tick.volume }} |
-
+ |
|