first commit
This commit is contained in:
+24
@@ -0,0 +1,24 @@
|
||||
# Python
|
||||
backend/venv/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.egg-info/
|
||||
.pytest_cache/
|
||||
|
||||
# Node
|
||||
frontend/node_modules/
|
||||
frontend/dist/
|
||||
|
||||
# 数据库(生产环境数据保留在服务器,不提交)
|
||||
backend/data/*.db
|
||||
|
||||
# IDE / OS
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# PM2
|
||||
.pm2/
|
||||
logs/
|
||||
@@ -0,0 +1,193 @@
|
||||
# 部署文档
|
||||
|
||||
> 代码仓库:[https://git.bz121.com/dekun/crypto-pre-trade-system.git](https://git.bz121.com/dekun/crypto-pre-trade-system.git)
|
||||
|
||||
本文档说明如何在 **Ubuntu 服务器**上使用 **PM2** 守护进程部署本系统。
|
||||
|
||||
---
|
||||
|
||||
## 部署环境要求
|
||||
|
||||
| 项目 | 要求 |
|
||||
|------|------|
|
||||
| 操作系统 | Ubuntu 20.04 / 22.04 / 24.04 |
|
||||
| 运行用户 | root |
|
||||
| 安装路径 | `/opt/crypto-pre-trade-system` |
|
||||
| 进程守护 | PM2 |
|
||||
| Python | 3.10+(虚拟环境) |
|
||||
| Node.js | 18+(仅构建前端时使用) |
|
||||
| 访问端口 | **1125** |
|
||||
|
||||
---
|
||||
|
||||
## 一键部署(推荐)
|
||||
|
||||
以 **root** 用户登录服务器,执行:
|
||||
|
||||
```bash
|
||||
# 首次部署:克隆仓库并运行安装脚本
|
||||
git clone https://git.bz121.com/dekun/crypto-pre-trade-system.git /opt/crypto-pre-trade-system
|
||||
cd /opt/crypto-pre-trade-system
|
||||
bash deploy/install.sh
|
||||
```
|
||||
|
||||
脚本会自动完成:
|
||||
|
||||
1. 安装系统依赖(git、python3、nodejs、pm2)
|
||||
2. 创建 Python 虚拟环境并安装后端依赖
|
||||
3. 构建前端静态资源
|
||||
4. 通过 PM2 启动服务并设置开机自启
|
||||
5. 执行健康检查
|
||||
|
||||
部署完成后访问:**http://\<服务器IP\>:1125**
|
||||
|
||||
---
|
||||
|
||||
## 更新部署
|
||||
|
||||
代码有更新时,在服务器上重新执行安装脚本即可:
|
||||
|
||||
```bash
|
||||
cd /opt/crypto-pre-trade-system
|
||||
bash deploy/install.sh
|
||||
```
|
||||
|
||||
脚本会自动 `git pull`、重新构建前端、重启 PM2 进程。
|
||||
|
||||
---
|
||||
|
||||
## 手动部署步骤
|
||||
|
||||
若需手动逐步部署,按以下步骤操作:
|
||||
|
||||
### 1. 克隆仓库
|
||||
|
||||
```bash
|
||||
git clone https://git.bz121.com/dekun/crypto-pre-trade-system.git /opt/crypto-pre-trade-system
|
||||
cd /opt/crypto-pre-trade-system
|
||||
```
|
||||
|
||||
### 2. 安装系统依赖
|
||||
|
||||
```bash
|
||||
apt-get update
|
||||
apt-get install -y git python3 python3-venv python3-pip curl
|
||||
|
||||
# Node.js 20
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
|
||||
apt-get install -y nodejs
|
||||
|
||||
# PM2
|
||||
npm install -g pm2
|
||||
```
|
||||
|
||||
### 3. 后端虚拟环境
|
||||
|
||||
```bash
|
||||
cd /opt/crypto-pre-trade-system/backend
|
||||
python3 -m venv venv
|
||||
venv/bin/pip install -r requirements.txt
|
||||
mkdir -p data
|
||||
```
|
||||
|
||||
### 4. 构建前端
|
||||
|
||||
```bash
|
||||
cd /opt/crypto-pre-trade-system/frontend
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
### 5. PM2 启动
|
||||
|
||||
```bash
|
||||
cd /opt/crypto-pre-trade-system
|
||||
mkdir -p logs
|
||||
pm2 start ecosystem.config.cjs
|
||||
pm2 save
|
||||
pm2 startup systemd -u root --hp /root
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PM2 常用命令
|
||||
|
||||
```bash
|
||||
pm2 status # 查看进程状态
|
||||
pm2 logs crypto-pre-trade # 查看实时日志
|
||||
pm2 restart crypto-pre-trade # 重启服务
|
||||
pm2 stop crypto-pre-trade # 停止服务
|
||||
pm2 delete crypto-pre-trade # 删除进程
|
||||
```
|
||||
|
||||
日志文件位置:
|
||||
|
||||
- 标准输出:`/opt/crypto-pre-trade-system/logs/pm2-out.log`
|
||||
- 错误输出:`/opt/crypto-pre-trade-system/logs/pm2-error.log`
|
||||
|
||||
---
|
||||
|
||||
## 架构说明
|
||||
|
||||
生产环境采用 **单进程单端口** 模式:
|
||||
|
||||
```
|
||||
PM2 → uvicorn (0.0.0.0:1125)
|
||||
├── /api/* → FastAPI 后端接口
|
||||
└── /* → Vue3 前端静态资源(frontend/dist)
|
||||
```
|
||||
|
||||
- 前端构建产物由 FastAPI 直接托管,无需额外 Nginx
|
||||
- SQLite 数据库文件:`/opt/crypto-pre-trade-system/backend/data/pretrade.db`
|
||||
- 重启服务不丢失数据
|
||||
|
||||
---
|
||||
|
||||
## 防火墙
|
||||
|
||||
若服务器开启了防火墙,需放行 1125 端口:
|
||||
|
||||
```bash
|
||||
# ufw
|
||||
ufw allow 1125/tcp
|
||||
|
||||
# firewalld
|
||||
firewall-cmd --permanent --add-port=1125/tcp
|
||||
firewall-cmd --reload
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 重置数据库
|
||||
|
||||
```bash
|
||||
pm2 stop crypto-pre-trade
|
||||
rm /opt/crypto-pre-trade-system/backend/data/pretrade.db
|
||||
pm2 start crypto-pre-trade
|
||||
```
|
||||
|
||||
重启后系统自动重建表结构并写入默认数据。
|
||||
|
||||
---
|
||||
|
||||
## 故障排查
|
||||
|
||||
| 现象 | 排查方式 |
|
||||
|------|---------|
|
||||
| 无法访问 | `pm2 status` 确认进程 online;`curl http://127.0.0.1:1125/api/health` |
|
||||
| 502 / 连接拒绝 | 检查防火墙是否放行 1125 端口 |
|
||||
| 前端白屏 | 确认 `frontend/dist` 存在;重新 `npm run build` |
|
||||
| API 报错 | `pm2 logs crypto-pre-trade --lines 50` |
|
||||
| 数据库问题 | 检查 `backend/data/` 目录权限 |
|
||||
|
||||
---
|
||||
|
||||
## 本地开发 vs 生产部署
|
||||
|
||||
| | 本地开发 | 生产部署 |
|
||||
|---|---------|---------|
|
||||
| 后端 | `uvicorn --reload --port 8000` | PM2 + uvicorn `:1125` |
|
||||
| 前端 | `npm run dev :1125`(代理到 8000) | `npm run build`,由 FastAPI 托管 |
|
||||
| 访问 | http://localhost:1125 | http://\<服务器IP\>:1125 |
|
||||
|
||||
本地开发说明见 [README.md](./README.md)。
|
||||
@@ -0,0 +1,195 @@
|
||||
# 加密货币大盘-周期-阶段-强弱-方向-账户-策略前置匹配系统
|
||||
|
||||
纯本地部署的前置策略匹配工具。前后端分离,SQLite 持久化,黑色极简主题,无登录。
|
||||
|
||||
> **代码仓库**:[https://git.bz121.com/dekun/crypto-pre-trade-system.git](https://git.bz121.com/dekun/crypto-pre-trade-system.git)
|
||||
> **生产部署**:见 [DEPLOY.md](./DEPLOY.md)(Ubuntu + PM2 一键部署)
|
||||
|
||||
> **本系统仅做前置策略匹配**,不处理币种、K线识别、箱体判断、点位计算、突破校验、自动下单或交易所对接。
|
||||
> 所有箱体细节、触碰次数、突破确认条件、顺势/逆势箱体,全部由人工手动筛选输入。
|
||||
|
||||
---
|
||||
|
||||
## 技术栈
|
||||
|
||||
| 层级 | 技术 |
|
||||
|------|------|
|
||||
| 后端 | Python 3.10+ / FastAPI / SQLAlchemy / SQLite |
|
||||
| 前端 | Vue 3 / Vite / TailwindCSS / Axios |
|
||||
| 数据库 | SQLite(`backend/data/pretrade.db`) |
|
||||
|
||||
---
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
crypto-pre-trade-system/
|
||||
├── backend/
|
||||
│ ├── app/
|
||||
│ │ ├── main.py # FastAPI 入口
|
||||
│ │ ├── database.py # 数据库连接 + 默认数据初始化
|
||||
│ │ ├── models.py # ORM 模型
|
||||
│ │ ├── schemas.py # Pydantic 模型
|
||||
│ │ ├── crud.py # CRUD 操作
|
||||
│ │ ├── matcher.py # 前置匹配核心逻辑
|
||||
│ │ └── routes/ # API 路由
|
||||
│ ├── init_db.sql # 建表 SQL(参考)
|
||||
│ ├── requirements.txt
|
||||
│ └── data/ # SQLite 数据库(自动创建)
|
||||
├── frontend/
|
||||
│ ├── src/
|
||||
│ │ ├── views/
|
||||
│ │ │ ├── DailyMatch.vue # 日常使用页(核心)
|
||||
│ │ │ └── ConfigCenter.vue # 系统配置中心
|
||||
│ │ ├── api/index.js # API 封装
|
||||
│ │ └── ...
|
||||
│ └── package.json
|
||||
├── deploy/
|
||||
│ └── install.sh # Ubuntu 一键部署脚本
|
||||
├── ecosystem.config.cjs # PM2 进程配置
|
||||
├── DEPLOY.md # 部署文档
|
||||
└── README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 快速启动
|
||||
|
||||
### 1. 启动后端
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
|
||||
# 创建虚拟环境(推荐)
|
||||
python -m venv venv
|
||||
# Windows:
|
||||
venv\Scripts\activate
|
||||
# macOS/Linux:
|
||||
# source venv/bin/activate
|
||||
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 启动 API 服务(首次启动自动建表 + 写入默认数据)
|
||||
uvicorn app.main:app --reload --host 127.0.0.1 --port 8000
|
||||
```
|
||||
|
||||
后端启动后访问:
|
||||
- API 文档:http://127.0.0.1:8000/docs
|
||||
- 健康检查:http://127.0.0.1:8000/api/health
|
||||
|
||||
### 2. 启动前端
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
前端访问:http://localhost:1125
|
||||
|
||||
---
|
||||
|
||||
## 使用流程
|
||||
|
||||
### 日常使用(核心页面)
|
||||
|
||||
1. **人工选择**大盘周期(日线 / 4H / 1H)
|
||||
2. **人工选择**大盘阶段(8 个固定阶段)
|
||||
3. **人工选择**趋势强弱(强 / 弱 / 震荡)
|
||||
4. 点击「执行前置匹配」
|
||||
5. 系统自动输出:
|
||||
- 交易类型(顺势 / 反转 / 观望)
|
||||
- 允许开仓方向
|
||||
- 可用账户列表
|
||||
- 可用策略列表(含规则文本,仅展示)
|
||||
|
||||
### 系统配置中心
|
||||
|
||||
- **大盘管理**:增删改 8 个大盘阶段及交易方向
|
||||
- **账户管理**:动态管理账户(本金、周期、风险比)
|
||||
- **策略管理**:增删改策略及规则文本
|
||||
- **匹配配置**:绑定「大盘周期 + 阶段 + 强弱」→ 账户 + 策略 + 方向
|
||||
|
||||
---
|
||||
|
||||
## 匹配逻辑(默认规则)
|
||||
|
||||
| 大盘阶段 | 趋势强弱 | 匹配策略 | 方向 |
|
||||
|---------|---------|---------|------|
|
||||
| 上涨初期/中期 | 强 | 箱体顺势突破 | 做多 |
|
||||
| 上涨初期/中期 | 弱 | 斐波回调 | 做多 |
|
||||
| 上涨末期 | 强/弱 | 手工主观 | 做空 |
|
||||
| 下跌初期/中期 | 强 | 箱体顺势突破 | 做空 |
|
||||
| 下跌初期/中期 | 弱 | 斐波回调 | 做空 |
|
||||
| 下跌末期 | 强/弱 | 手工主观 | 做多 |
|
||||
| 宽幅震荡末期 | 强/弱 | 收敛结构突破 | 多空均可 |
|
||||
| 宽幅震荡 | 任意 | **全部禁用** | 禁止 |
|
||||
| 任意阶段 | 震荡 | **全部禁用** | 观望 |
|
||||
|
||||
---
|
||||
|
||||
## API 接口
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/api/regimes` | 获取大盘阶段列表 |
|
||||
| POST/PUT/DELETE | `/api/regimes` | 增删改大盘阶段 |
|
||||
| GET | `/api/accounts` | 获取账户列表 |
|
||||
| POST/PUT/DELETE | `/api/accounts` | 增删改账户 |
|
||||
| GET | `/api/strategies` | 获取策略列表 |
|
||||
| POST/PUT/DELETE | `/api/strategies` | 增删改策略 |
|
||||
| GET | `/api/matches` | 获取匹配配置 |
|
||||
| POST/PUT/DELETE | `/api/matches` | 增删改匹配配置 |
|
||||
| GET | `/api/match` | **前置匹配**(核心接口) |
|
||||
|
||||
匹配接口参数:
|
||||
```
|
||||
GET /api/match?market_cycle=日线&market_regime_id=1&trend_strength=强
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据库表
|
||||
|
||||
| 表名 | 说明 |
|
||||
|------|------|
|
||||
| `market_regime` | 大盘阶段(名称、交易类型、允许方向) |
|
||||
| `account` | 账户(名称、本金、周期、风险比、启用状态) |
|
||||
| `strategy` | 策略(名称、适用周期/强弱、规则文本) |
|
||||
| `regime_match` | 匹配绑定(阶段+周期+强弱 → 账户+策略+方向) |
|
||||
|
||||
建表 SQL 见 `backend/init_db.sql`,也可通过后端启动自动创建。
|
||||
|
||||
---
|
||||
|
||||
## 约束说明
|
||||
|
||||
1. **黑色极简** UI,左右分栏布局
|
||||
2. **SQLite 持久化**,重启不丢数据
|
||||
3. **无登录**,纯本地部署
|
||||
4. **账户/策略/匹配规则** 全部可动态增删改
|
||||
5. **严格禁止**:自动 K 线识别、自动箱体判断、自动下单、交易所对接、币种输入、点位计算、突破条件校验
|
||||
6. 策略规则文本**仅作展示**,系统不做任何自动校验
|
||||
|
||||
---
|
||||
|
||||
## 重置数据库
|
||||
|
||||
删除 `backend/data/pretrade.db` 后重启后端,系统将自动重建并写入默认数据。
|
||||
|
||||
---
|
||||
|
||||
## 生产部署
|
||||
|
||||
服务器部署(Ubuntu + PM2 + root + `/opt`)详见 **[DEPLOY.md](./DEPLOY.md)**。
|
||||
|
||||
一键部署:
|
||||
|
||||
```bash
|
||||
git clone https://git.bz121.com/dekun/crypto-pre-trade-system.git /opt/crypto-pre-trade-system
|
||||
cd /opt/crypto-pre-trade-system
|
||||
bash deploy/install.sh
|
||||
```
|
||||
|
||||
部署完成后访问:**http://\<服务器IP\>:1125**
|
||||
@@ -0,0 +1,173 @@
|
||||
"""数据库 CRUD 操作"""
|
||||
|
||||
from typing import List, Optional
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
|
||||
from app.models import MarketRegime, Account, Strategy, RegimeMatch
|
||||
from app.schemas import (
|
||||
MarketRegimeCreate, MarketRegimeUpdate,
|
||||
AccountCreate, AccountUpdate,
|
||||
StrategyCreate, StrategyUpdate,
|
||||
RegimeMatchCreate, RegimeMatchUpdate,
|
||||
)
|
||||
|
||||
|
||||
# ── 大盘阶段 ──
|
||||
|
||||
def get_regimes(db: Session) -> List[MarketRegime]:
|
||||
return db.query(MarketRegime).order_by(MarketRegime.id).all()
|
||||
|
||||
|
||||
def get_regime(db: Session, regime_id: int) -> Optional[MarketRegime]:
|
||||
return db.query(MarketRegime).filter(MarketRegime.id == regime_id).first()
|
||||
|
||||
|
||||
def create_regime(db: Session, data: MarketRegimeCreate) -> MarketRegime:
|
||||
obj = MarketRegime(**data.model_dump())
|
||||
db.add(obj)
|
||||
db.commit()
|
||||
db.refresh(obj)
|
||||
return obj
|
||||
|
||||
|
||||
def update_regime(db: Session, regime_id: int, data: MarketRegimeUpdate) -> Optional[MarketRegime]:
|
||||
obj = get_regime(db, regime_id)
|
||||
if not obj:
|
||||
return None
|
||||
for k, v in data.model_dump(exclude_unset=True).items():
|
||||
setattr(obj, k, v)
|
||||
db.commit()
|
||||
db.refresh(obj)
|
||||
return obj
|
||||
|
||||
|
||||
def delete_regime(db: Session, regime_id: int) -> bool:
|
||||
obj = get_regime(db, regime_id)
|
||||
if not obj:
|
||||
return False
|
||||
db.delete(obj)
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
|
||||
# ── 账户 ──
|
||||
|
||||
def get_accounts(db: Session) -> List[Account]:
|
||||
return db.query(Account).order_by(Account.id).all()
|
||||
|
||||
|
||||
def get_account(db: Session, account_id: int) -> Optional[Account]:
|
||||
return db.query(Account).filter(Account.id == account_id).first()
|
||||
|
||||
|
||||
def create_account(db: Session, data: AccountCreate) -> Account:
|
||||
obj = Account(**data.model_dump())
|
||||
db.add(obj)
|
||||
db.commit()
|
||||
db.refresh(obj)
|
||||
return obj
|
||||
|
||||
|
||||
def update_account(db: Session, account_id: int, data: AccountUpdate) -> Optional[Account]:
|
||||
obj = get_account(db, account_id)
|
||||
if not obj:
|
||||
return None
|
||||
for k, v in data.model_dump(exclude_unset=True).items():
|
||||
setattr(obj, k, v)
|
||||
db.commit()
|
||||
db.refresh(obj)
|
||||
return obj
|
||||
|
||||
|
||||
def delete_account(db: Session, account_id: int) -> bool:
|
||||
obj = get_account(db, account_id)
|
||||
if not obj:
|
||||
return False
|
||||
db.delete(obj)
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
|
||||
# ── 策略 ──
|
||||
|
||||
def get_strategies(db: Session) -> List[Strategy]:
|
||||
return db.query(Strategy).order_by(Strategy.id).all()
|
||||
|
||||
|
||||
def get_strategy(db: Session, strategy_id: int) -> Optional[Strategy]:
|
||||
return db.query(Strategy).filter(Strategy.id == strategy_id).first()
|
||||
|
||||
|
||||
def create_strategy(db: Session, data: StrategyCreate) -> Strategy:
|
||||
obj = Strategy(**data.model_dump())
|
||||
db.add(obj)
|
||||
db.commit()
|
||||
db.refresh(obj)
|
||||
return obj
|
||||
|
||||
|
||||
def update_strategy(db: Session, strategy_id: int, data: StrategyUpdate) -> Optional[Strategy]:
|
||||
obj = get_strategy(db, strategy_id)
|
||||
if not obj:
|
||||
return None
|
||||
for k, v in data.model_dump(exclude_unset=True).items():
|
||||
setattr(obj, k, v)
|
||||
db.commit()
|
||||
db.refresh(obj)
|
||||
return obj
|
||||
|
||||
|
||||
def delete_strategy(db: Session, strategy_id: int) -> bool:
|
||||
obj = get_strategy(db, strategy_id)
|
||||
if not obj:
|
||||
return False
|
||||
db.delete(obj)
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
|
||||
# ── 匹配绑定 ──
|
||||
|
||||
def get_matches(db: Session) -> List[RegimeMatch]:
|
||||
return (
|
||||
db.query(RegimeMatch)
|
||||
.options(
|
||||
joinedload(RegimeMatch.regime),
|
||||
joinedload(RegimeMatch.account),
|
||||
joinedload(RegimeMatch.strategy),
|
||||
)
|
||||
.order_by(RegimeMatch.id)
|
||||
.all()
|
||||
)
|
||||
|
||||
|
||||
def get_match(db: Session, match_id: int) -> Optional[RegimeMatch]:
|
||||
return db.query(RegimeMatch).filter(RegimeMatch.id == match_id).first()
|
||||
|
||||
|
||||
def create_match(db: Session, data: RegimeMatchCreate) -> RegimeMatch:
|
||||
obj = RegimeMatch(**data.model_dump())
|
||||
db.add(obj)
|
||||
db.commit()
|
||||
db.refresh(obj)
|
||||
return obj
|
||||
|
||||
|
||||
def update_match(db: Session, match_id: int, data: RegimeMatchUpdate) -> Optional[RegimeMatch]:
|
||||
obj = get_match(db, match_id)
|
||||
if not obj:
|
||||
return None
|
||||
for k, v in data.model_dump(exclude_unset=True).items():
|
||||
setattr(obj, k, v)
|
||||
db.commit()
|
||||
db.refresh(obj)
|
||||
return obj
|
||||
|
||||
|
||||
def delete_match(db: Session, match_id: int) -> bool:
|
||||
obj = get_match(db, match_id)
|
||||
if not obj:
|
||||
return False
|
||||
db.delete(obj)
|
||||
db.commit()
|
||||
return True
|
||||
@@ -0,0 +1,175 @@
|
||||
"""数据库连接与初始化"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy import create_engine, text
|
||||
from sqlalchemy.orm import sessionmaker, declarative_base
|
||||
|
||||
# 数据库文件路径
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
DB_PATH = BASE_DIR / "data" / "pretrade.db"
|
||||
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
DATABASE_URL = f"sqlite:///{DB_PATH}"
|
||||
|
||||
engine = create_engine(
|
||||
DATABASE_URL,
|
||||
connect_args={"check_same_thread": False},
|
||||
)
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
def get_db():
|
||||
"""FastAPI 依赖:获取数据库会话"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def init_database():
|
||||
"""执行建表 SQL 并写入默认数据"""
|
||||
from app.models import MarketRegime, Account, Strategy, RegimeMatch # noqa: F401
|
||||
|
||||
# 建表
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
# 若已有数据则跳过初始化
|
||||
db = SessionLocal()
|
||||
try:
|
||||
if db.query(MarketRegime).count() > 0:
|
||||
return
|
||||
|
||||
_seed_default_data(db)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def _seed_default_data(db):
|
||||
"""写入默认大盘阶段、账户、策略及匹配规则"""
|
||||
from app.models import MarketRegime, Account, Strategy, RegimeMatch
|
||||
|
||||
# ── 大盘阶段(8 个固定) ──
|
||||
regimes = [
|
||||
MarketRegime(name="上涨初期", trade_type="顺势", allow_direction="做多", remark="顺势做多,禁止做空"),
|
||||
MarketRegime(name="上涨中期", trade_type="顺势", allow_direction="做多", remark="顺势做多,禁止做空"),
|
||||
MarketRegime(name="上涨末期", trade_type="反转", allow_direction="做空", remark="反转做空"),
|
||||
MarketRegime(name="宽幅震荡", trade_type="观望", allow_direction="禁止", remark="观望,禁止交易"),
|
||||
MarketRegime(name="宽幅震荡末期", trade_type="反转", allow_direction="多空均可", remark="反转交易(收敛突破)"),
|
||||
MarketRegime(name="下跌初期", trade_type="顺势", allow_direction="做空", remark="顺势做空,禁止做多"),
|
||||
MarketRegime(name="下跌中期", trade_type="顺势", allow_direction="做空", remark="顺势做空,禁止做多"),
|
||||
MarketRegime(name="下跌末期", trade_type="反转", allow_direction="做多", remark="反转做多"),
|
||||
]
|
||||
db.add_all(regimes)
|
||||
db.flush()
|
||||
|
||||
regime_map = {r.name: r.id for r in regimes}
|
||||
|
||||
# ── 账户(默认本金 100U) ──
|
||||
accounts = [
|
||||
Account(account_name="账户1-斐波回调", total_capital=100, trade_cycle="4H/1H", risk_ratio="5%~10%", remark="斐波回调专用"),
|
||||
Account(account_name="账户2-箱体突破", total_capital=100, trade_cycle="日内", risk_ratio="0.5%~1%", remark="箱体顺势突破专用"),
|
||||
Account(account_name="账户3-收敛突破", total_capital=100, trade_cycle="日内", risk_ratio="0.5%~1%", remark="收敛结构突破专用"),
|
||||
Account(account_name="账户4-手工主观", total_capital=100, trade_cycle="灵活", risk_ratio="2%", remark="手工主观策略专用"),
|
||||
]
|
||||
db.add_all(accounts)
|
||||
db.flush()
|
||||
|
||||
acc_map = {a.account_name: a.id for a in accounts}
|
||||
|
||||
# ── 策略(4 个内置) ──
|
||||
strategies = [
|
||||
Strategy(
|
||||
strategy_name="斐波回调",
|
||||
fit_cycle="4H/1H",
|
||||
fit_trend_strength="弱",
|
||||
trade_type="顺势",
|
||||
strategy_rule=(
|
||||
"入场:仅 0.618 / 0.786\n"
|
||||
"适用:通道式上涨/下跌\n"
|
||||
"适用:弱趋势\n"
|
||||
"周期:4H/1H\n"
|
||||
"注意:点位、触碰次数、突破条件均由人工手动筛选输入"
|
||||
),
|
||||
),
|
||||
Strategy(
|
||||
strategy_name="箱体顺势突破",
|
||||
fit_cycle="日内(5分钟K线)",
|
||||
fit_trend_strength="强",
|
||||
trade_type="顺势",
|
||||
strategy_rule=(
|
||||
"箱体时长要求:≥4小时(48根5分钟K线),优先8小时以上\n"
|
||||
"成立条件:人工手动判断触碰上下沿次数、顺势/逆势箱体、突破确认条件\n"
|
||||
"系统不校验,仅展示该策略可用\n"
|
||||
"适用:强趋势顺势阶段"
|
||||
),
|
||||
),
|
||||
Strategy(
|
||||
strategy_name="收敛结构突破",
|
||||
fit_cycle="日内",
|
||||
fit_trend_strength="强",
|
||||
trade_type="反转",
|
||||
strategy_rule=(
|
||||
"适用:宽幅震荡末期收敛三角\n"
|
||||
"人工确认结构,系统仅匹配可用\n"
|
||||
"注意:结构细节、突破条件均由人工手动确认"
|
||||
),
|
||||
),
|
||||
Strategy(
|
||||
strategy_name="手工主观",
|
||||
fit_cycle="灵活",
|
||||
fit_trend_strength="全部",
|
||||
trade_type="全部",
|
||||
strategy_rule="全场景人工自主判断,系统仅做前置匹配",
|
||||
),
|
||||
]
|
||||
db.add_all(strategies)
|
||||
db.flush()
|
||||
|
||||
strat_map = {s.strategy_name: s.id for s in strategies}
|
||||
|
||||
# ── 匹配绑定规则 ──
|
||||
cycles = ["日线", "4H", "1H"]
|
||||
matches = []
|
||||
|
||||
# 顺势阶段(上涨初/中期、下跌初/中期):强→箱体,弱→斐波
|
||||
shunshi_regimes = ["上涨初期", "上涨中期", "下跌初期", "下跌中期"]
|
||||
for rname in shunshi_regimes:
|
||||
rid = regime_map[rname]
|
||||
for cycle in cycles:
|
||||
matches.append(RegimeMatch(
|
||||
market_regime_id=rid, market_cycle=cycle, trend_strength="强",
|
||||
account_id=acc_map["账户2-箱体突破"], strategy_id=strat_map["箱体顺势突破"],
|
||||
))
|
||||
matches.append(RegimeMatch(
|
||||
market_regime_id=rid, market_cycle=cycle, trend_strength="弱",
|
||||
account_id=acc_map["账户1-斐波回调"], strategy_id=strat_map["斐波回调"],
|
||||
))
|
||||
|
||||
# 反转阶段(上涨末期、下跌末期):手工主观
|
||||
fanzhuan_regimes = ["上涨末期", "下跌末期"]
|
||||
for rname in fanzhuan_regimes:
|
||||
rid = regime_map[rname]
|
||||
for cycle in cycles:
|
||||
for strength in ["强", "弱"]:
|
||||
matches.append(RegimeMatch(
|
||||
market_regime_id=rid, market_cycle=cycle, trend_strength=strength,
|
||||
account_id=acc_map["账户4-手工主观"], strategy_id=strat_map["手工主观"],
|
||||
))
|
||||
|
||||
# 宽幅震荡末期:收敛突破
|
||||
rid = regime_map["宽幅震荡末期"]
|
||||
for cycle in cycles:
|
||||
for strength in ["强", "弱"]:
|
||||
matches.append(RegimeMatch(
|
||||
market_regime_id=rid, market_cycle=cycle, trend_strength=strength,
|
||||
account_id=acc_map["账户3-收敛突破"], strategy_id=strat_map["收敛结构突破"],
|
||||
))
|
||||
|
||||
# 宽幅震荡:不写入匹配(系统自动禁用)
|
||||
|
||||
db.add_all(matches)
|
||||
db.commit()
|
||||
@@ -0,0 +1,74 @@
|
||||
"""FastAPI 应用入口"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from app.database import init_database
|
||||
from app.routes import regimes, accounts, strategies, matches, match
|
||||
|
||||
# 前端构建产物目录(生产部署时由 PM2 单端口托管)
|
||||
FRONTEND_DIST = Path(__file__).resolve().parent.parent.parent / "frontend" / "dist"
|
||||
|
||||
app = FastAPI(
|
||||
title="加密货币前置匹配系统",
|
||||
description="大盘-周期-阶段-强弱-方向-账户-策略前置匹配(纯本地,无登录)",
|
||||
version="1.0.0",
|
||||
)
|
||||
|
||||
# 允许前端跨域(本地开发)
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# 注册 API 路由
|
||||
app.include_router(regimes.router)
|
||||
app.include_router(accounts.router)
|
||||
app.include_router(strategies.router)
|
||||
app.include_router(matches.router)
|
||||
app.include_router(match.router)
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
def on_startup():
|
||||
"""启动时初始化数据库"""
|
||||
init_database()
|
||||
|
||||
|
||||
@app.get("/api/health")
|
||||
def health():
|
||||
return {"status": "ok", "message": "加密货币前置匹配系统运行中"}
|
||||
|
||||
|
||||
def _mount_frontend():
|
||||
"""生产环境:托管 Vue 前端静态资源,支持 SPA 路由"""
|
||||
if not FRONTEND_DIST.exists():
|
||||
return
|
||||
|
||||
assets_dir = FRONTEND_DIST / "assets"
|
||||
if assets_dir.exists():
|
||||
app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")
|
||||
|
||||
@app.get("/")
|
||||
async def serve_index():
|
||||
return FileResponse(FRONTEND_DIST / "index.html")
|
||||
|
||||
@app.get("/{path:path}")
|
||||
async def serve_spa(path: str):
|
||||
# API 路径不走 SPA 回退
|
||||
if path.startswith("api"):
|
||||
return {"detail": "Not Found"}
|
||||
file = FRONTEND_DIST / path
|
||||
if file.is_file():
|
||||
return FileResponse(file)
|
||||
return FileResponse(FRONTEND_DIST / "index.html")
|
||||
|
||||
|
||||
_mount_frontend()
|
||||
@@ -0,0 +1,138 @@
|
||||
"""前置策略匹配核心逻辑
|
||||
|
||||
本模块仅根据人工选择的大盘周期、阶段、趋势强弱,
|
||||
查询匹配绑定表并输出可用账户与策略。
|
||||
不做任何 K 线识别、箱体判断、点位计算或突破校验。
|
||||
"""
|
||||
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
|
||||
from app.models import MarketRegime, RegimeMatch
|
||||
from app.schemas import MatchRequest, MatchResult, MatchAccountOut, MatchStrategyOut
|
||||
|
||||
|
||||
def run_match(db: Session, req: MatchRequest) -> MatchResult:
|
||||
"""执行前置匹配"""
|
||||
regime = db.query(MarketRegime).filter(MarketRegime.id == req.market_regime_id).first()
|
||||
if not regime:
|
||||
return MatchResult(
|
||||
market_cycle=req.market_cycle,
|
||||
regime_name="未知",
|
||||
trade_type="",
|
||||
allow_direction="",
|
||||
trend_strength=req.trend_strength,
|
||||
status="disabled",
|
||||
message="大盘阶段不存在",
|
||||
)
|
||||
|
||||
base = dict(
|
||||
market_cycle=req.market_cycle,
|
||||
regime_name=regime.name,
|
||||
trade_type=regime.trade_type,
|
||||
allow_direction=regime.allow_direction,
|
||||
trend_strength=req.trend_strength,
|
||||
)
|
||||
|
||||
# 震荡 → 全部禁用
|
||||
if req.trend_strength == "震荡":
|
||||
return MatchResult(
|
||||
**base,
|
||||
status="watch",
|
||||
message="趋势强弱为「震荡」,全部策略禁用,建议观望",
|
||||
)
|
||||
|
||||
# 宽幅震荡阶段 → 观望
|
||||
if regime.trade_type == "观望" or regime.allow_direction == "禁止":
|
||||
return MatchResult(
|
||||
**base,
|
||||
status="watch",
|
||||
message=f"大盘阶段「{regime.name}」为观望阶段,禁止交易",
|
||||
)
|
||||
|
||||
# 查询匹配绑定
|
||||
matches = (
|
||||
db.query(RegimeMatch)
|
||||
.options(
|
||||
joinedload(RegimeMatch.account),
|
||||
joinedload(RegimeMatch.strategy),
|
||||
)
|
||||
.filter(
|
||||
RegimeMatch.market_regime_id == req.market_regime_id,
|
||||
RegimeMatch.market_cycle == req.market_cycle,
|
||||
RegimeMatch.trend_strength == req.trend_strength,
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
if not matches:
|
||||
return MatchResult(
|
||||
**base,
|
||||
status="disabled",
|
||||
message="未找到匹配的账户/策略绑定,请在配置中心添加匹配规则",
|
||||
)
|
||||
|
||||
# 过滤:仅保留已启用账户
|
||||
accounts_out = []
|
||||
strategies_out = []
|
||||
seen_acc = set()
|
||||
seen_strat = set()
|
||||
|
||||
for m in matches:
|
||||
if not m.account or m.account.enable != 1:
|
||||
continue
|
||||
|
||||
# 趋势强弱过滤(双重保险)
|
||||
strat = m.strategy
|
||||
if not strat:
|
||||
continue
|
||||
if not _strength_compatible(req.trend_strength, strat.fit_trend_strength):
|
||||
continue
|
||||
|
||||
direction = m.force_direction or regime.allow_direction
|
||||
|
||||
if m.account_id not in seen_acc:
|
||||
seen_acc.add(m.account_id)
|
||||
accounts_out.append(MatchAccountOut(
|
||||
id=m.account.id,
|
||||
account_name=m.account.account_name,
|
||||
total_capital=m.account.total_capital,
|
||||
trade_cycle=m.account.trade_cycle,
|
||||
risk_ratio=m.account.risk_ratio,
|
||||
force_direction=direction,
|
||||
))
|
||||
|
||||
if m.strategy_id not in seen_strat:
|
||||
seen_strat.add(m.strategy_id)
|
||||
strategies_out.append(MatchStrategyOut(
|
||||
id=strat.id,
|
||||
strategy_name=strat.strategy_name,
|
||||
fit_cycle=strat.fit_cycle,
|
||||
strategy_rule=strat.strategy_rule,
|
||||
force_direction=direction,
|
||||
))
|
||||
|
||||
if not accounts_out:
|
||||
return MatchResult(
|
||||
**base,
|
||||
status="disabled",
|
||||
message="无可用账户(可能全部被禁用或不匹配当前趋势强弱)",
|
||||
)
|
||||
|
||||
return MatchResult(
|
||||
**base,
|
||||
status="ok",
|
||||
message="匹配成功,以下为前置策略匹配结果(箱体/点位/突破条件需人工确认)",
|
||||
accounts=accounts_out,
|
||||
strategies=strategies_out,
|
||||
)
|
||||
|
||||
|
||||
def _strength_compatible(selected: str, fit: str) -> bool:
|
||||
"""判断趋势强弱是否与策略适配"""
|
||||
if fit == "全部":
|
||||
return True
|
||||
if selected == "强" and fit == "强":
|
||||
return True
|
||||
if selected == "弱" and fit == "弱":
|
||||
return True
|
||||
return False
|
||||
@@ -0,0 +1,66 @@
|
||||
"""SQLAlchemy ORM 模型"""
|
||||
|
||||
from sqlalchemy import Column, Integer, String, Float, ForeignKey, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class MarketRegime(Base):
|
||||
"""大盘阶段"""
|
||||
__tablename__ = "market_regime"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(String, nullable=False, unique=True)
|
||||
trade_type = Column(String, nullable=False) # 顺势 / 反转 / 观望
|
||||
allow_direction = Column(String, nullable=False) # 做多 / 做空 / 禁止 / 多空均可
|
||||
remark = Column(Text, default="")
|
||||
|
||||
matches = relationship("RegimeMatch", back_populates="regime", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class Account(Base):
|
||||
"""交易账户"""
|
||||
__tablename__ = "account"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
account_name = Column(String, nullable=False)
|
||||
total_capital = Column(Float, nullable=False, default=100)
|
||||
trade_cycle = Column(String, nullable=False)
|
||||
risk_ratio = Column(String, nullable=False)
|
||||
enable = Column(Integer, nullable=False, default=1)
|
||||
remark = Column(Text, default="")
|
||||
|
||||
matches = relationship("RegimeMatch", back_populates="account", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class Strategy(Base):
|
||||
"""交易策略(规则文本仅展示,系统不做校验)"""
|
||||
__tablename__ = "strategy"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
strategy_name = Column(String, nullable=False)
|
||||
fit_cycle = Column(String, nullable=False)
|
||||
fit_trend_strength = Column(String, nullable=False) # 强 / 弱 / 全部
|
||||
trade_type = Column(String, nullable=False) # 顺势 / 反转 / 全部
|
||||
strategy_rule = Column(Text, nullable=False)
|
||||
remark = Column(Text, default="")
|
||||
|
||||
matches = relationship("RegimeMatch", back_populates="strategy", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class RegimeMatch(Base):
|
||||
"""匹配绑定:大盘周期+阶段+强弱 → 账户+策略+方向"""
|
||||
__tablename__ = "regime_match"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
market_regime_id = Column(Integer, ForeignKey("market_regime.id", ondelete="CASCADE"), nullable=False)
|
||||
market_cycle = Column(String, nullable=False) # 日线 / 4H / 1H
|
||||
trend_strength = Column(String, nullable=False) # 强 / 弱 / 震荡
|
||||
account_id = Column(Integer, ForeignKey("account.id", ondelete="CASCADE"), nullable=False)
|
||||
strategy_id = Column(Integer, ForeignKey("strategy.id", ondelete="CASCADE"), nullable=False)
|
||||
force_direction = Column(String, default="") # 做多 / 做空 / 空=跟随大盘
|
||||
|
||||
regime = relationship("MarketRegime", back_populates="matches")
|
||||
account = relationship("Account", back_populates="matches")
|
||||
strategy = relationship("Strategy", back_populates="matches")
|
||||
@@ -0,0 +1,35 @@
|
||||
"""API 路由:账户"""
|
||||
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.database import get_db
|
||||
from app import crud, schemas
|
||||
|
||||
router = APIRouter(prefix="/api/accounts", tags=["账户"])
|
||||
|
||||
|
||||
@router.get("", response_model=List[schemas.AccountOut])
|
||||
def list_accounts(db: Session = Depends(get_db)):
|
||||
return crud.get_accounts(db)
|
||||
|
||||
|
||||
@router.post("", response_model=schemas.AccountOut, status_code=201)
|
||||
def create_account(data: schemas.AccountCreate, db: Session = Depends(get_db)):
|
||||
return crud.create_account(db, data)
|
||||
|
||||
|
||||
@router.put("/{account_id}", response_model=schemas.AccountOut)
|
||||
def update_account(account_id: int, data: schemas.AccountUpdate, db: Session = Depends(get_db)):
|
||||
obj = crud.update_account(db, account_id, data)
|
||||
if not obj:
|
||||
raise HTTPException(404, "账户不存在")
|
||||
return obj
|
||||
|
||||
|
||||
@router.delete("/{account_id}")
|
||||
def delete_account(account_id: int, db: Session = Depends(get_db)):
|
||||
if not crud.delete_account(db, account_id):
|
||||
raise HTTPException(404, "账户不存在")
|
||||
return {"ok": True}
|
||||
@@ -0,0 +1,31 @@
|
||||
"""API 路由:日常使用 - 前置匹配"""
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.database import get_db
|
||||
from app import schemas
|
||||
from app.matcher import run_match
|
||||
|
||||
router = APIRouter(prefix="/api/match", tags=["前置匹配"])
|
||||
|
||||
|
||||
@router.get("", response_model=schemas.MatchResult)
|
||||
def do_match(
|
||||
market_cycle: str = Query(..., description="大盘周期:日线/4H/1H"),
|
||||
market_regime_id: int = Query(..., description="大盘阶段 ID"),
|
||||
trend_strength: str = Query(..., description="趋势强弱:强/弱/震荡"),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""根据人工选择的三项参数,输出前置匹配结果"""
|
||||
req = schemas.MatchRequest(
|
||||
market_cycle=market_cycle,
|
||||
market_regime_id=market_regime_id,
|
||||
trend_strength=trend_strength,
|
||||
)
|
||||
return run_match(db, req)
|
||||
|
||||
|
||||
@router.post("", response_model=schemas.MatchResult)
|
||||
def do_match_post(req: schemas.MatchRequest, db: Session = Depends(get_db)):
|
||||
return run_match(db, req)
|
||||
@@ -0,0 +1,61 @@
|
||||
"""API 路由:匹配绑定"""
|
||||
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.database import get_db
|
||||
from app import crud, schemas
|
||||
|
||||
router = APIRouter(prefix="/api/matches", tags=["匹配配置"])
|
||||
|
||||
|
||||
def _enrich_match(m) -> schemas.RegimeMatchOut:
|
||||
"""填充关联名称"""
|
||||
return schemas.RegimeMatchOut(
|
||||
id=m.id,
|
||||
market_regime_id=m.market_regime_id,
|
||||
market_cycle=m.market_cycle,
|
||||
trend_strength=m.trend_strength,
|
||||
account_id=m.account_id,
|
||||
strategy_id=m.strategy_id,
|
||||
force_direction=m.force_direction or "",
|
||||
regime_name=m.regime.name if m.regime else None,
|
||||
account_name=m.account.account_name if m.account else None,
|
||||
strategy_name=m.strategy.strategy_name if m.strategy else None,
|
||||
)
|
||||
|
||||
|
||||
@router.get("", response_model=List[schemas.RegimeMatchOut])
|
||||
def list_matches(db: Session = Depends(get_db)):
|
||||
return [_enrich_match(m) for m in crud.get_matches(db)]
|
||||
|
||||
|
||||
@router.post("", response_model=schemas.RegimeMatchOut, status_code=201)
|
||||
def create_match(data: schemas.RegimeMatchCreate, db: Session = Depends(get_db)):
|
||||
obj = crud.create_match(db, data)
|
||||
# 重新加载关联
|
||||
matches = crud.get_matches(db)
|
||||
for m in matches:
|
||||
if m.id == obj.id:
|
||||
return _enrich_match(m)
|
||||
return schemas.RegimeMatchOut(id=obj.id, **data.model_dump())
|
||||
|
||||
|
||||
@router.put("/{match_id}", response_model=schemas.RegimeMatchOut)
|
||||
def update_match(match_id: int, data: schemas.RegimeMatchUpdate, db: Session = Depends(get_db)):
|
||||
obj = crud.update_match(db, match_id, data)
|
||||
if not obj:
|
||||
raise HTTPException(404, "匹配规则不存在")
|
||||
matches = crud.get_matches(db)
|
||||
for m in matches:
|
||||
if m.id == match_id:
|
||||
return _enrich_match(m)
|
||||
raise HTTPException(404, "匹配规则不存在")
|
||||
|
||||
|
||||
@router.delete("/{match_id}")
|
||||
def delete_match(match_id: int, db: Session = Depends(get_db)):
|
||||
if not crud.delete_match(db, match_id):
|
||||
raise HTTPException(404, "匹配规则不存在")
|
||||
return {"ok": True}
|
||||
@@ -0,0 +1,35 @@
|
||||
"""API 路由:大盘阶段"""
|
||||
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.database import get_db
|
||||
from app import crud, schemas
|
||||
|
||||
router = APIRouter(prefix="/api/regimes", tags=["大盘阶段"])
|
||||
|
||||
|
||||
@router.get("", response_model=List[schemas.MarketRegimeOut])
|
||||
def list_regimes(db: Session = Depends(get_db)):
|
||||
return crud.get_regimes(db)
|
||||
|
||||
|
||||
@router.post("", response_model=schemas.MarketRegimeOut, status_code=201)
|
||||
def create_regime(data: schemas.MarketRegimeCreate, db: Session = Depends(get_db)):
|
||||
return crud.create_regime(db, data)
|
||||
|
||||
|
||||
@router.put("/{regime_id}", response_model=schemas.MarketRegimeOut)
|
||||
def update_regime(regime_id: int, data: schemas.MarketRegimeUpdate, db: Session = Depends(get_db)):
|
||||
obj = crud.update_regime(db, regime_id, data)
|
||||
if not obj:
|
||||
raise HTTPException(404, "大盘阶段不存在")
|
||||
return obj
|
||||
|
||||
|
||||
@router.delete("/{regime_id}")
|
||||
def delete_regime(regime_id: int, db: Session = Depends(get_db)):
|
||||
if not crud.delete_regime(db, regime_id):
|
||||
raise HTTPException(404, "大盘阶段不存在")
|
||||
return {"ok": True}
|
||||
@@ -0,0 +1,35 @@
|
||||
"""API 路由:策略"""
|
||||
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.database import get_db
|
||||
from app import crud, schemas
|
||||
|
||||
router = APIRouter(prefix="/api/strategies", tags=["策略"])
|
||||
|
||||
|
||||
@router.get("", response_model=List[schemas.StrategyOut])
|
||||
def list_strategies(db: Session = Depends(get_db)):
|
||||
return crud.get_strategies(db)
|
||||
|
||||
|
||||
@router.post("", response_model=schemas.StrategyOut, status_code=201)
|
||||
def create_strategy(data: schemas.StrategyCreate, db: Session = Depends(get_db)):
|
||||
return crud.create_strategy(db, data)
|
||||
|
||||
|
||||
@router.put("/{strategy_id}", response_model=schemas.StrategyOut)
|
||||
def update_strategy(strategy_id: int, data: schemas.StrategyUpdate, db: Session = Depends(get_db)):
|
||||
obj = crud.update_strategy(db, strategy_id, data)
|
||||
if not obj:
|
||||
raise HTTPException(404, "策略不存在")
|
||||
return obj
|
||||
|
||||
|
||||
@router.delete("/{strategy_id}")
|
||||
def delete_strategy(strategy_id: int, db: Session = Depends(get_db)):
|
||||
if not crud.delete_strategy(db, strategy_id):
|
||||
raise HTTPException(404, "策略不存在")
|
||||
return {"ok": True}
|
||||
@@ -0,0 +1,171 @@
|
||||
"""Pydantic 请求/响应模型"""
|
||||
|
||||
from typing import Optional, List
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
# ── 大盘阶段 ──
|
||||
|
||||
class MarketRegimeBase(BaseModel):
|
||||
name: str
|
||||
trade_type: str
|
||||
allow_direction: str
|
||||
remark: str = ""
|
||||
|
||||
|
||||
class MarketRegimeCreate(MarketRegimeBase):
|
||||
pass
|
||||
|
||||
|
||||
class MarketRegimeUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
trade_type: Optional[str] = None
|
||||
allow_direction: Optional[str] = None
|
||||
remark: Optional[str] = None
|
||||
|
||||
|
||||
class MarketRegimeOut(MarketRegimeBase):
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# ── 账户 ──
|
||||
|
||||
class AccountBase(BaseModel):
|
||||
account_name: str
|
||||
total_capital: float = 100
|
||||
trade_cycle: str
|
||||
risk_ratio: str
|
||||
enable: int = 1
|
||||
remark: str = ""
|
||||
|
||||
|
||||
class AccountCreate(AccountBase):
|
||||
pass
|
||||
|
||||
|
||||
class AccountUpdate(BaseModel):
|
||||
account_name: Optional[str] = None
|
||||
total_capital: Optional[float] = None
|
||||
trade_cycle: Optional[str] = None
|
||||
risk_ratio: Optional[str] = None
|
||||
enable: Optional[int] = None
|
||||
remark: Optional[str] = None
|
||||
|
||||
|
||||
class AccountOut(AccountBase):
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# ── 策略 ──
|
||||
|
||||
class StrategyBase(BaseModel):
|
||||
strategy_name: str
|
||||
fit_cycle: str
|
||||
fit_trend_strength: str
|
||||
trade_type: str
|
||||
strategy_rule: str
|
||||
remark: str = ""
|
||||
|
||||
|
||||
class StrategyCreate(StrategyBase):
|
||||
pass
|
||||
|
||||
|
||||
class StrategyUpdate(BaseModel):
|
||||
strategy_name: Optional[str] = None
|
||||
fit_cycle: Optional[str] = None
|
||||
fit_trend_strength: Optional[str] = None
|
||||
trade_type: Optional[str] = None
|
||||
strategy_rule: Optional[str] = None
|
||||
remark: Optional[str] = None
|
||||
|
||||
|
||||
class StrategyOut(StrategyBase):
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# ── 匹配绑定 ──
|
||||
|
||||
class RegimeMatchBase(BaseModel):
|
||||
market_regime_id: int
|
||||
market_cycle: str
|
||||
trend_strength: str
|
||||
account_id: int
|
||||
strategy_id: int
|
||||
force_direction: str = ""
|
||||
|
||||
|
||||
class RegimeMatchCreate(RegimeMatchBase):
|
||||
pass
|
||||
|
||||
|
||||
class RegimeMatchUpdate(BaseModel):
|
||||
market_regime_id: Optional[int] = None
|
||||
market_cycle: Optional[str] = None
|
||||
trend_strength: Optional[str] = None
|
||||
account_id: Optional[int] = None
|
||||
strategy_id: Optional[int] = None
|
||||
force_direction: Optional[str] = None
|
||||
|
||||
|
||||
class RegimeMatchOut(RegimeMatchBase):
|
||||
id: int
|
||||
regime_name: Optional[str] = None
|
||||
account_name: Optional[str] = None
|
||||
strategy_name: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# ── 日常使用:匹配查询 ──
|
||||
|
||||
class MatchRequest(BaseModel):
|
||||
market_cycle: str = Field(..., description="大盘周期:日线/4H/1H")
|
||||
market_regime_id: int = Field(..., description="大盘阶段 ID")
|
||||
trend_strength: str = Field(..., description="趋势强弱:强/弱/震荡")
|
||||
|
||||
|
||||
class MatchAccountOut(BaseModel):
|
||||
id: int
|
||||
account_name: str
|
||||
total_capital: float
|
||||
trade_cycle: str
|
||||
risk_ratio: str
|
||||
force_direction: str = ""
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class MatchStrategyOut(BaseModel):
|
||||
id: int
|
||||
strategy_name: str
|
||||
fit_cycle: str
|
||||
strategy_rule: str
|
||||
force_direction: str = ""
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class MatchResult(BaseModel):
|
||||
"""匹配结果:系统仅做前置策略匹配,不做任何自动校验"""
|
||||
market_cycle: str
|
||||
regime_name: str
|
||||
trade_type: str
|
||||
allow_direction: str
|
||||
trend_strength: str
|
||||
status: str # ok / watch / disabled
|
||||
message: str = ""
|
||||
accounts: List[MatchAccountOut] = []
|
||||
strategies: List[MatchStrategyOut] = []
|
||||
@@ -0,0 +1,50 @@
|
||||
-- 加密货币前置匹配系统 - SQLite 建表脚本
|
||||
-- 本系统仅做前置策略匹配,不处理币种、点位、箱体细节
|
||||
|
||||
-- 1. 大盘阶段表
|
||||
CREATE TABLE IF NOT EXISTS market_regime (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
trade_type TEXT NOT NULL, -- 顺势 / 反转 / 观望
|
||||
allow_direction TEXT NOT NULL, -- 做多 / 做空 / 禁止 / 多空均可
|
||||
remark TEXT DEFAULT ''
|
||||
);
|
||||
|
||||
-- 2. 账户表
|
||||
CREATE TABLE IF NOT EXISTS account (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
account_name TEXT NOT NULL,
|
||||
total_capital REAL NOT NULL DEFAULT 100,
|
||||
trade_cycle TEXT NOT NULL, -- 如 4H/1H、日内、灵活
|
||||
risk_ratio TEXT NOT NULL, -- 如 5%~10%
|
||||
enable INTEGER NOT NULL DEFAULT 1,
|
||||
remark TEXT DEFAULT ''
|
||||
);
|
||||
|
||||
-- 3. 策略表
|
||||
CREATE TABLE IF NOT EXISTS strategy (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
strategy_name TEXT NOT NULL,
|
||||
fit_cycle TEXT NOT NULL, -- 适用周期
|
||||
fit_trend_strength TEXT NOT NULL, -- 强 / 弱 / 全部
|
||||
trade_type TEXT NOT NULL, -- 顺势 / 反转 / 全部
|
||||
strategy_rule TEXT NOT NULL, -- 策略规则文本(仅展示,不做校验)
|
||||
remark TEXT DEFAULT ''
|
||||
);
|
||||
|
||||
-- 4. 匹配绑定表
|
||||
CREATE TABLE IF NOT EXISTS regime_match (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
market_regime_id INTEGER NOT NULL,
|
||||
market_cycle TEXT NOT NULL, -- 日线 / 4H / 1H
|
||||
trend_strength TEXT NOT NULL, -- 强 / 弱 / 震荡
|
||||
account_id INTEGER NOT NULL,
|
||||
strategy_id INTEGER NOT NULL,
|
||||
force_direction TEXT DEFAULT '', -- 强制方向:做多 / 做空 / 空=跟随大盘
|
||||
FOREIGN KEY (market_regime_id) REFERENCES market_regime(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (account_id) REFERENCES account(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (strategy_id) REFERENCES strategy(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_regime_match_lookup
|
||||
ON regime_match(market_regime_id, market_cycle, trend_strength);
|
||||
@@ -0,0 +1,4 @@
|
||||
fastapi==0.115.6
|
||||
uvicorn[standard]==0.34.0
|
||||
sqlalchemy==2.0.36
|
||||
pydantic==2.10.4
|
||||
@@ -0,0 +1,113 @@
|
||||
#!/bin/bash
|
||||
# ============================================================
|
||||
# 加密货币前置匹配系统 — Ubuntu 一键部署脚本
|
||||
# 仓库:https://git.bz121.com/dekun/crypto-pre-trade-system.git
|
||||
# 部署路径:/opt/crypto-pre-trade-system
|
||||
# 运行用户:root
|
||||
# 进程守护:PM2
|
||||
# 访问端口:1125
|
||||
# ============================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_URL="https://git.bz121.com/dekun/crypto-pre-trade-system.git"
|
||||
INSTALL_DIR="/opt/crypto-pre-trade-system"
|
||||
PORT=1125
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
err() { echo -e "${RED}[ERROR]${NC} $*"; exit 1; }
|
||||
|
||||
# ── 检查 root 权限 ──
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
err "请使用 root 用户运行:sudo bash deploy/install.sh"
|
||||
fi
|
||||
|
||||
log "========== 开始部署 crypto-pre-trade-system =========="
|
||||
|
||||
# ── 1. 安装系统依赖 ──
|
||||
log "安装系统依赖..."
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq git curl python3 python3-venv python3-pip
|
||||
|
||||
# Node.js 18+(若未安装)
|
||||
if ! command -v node &>/dev/null; then
|
||||
log "安装 Node.js 20 LTS..."
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
|
||||
apt-get install -y -qq nodejs
|
||||
fi
|
||||
|
||||
# PM2(若未安装)
|
||||
if ! command -v pm2 &>/dev/null; then
|
||||
log "安装 PM2..."
|
||||
npm install -g pm2
|
||||
fi
|
||||
|
||||
log "Node $(node -v) | Python $(python3 --version) | PM2 $(pm2 -v)"
|
||||
|
||||
# ── 2. 拉取代码 ──
|
||||
if [ -d "$INSTALL_DIR/.git" ]; then
|
||||
log "更新代码..."
|
||||
cd "$INSTALL_DIR"
|
||||
git pull origin main 2>/dev/null || git pull origin master 2>/dev/null || git pull
|
||||
else
|
||||
log "克隆仓库..."
|
||||
git clone "$REPO_URL" "$INSTALL_DIR"
|
||||
cd "$INSTALL_DIR"
|
||||
fi
|
||||
|
||||
# ── 3. 创建日志目录 ──
|
||||
mkdir -p "$INSTALL_DIR/logs"
|
||||
mkdir -p "$INSTALL_DIR/backend/data"
|
||||
|
||||
# ── 4. Python 虚拟环境 + 依赖 ──
|
||||
log "配置 Python 虚拟环境..."
|
||||
cd "$INSTALL_DIR/backend"
|
||||
if [ ! -d "venv" ]; then
|
||||
python3 -m venv venv
|
||||
fi
|
||||
venv/bin/pip install -q --upgrade pip
|
||||
venv/bin/pip install -q -r requirements.txt
|
||||
|
||||
# ── 5. 构建前端 ──
|
||||
log "构建前端..."
|
||||
cd "$INSTALL_DIR/frontend"
|
||||
npm install --silent
|
||||
npm run build
|
||||
|
||||
# ── 6. PM2 启动 / 重启 ──
|
||||
log "启动 PM2 守护进程..."
|
||||
cd "$INSTALL_DIR"
|
||||
pm2 delete crypto-pre-trade 2>/dev/null || true
|
||||
pm2 start ecosystem.config.cjs
|
||||
pm2 save
|
||||
|
||||
# 设置 PM2 开机自启(已配置则跳过)
|
||||
pm2 startup systemd -u root --hp /root 2>/dev/null | tail -1 | bash 2>/dev/null || true
|
||||
|
||||
# ── 7. 健康检查 ──
|
||||
log "等待服务启动..."
|
||||
sleep 3
|
||||
if curl -sf "http://127.0.0.1:${PORT}/api/health" > /dev/null; then
|
||||
log "健康检查通过 ✓"
|
||||
else
|
||||
warn "健康检查未通过,请查看日志:pm2 logs crypto-pre-trade"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
log "========== 部署完成 =========="
|
||||
echo -e " 访问地址:${GREEN}http://<服务器IP>:${PORT}${NC}"
|
||||
echo -e " API 文档:${GREEN}http://<服务器IP>:${PORT}/docs${NC}"
|
||||
echo -e " 安装目录:${INSTALL_DIR}"
|
||||
echo -e " 常用命令:"
|
||||
echo -e " pm2 status # 查看状态"
|
||||
echo -e " pm2 logs crypto-pre-trade # 查看日志"
|
||||
echo -e " pm2 restart crypto-pre-trade # 重启服务"
|
||||
echo -e " bash deploy/install.sh # 更新并重新部署"
|
||||
echo ""
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* PM2 进程配置
|
||||
* 部署路径:/opt/crypto-pre-trade-system
|
||||
* 运行用户:root
|
||||
* 访问端口:1125
|
||||
*/
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: 'crypto-pre-trade',
|
||||
cwd: '/opt/crypto-pre-trade-system/backend',
|
||||
script: 'venv/bin/uvicorn',
|
||||
args: 'app.main:app --host 0.0.0.0 --port 1125',
|
||||
interpreter: 'none',
|
||||
autorestart: true,
|
||||
watch: false,
|
||||
max_memory_restart: '300M',
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
},
|
||||
error_file: '/opt/crypto-pre-trade-system/logs/pm2-error.log',
|
||||
out_file: '/opt/crypto-pre-trade-system/logs/pm2-out.log',
|
||||
log_date_format: 'YYYY-MM-DD HH:mm:ss',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>加密货币前置匹配系统</title>
|
||||
</head>
|
||||
<body class="bg-dark-bg text-dark-text">
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+2773
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "crypto-pre-trade-frontend",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0",
|
||||
"axios": "^1.7.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.49",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"vite": "^6.0.5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div class="min-h-screen flex flex-col">
|
||||
<!-- 顶部导航 -->
|
||||
<header class="border-b border-dark-border bg-dark-card px-6 py-3 flex items-center justify-between">
|
||||
<h1 class="text-lg font-semibold tracking-wide">加密货币前置匹配系统</h1>
|
||||
<nav class="flex gap-1">
|
||||
<router-link
|
||||
to="/"
|
||||
class="px-4 py-2 text-sm rounded-md transition-colors"
|
||||
:class="$route.path === '/' ? 'bg-dark-accent text-white' : 'text-dark-muted hover:text-dark-text'"
|
||||
>
|
||||
日常使用
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/config"
|
||||
class="px-4 py-2 text-sm rounded-md transition-colors"
|
||||
:class="$route.path === '/config' ? 'bg-dark-accent text-white' : 'text-dark-muted hover:text-dark-text'"
|
||||
>
|
||||
系统配置
|
||||
</router-link>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<!-- 主内容 -->
|
||||
<main class="flex-1 p-6">
|
||||
<router-view />
|
||||
</main>
|
||||
|
||||
<!-- 底部 -->
|
||||
<footer class="border-t border-dark-border px-6 py-2 text-xs text-dark-muted text-center">
|
||||
纯本地部署 · 无登录 · 仅做前置策略匹配 · 箱体/点位/突破条件需人工确认
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
</script>
|
||||
@@ -0,0 +1,35 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: '/api',
|
||||
timeout: 10000,
|
||||
})
|
||||
|
||||
// ── 大盘阶段 ──
|
||||
export const getRegimes = () => api.get('/regimes')
|
||||
export const createRegime = (data) => api.post('/regimes', data)
|
||||
export const updateRegime = (id, data) => api.put(`/regimes/${id}`, data)
|
||||
export const deleteRegime = (id) => api.delete(`/regimes/${id}`)
|
||||
|
||||
// ── 账户 ──
|
||||
export const getAccounts = () => api.get('/accounts')
|
||||
export const createAccount = (data) => api.post('/accounts', data)
|
||||
export const updateAccount = (id, data) => api.put(`/accounts/${id}`, data)
|
||||
export const deleteAccount = (id) => api.delete(`/accounts/${id}`)
|
||||
|
||||
// ── 策略 ──
|
||||
export const getStrategies = () => api.get('/strategies')
|
||||
export const createStrategy = (data) => api.post('/strategies', data)
|
||||
export const updateStrategy = (id, data) => api.put(`/strategies/${id}`, data)
|
||||
export const deleteStrategy = (id) => api.delete(`/strategies/${id}`)
|
||||
|
||||
// ── 匹配配置 ──
|
||||
export const getMatches = () => api.get('/matches')
|
||||
export const createMatch = (data) => api.post('/matches', data)
|
||||
export const updateMatch = (id, data) => api.put(`/matches/${id}`, data)
|
||||
export const deleteMatch = (id) => api.delete(`/matches/${id}`)
|
||||
|
||||
// ── 前置匹配 ──
|
||||
export const doMatch = (params) => api.get('/match', { params })
|
||||
|
||||
export default api
|
||||
@@ -0,0 +1,6 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import './style.css'
|
||||
|
||||
createApp(App).use(router).mount('#app')
|
||||
@@ -0,0 +1,13 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import DailyMatch from '../views/DailyMatch.vue'
|
||||
import ConfigCenter from '../views/ConfigCenter.vue'
|
||||
|
||||
const routes = [
|
||||
{ path: '/', name: 'DailyMatch', component: DailyMatch },
|
||||
{ path: '/config', name: 'ConfigCenter', component: ConfigCenter },
|
||||
]
|
||||
|
||||
export default createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
})
|
||||
@@ -0,0 +1,51 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
::-webkit-scrollbar-track { background: #141414; }
|
||||
::-webkit-scrollbar-thumb { background: #404040; border-radius: 3px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #525252; }
|
||||
|
||||
/* 通用卡片 */
|
||||
.card {
|
||||
@apply bg-dark-card border border-dark-border rounded-lg p-4;
|
||||
}
|
||||
|
||||
/* 通用按钮 */
|
||||
.btn {
|
||||
@apply px-4 py-2 rounded-md text-sm font-medium transition-colors;
|
||||
}
|
||||
.btn-primary {
|
||||
@apply btn bg-dark-accent text-white hover:bg-blue-600;
|
||||
}
|
||||
.btn-danger {
|
||||
@apply btn bg-dark-danger text-white hover:bg-red-600;
|
||||
}
|
||||
.btn-ghost {
|
||||
@apply btn bg-dark-hover text-dark-text hover:bg-dark-border;
|
||||
}
|
||||
|
||||
/* 表单元素 */
|
||||
.input {
|
||||
@apply w-full bg-dark-bg border border-dark-border rounded-md px-3 py-2 text-sm
|
||||
text-dark-text focus:outline-none focus:border-dark-accent;
|
||||
}
|
||||
.select {
|
||||
@apply input appearance-none cursor-pointer;
|
||||
}
|
||||
.textarea {
|
||||
@apply input resize-y min-h-[80px];
|
||||
}
|
||||
|
||||
/* 标签页 */
|
||||
.tab-active {
|
||||
@apply border-b-2 border-dark-accent text-dark-accent;
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- 标签页切换 -->
|
||||
<div class="flex gap-0 border-b border-dark-border mb-6">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
@click="activeTab = tab.key"
|
||||
class="px-5 py-3 text-sm font-medium transition-colors"
|
||||
:class="activeTab === tab.key ? 'tab-active' : 'text-dark-muted hover:text-dark-text'"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 大盘管理 -->
|
||||
<section v-if="activeTab === 'regimes'">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-base font-semibold">大盘阶段管理</h2>
|
||||
<button @click="openRegimeForm()" class="btn-primary">新增阶段</button>
|
||||
</div>
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-dark-border text-dark-muted text-left">
|
||||
<th class="py-2 px-3">ID</th>
|
||||
<th class="py-2 px-3">名称</th>
|
||||
<th class="py-2 px-3">交易类型</th>
|
||||
<th class="py-2 px-3">允许方向</th>
|
||||
<th class="py-2 px-3">备注</th>
|
||||
<th class="py-2 px-3 w-32">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="r in regimes" :key="r.id" class="border-b border-dark-border/50 hover:bg-dark-hover">
|
||||
<td class="py-2 px-3">{{ r.id }}</td>
|
||||
<td class="py-2 px-3 font-medium">{{ r.name }}</td>
|
||||
<td class="py-2 px-3">{{ r.trade_type }}</td>
|
||||
<td class="py-2 px-3">{{ r.allow_direction }}</td>
|
||||
<td class="py-2 px-3 text-dark-muted">{{ r.remark }}</td>
|
||||
<td class="py-2 px-3">
|
||||
<button @click="openRegimeForm(r)" class="text-dark-accent text-xs mr-2 hover:underline">编辑</button>
|
||||
<button @click="handleDeleteRegime(r.id)" class="text-dark-danger text-xs hover:underline">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<!-- 账户管理 -->
|
||||
<section v-if="activeTab === 'accounts'">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-base font-semibold">账户管理</h2>
|
||||
<button @click="openAccountForm()" class="btn-primary">新增账户</button>
|
||||
</div>
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-dark-border text-dark-muted text-left">
|
||||
<th class="py-2 px-3">ID</th>
|
||||
<th class="py-2 px-3">账户名</th>
|
||||
<th class="py-2 px-3">本金(U)</th>
|
||||
<th class="py-2 px-3">交易周期</th>
|
||||
<th class="py-2 px-3">风险比</th>
|
||||
<th class="py-2 px-3">状态</th>
|
||||
<th class="py-2 px-3 w-32">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="a in accounts" :key="a.id" class="border-b border-dark-border/50 hover:bg-dark-hover">
|
||||
<td class="py-2 px-3">{{ a.id }}</td>
|
||||
<td class="py-2 px-3 font-medium">{{ a.account_name }}</td>
|
||||
<td class="py-2 px-3">{{ a.total_capital }}</td>
|
||||
<td class="py-2 px-3">{{ a.trade_cycle }}</td>
|
||||
<td class="py-2 px-3">{{ a.risk_ratio }}</td>
|
||||
<td class="py-2 px-3">
|
||||
<span :class="a.enable ? 'text-dark-success' : 'text-dark-danger'">
|
||||
{{ a.enable ? '启用' : '禁用' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-2 px-3">
|
||||
<button @click="openAccountForm(a)" class="text-dark-accent text-xs mr-2 hover:underline">编辑</button>
|
||||
<button @click="handleDeleteAccount(a.id)" class="text-dark-danger text-xs hover:underline">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<!-- 策略管理 -->
|
||||
<section v-if="activeTab === 'strategies'">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-base font-semibold">策略管理</h2>
|
||||
<button @click="openStrategyForm()" class="btn-primary">新增策略</button>
|
||||
</div>
|
||||
<div class="grid gap-4">
|
||||
<div v-for="s in strategies" :key="s.id" class="card">
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<div>
|
||||
<h3 class="font-medium">{{ s.strategy_name }}</h3>
|
||||
<p class="text-xs text-dark-muted mt-1">
|
||||
周期: {{ s.fit_cycle }} · 趋势: {{ s.fit_trend_strength }} · 类型: {{ s.trade_type }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<button @click="openStrategyForm(s)" class="text-dark-accent text-xs mr-2 hover:underline">编辑</button>
|
||||
<button @click="handleDeleteStrategy(s.id)" class="text-dark-danger text-xs hover:underline">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
<pre class="text-xs text-dark-muted whitespace-pre-wrap bg-dark-bg rounded p-3">{{ s.strategy_rule }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 匹配配置 -->
|
||||
<section v-if="activeTab === 'matches'">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-base font-semibold">匹配配置</h2>
|
||||
<button @click="openMatchForm()" class="btn-primary">新增匹配</button>
|
||||
</div>
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-dark-border text-dark-muted text-left">
|
||||
<th class="py-2 px-3">ID</th>
|
||||
<th class="py-2 px-3">大盘阶段</th>
|
||||
<th class="py-2 px-3">周期</th>
|
||||
<th class="py-2 px-3">强弱</th>
|
||||
<th class="py-2 px-3">账户</th>
|
||||
<th class="py-2 px-3">策略</th>
|
||||
<th class="py-2 px-3">强制方向</th>
|
||||
<th class="py-2 px-3 w-32">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="m in matchList" :key="m.id" class="border-b border-dark-border/50 hover:bg-dark-hover">
|
||||
<td class="py-2 px-3">{{ m.id }}</td>
|
||||
<td class="py-2 px-3">{{ m.regime_name }}</td>
|
||||
<td class="py-2 px-3">{{ m.market_cycle }}</td>
|
||||
<td class="py-2 px-3">{{ m.trend_strength }}</td>
|
||||
<td class="py-2 px-3">{{ m.account_name }}</td>
|
||||
<td class="py-2 px-3">{{ m.strategy_name }}</td>
|
||||
<td class="py-2 px-3 text-dark-muted">{{ m.force_direction || '跟随大盘' }}</td>
|
||||
<td class="py-2 px-3">
|
||||
<button @click="openMatchForm(m)" class="text-dark-accent text-xs mr-2 hover:underline">编辑</button>
|
||||
<button @click="handleDeleteMatch(m.id)" class="text-dark-danger text-xs hover:underline">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<!-- 通用弹窗 -->
|
||||
<div v-if="showModal" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50" @click.self="showModal = false">
|
||||
<div class="card w-full max-w-lg max-h-[80vh] overflow-y-auto">
|
||||
<h3 class="text-base font-semibold mb-4">{{ modalTitle }}</h3>
|
||||
<form @submit.prevent="handleSubmit" class="flex flex-col gap-3">
|
||||
<!-- 大盘阶段表单 -->
|
||||
<template v-if="formType === 'regime'">
|
||||
<input v-model="formData.name" class="input" placeholder="阶段名称" required />
|
||||
<select v-model="formData.trade_type" class="select" required>
|
||||
<option value="顺势">顺势</option>
|
||||
<option value="反转">反转</option>
|
||||
<option value="观望">观望</option>
|
||||
</select>
|
||||
<select v-model="formData.allow_direction" class="select" required>
|
||||
<option value="做多">做多</option>
|
||||
<option value="做空">做空</option>
|
||||
<option value="禁止">禁止</option>
|
||||
<option value="多空均可">多空均可</option>
|
||||
</select>
|
||||
<input v-model="formData.remark" class="input" placeholder="备注" />
|
||||
</template>
|
||||
|
||||
<!-- 账户表单 -->
|
||||
<template v-if="formType === 'account'">
|
||||
<input v-model="formData.account_name" class="input" placeholder="账户名称" required />
|
||||
<input v-model.number="formData.total_capital" type="number" class="input" placeholder="本金(U)" required />
|
||||
<input v-model="formData.trade_cycle" class="input" placeholder="交易周期" required />
|
||||
<input v-model="formData.risk_ratio" class="input" placeholder="风险比" required />
|
||||
<select v-model.number="formData.enable" class="select">
|
||||
<option :value="1">启用</option>
|
||||
<option :value="0">禁用</option>
|
||||
</select>
|
||||
<input v-model="formData.remark" class="input" placeholder="备注" />
|
||||
</template>
|
||||
|
||||
<!-- 策略表单 -->
|
||||
<template v-if="formType === 'strategy'">
|
||||
<input v-model="formData.strategy_name" class="input" placeholder="策略名称" required />
|
||||
<input v-model="formData.fit_cycle" class="input" placeholder="适用周期" required />
|
||||
<select v-model="formData.fit_trend_strength" class="select" required>
|
||||
<option value="强">强</option>
|
||||
<option value="弱">弱</option>
|
||||
<option value="全部">全部</option>
|
||||
</select>
|
||||
<select v-model="formData.trade_type" class="select" required>
|
||||
<option value="顺势">顺势</option>
|
||||
<option value="反转">反转</option>
|
||||
<option value="全部">全部</option>
|
||||
</select>
|
||||
<textarea v-model="formData.strategy_rule" class="textarea" placeholder="策略规则文本" required></textarea>
|
||||
<input v-model="formData.remark" class="input" placeholder="备注" />
|
||||
</template>
|
||||
|
||||
<!-- 匹配表单 -->
|
||||
<template v-if="formType === 'match'">
|
||||
<select v-model.number="formData.market_regime_id" class="select" required>
|
||||
<option :value="null" disabled>选择大盘阶段</option>
|
||||
<option v-for="r in regimes" :key="r.id" :value="r.id">{{ r.name }}</option>
|
||||
</select>
|
||||
<select v-model="formData.market_cycle" class="select" required>
|
||||
<option value="日线">日线</option>
|
||||
<option value="4H">4H</option>
|
||||
<option value="1H">1H</option>
|
||||
</select>
|
||||
<select v-model="formData.trend_strength" class="select" required>
|
||||
<option value="强">强</option>
|
||||
<option value="弱">弱</option>
|
||||
<option value="震荡">震荡</option>
|
||||
</select>
|
||||
<select v-model.number="formData.account_id" class="select" required>
|
||||
<option :value="null" disabled>选择账户</option>
|
||||
<option v-for="a in accounts" :key="a.id" :value="a.id">{{ a.account_name }}</option>
|
||||
</select>
|
||||
<select v-model.number="formData.strategy_id" class="select" required>
|
||||
<option :value="null" disabled>选择策略</option>
|
||||
<option v-for="s in strategies" :key="s.id" :value="s.id">{{ s.strategy_name }}</option>
|
||||
</select>
|
||||
<select v-model="formData.force_direction" class="select">
|
||||
<option value="">跟随大盘</option>
|
||||
<option value="做多">做多</option>
|
||||
<option value="做空">做空</option>
|
||||
</select>
|
||||
</template>
|
||||
|
||||
<div class="flex gap-2 justify-end mt-2">
|
||||
<button type="button" @click="showModal = false" class="btn-ghost">取消</button>
|
||||
<button type="submit" class="btn-primary">保存</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import {
|
||||
getRegimes, createRegime, updateRegime, deleteRegime,
|
||||
getAccounts, createAccount, updateAccount, deleteAccount,
|
||||
getStrategies, createStrategy, updateStrategy, deleteStrategy,
|
||||
getMatches, createMatch, updateMatch, deleteMatch,
|
||||
} from '../api'
|
||||
|
||||
const tabs = [
|
||||
{ key: 'regimes', label: '大盘管理' },
|
||||
{ key: 'accounts', label: '账户管理' },
|
||||
{ key: 'strategies', label: '策略管理' },
|
||||
{ key: 'matches', label: '匹配配置' },
|
||||
]
|
||||
|
||||
const activeTab = ref('regimes')
|
||||
const regimes = ref([])
|
||||
const accounts = ref([])
|
||||
const strategies = ref([])
|
||||
const matchList = ref([])
|
||||
|
||||
// 弹窗
|
||||
const showModal = ref(false)
|
||||
const modalTitle = ref('')
|
||||
const formType = ref('')
|
||||
const formData = ref({})
|
||||
const editingId = ref(null)
|
||||
|
||||
async function loadAll() {
|
||||
const [r, a, s, m] = await Promise.all([
|
||||
getRegimes(), getAccounts(), getStrategies(), getMatches(),
|
||||
])
|
||||
regimes.value = r.data
|
||||
accounts.value = a.data
|
||||
strategies.value = s.data
|
||||
matchList.value = m.data
|
||||
}
|
||||
|
||||
// ── 大盘 ──
|
||||
function openRegimeForm(item = null) {
|
||||
formType.value = 'regime'
|
||||
editingId.value = item?.id || null
|
||||
modalTitle.value = item ? '编辑大盘阶段' : '新增大盘阶段'
|
||||
formData.value = item ? { ...item } : { name: '', trade_type: '顺势', allow_direction: '做多', remark: '' }
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
async function handleDeleteRegime(id) {
|
||||
if (!confirm('确定删除此大盘阶段?关联匹配规则也会删除。')) return
|
||||
await deleteRegime(id)
|
||||
loadAll()
|
||||
}
|
||||
|
||||
// ── 账户 ──
|
||||
function openAccountForm(item = null) {
|
||||
formType.value = 'account'
|
||||
editingId.value = item?.id || null
|
||||
modalTitle.value = item ? '编辑账户' : '新增账户'
|
||||
formData.value = item ? { ...item } : { account_name: '', total_capital: 100, trade_cycle: '', risk_ratio: '', enable: 1, remark: '' }
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
async function handleDeleteAccount(id) {
|
||||
if (!confirm('确定删除此账户?')) return
|
||||
await deleteAccount(id)
|
||||
loadAll()
|
||||
}
|
||||
|
||||
// ── 策略 ──
|
||||
function openStrategyForm(item = null) {
|
||||
formType.value = 'strategy'
|
||||
editingId.value = item?.id || null
|
||||
modalTitle.value = item ? '编辑策略' : '新增策略'
|
||||
formData.value = item ? { ...item } : { strategy_name: '', fit_cycle: '', fit_trend_strength: '强', trade_type: '顺势', strategy_rule: '', remark: '' }
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
async function handleDeleteStrategy(id) {
|
||||
if (!confirm('确定删除此策略?')) return
|
||||
await deleteStrategy(id)
|
||||
loadAll()
|
||||
}
|
||||
|
||||
// ── 匹配 ──
|
||||
function openMatchForm(item = null) {
|
||||
formType.value = 'match'
|
||||
editingId.value = item?.id || null
|
||||
modalTitle.value = item ? '编辑匹配规则' : '新增匹配规则'
|
||||
formData.value = item
|
||||
? { ...item }
|
||||
: { market_regime_id: null, market_cycle: '日线', trend_strength: '强', account_id: null, strategy_id: null, force_direction: '' }
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
async function handleDeleteMatch(id) {
|
||||
if (!confirm('确定删除此匹配规则?')) return
|
||||
await deleteMatch(id)
|
||||
loadAll()
|
||||
}
|
||||
|
||||
// ── 提交 ──
|
||||
async function handleSubmit() {
|
||||
const id = editingId.value
|
||||
const data = { ...formData.value }
|
||||
delete data.id
|
||||
delete data.regime_name
|
||||
delete data.account_name
|
||||
delete data.strategy_name
|
||||
|
||||
const actions = {
|
||||
regime: id ? () => updateRegime(id, data) : () => createRegime(data),
|
||||
account: id ? () => updateAccount(id, data) : () => createAccount(data),
|
||||
strategy: id ? () => updateStrategy(id, data) : () => createStrategy(data),
|
||||
match: id ? () => updateMatch(id, data) : () => createMatch(data),
|
||||
}
|
||||
|
||||
await actions[formType.value]()
|
||||
showModal.value = false
|
||||
loadAll()
|
||||
}
|
||||
|
||||
onMounted(loadAll)
|
||||
</script>
|
||||
@@ -0,0 +1,290 @@
|
||||
<template>
|
||||
<div class="flex gap-6 h-[calc(100vh-120px)]">
|
||||
<!-- 左侧:人工选择 -->
|
||||
<div class="w-80 shrink-0 card flex flex-col gap-6">
|
||||
<h2 class="text-base font-semibold border-b border-dark-border pb-3">人工选择</h2>
|
||||
|
||||
<!-- 大盘周期 -->
|
||||
<div>
|
||||
<label class="block text-sm text-dark-muted mb-2">大盘周期</label>
|
||||
<div class="flex flex-col gap-2">
|
||||
<button
|
||||
v-for="c in cycles"
|
||||
:key="c"
|
||||
@click="selected.cycle = c"
|
||||
class="px-4 py-2.5 rounded-md text-sm text-left transition-colors border"
|
||||
:class="selected.cycle === c
|
||||
? 'border-dark-accent bg-dark-accent/10 text-dark-accent'
|
||||
: 'border-dark-border hover:border-dark-muted text-dark-text'"
|
||||
>
|
||||
{{ c }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 大盘阶段 -->
|
||||
<div>
|
||||
<label class="block text-sm text-dark-muted mb-2">大盘阶段</label>
|
||||
<div class="flex flex-col gap-1.5 max-h-64 overflow-y-auto">
|
||||
<button
|
||||
v-for="r in regimes"
|
||||
:key="r.id"
|
||||
@click="selected.regimeId = r.id"
|
||||
class="px-3 py-2 rounded-md text-sm text-left transition-colors border"
|
||||
:class="selected.regimeId === r.id
|
||||
? 'border-dark-accent bg-dark-accent/10 text-dark-accent'
|
||||
: 'border-dark-border hover:border-dark-muted text-dark-text'"
|
||||
>
|
||||
<span>{{ r.name }}</span>
|
||||
<span class="ml-2 text-xs text-dark-muted">{{ r.trade_type }} · {{ r.allow_direction }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 趋势强弱 -->
|
||||
<div>
|
||||
<label class="block text-sm text-dark-muted mb-2">趋势强弱</label>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
v-for="s in strengths"
|
||||
:key="s.value"
|
||||
@click="selected.strength = s.value"
|
||||
class="flex-1 px-3 py-2.5 rounded-md text-sm transition-colors border"
|
||||
:class="selected.strength === s.value
|
||||
? strengthActiveClass(s.value)
|
||||
: 'border-dark-border hover:border-dark-muted text-dark-text'"
|
||||
>
|
||||
{{ s.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 匹配按钮 -->
|
||||
<button
|
||||
@click="runMatchQuery"
|
||||
:disabled="!canMatch || loading"
|
||||
class="btn-primary w-full py-3 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
>
|
||||
{{ loading ? '匹配中...' : '执行前置匹配' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:匹配结果 -->
|
||||
<div class="flex-1 flex flex-col gap-4 overflow-y-auto">
|
||||
<!-- 未匹配提示 -->
|
||||
<div v-if="!result" class="card flex-1 flex items-center justify-center text-dark-muted">
|
||||
<div class="text-center">
|
||||
<p class="text-lg mb-2">请选择大盘周期、阶段、趋势强弱</p>
|
||||
<p class="text-sm">点击「执行前置匹配」查看可用账户与策略</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<!-- 状态提示 -->
|
||||
<div
|
||||
class="card border-l-4"
|
||||
:class="statusBorderClass"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-2xl">{{ statusIcon }}</span>
|
||||
<div>
|
||||
<p class="font-medium">{{ result.message }}</p>
|
||||
<p class="text-sm text-dark-muted mt-1">
|
||||
{{ result.market_cycle }} · {{ result.regime_name }} · {{ result.trend_strength }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 大盘信息 -->
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="card text-center">
|
||||
<p class="text-xs text-dark-muted mb-1">交易类型</p>
|
||||
<p class="text-lg font-semibold" :class="tradeTypeColor">{{ result.trade_type || '—' }}</p>
|
||||
</div>
|
||||
<div class="card text-center">
|
||||
<p class="text-xs text-dark-muted mb-1">允许开仓方向</p>
|
||||
<p class="text-lg font-semibold" :class="directionColor">{{ result.allow_direction || '—' }}</p>
|
||||
</div>
|
||||
<div class="card text-center">
|
||||
<p class="text-xs text-dark-muted mb-1">匹配状态</p>
|
||||
<p class="text-lg font-semibold" :class="statusTextColor">{{ statusLabel }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 可用账户 -->
|
||||
<div class="card">
|
||||
<h3 class="text-sm font-semibold mb-3 flex items-center gap-2">
|
||||
<span class="w-2 h-2 rounded-full bg-dark-success"></span>
|
||||
可用账户
|
||||
<span class="text-dark-muted font-normal">({{ result.accounts?.length || 0 }})</span>
|
||||
</h3>
|
||||
<div v-if="!result.accounts?.length" class="text-dark-muted text-sm py-4 text-center">
|
||||
无可用账户
|
||||
</div>
|
||||
<div v-else class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div
|
||||
v-for="acc in result.accounts"
|
||||
:key="acc.id"
|
||||
class="border border-dark-border rounded-md p-3 hover:border-dark-accent/50 transition-colors"
|
||||
>
|
||||
<div class="flex justify-between items-start">
|
||||
<p class="font-medium">{{ acc.account_name }}</p>
|
||||
<span class="text-xs px-2 py-0.5 rounded bg-dark-hover text-dark-muted">
|
||||
{{ acc.force_direction || result.allow_direction }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-2 grid grid-cols-3 gap-2 text-xs text-dark-muted">
|
||||
<div>
|
||||
<span class="block text-dark-text font-medium">{{ acc.total_capital }}U</span>
|
||||
本金
|
||||
</div>
|
||||
<div>
|
||||
<span class="block text-dark-text font-medium">{{ acc.trade_cycle }}</span>
|
||||
周期
|
||||
</div>
|
||||
<div>
|
||||
<span class="block text-dark-text font-medium">{{ acc.risk_ratio }}</span>
|
||||
风险
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 可用策略 -->
|
||||
<div class="card">
|
||||
<h3 class="text-sm font-semibold mb-3 flex items-center gap-2">
|
||||
<span class="w-2 h-2 rounded-full bg-dark-accent"></span>
|
||||
可用策略
|
||||
<span class="text-dark-muted font-normal">({{ result.strategies?.length || 0 }})</span>
|
||||
</h3>
|
||||
<div v-if="!result.strategies?.length" class="text-dark-muted text-sm py-4 text-center">
|
||||
无可用策略
|
||||
</div>
|
||||
<div v-else class="flex flex-col gap-3">
|
||||
<div
|
||||
v-for="strat in result.strategies"
|
||||
:key="strat.id"
|
||||
class="border border-dark-border rounded-md p-4 hover:border-dark-accent/50 transition-colors"
|
||||
>
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<p class="font-medium text-base">{{ strat.strategy_name }}</p>
|
||||
<span class="text-xs px-2 py-0.5 rounded bg-dark-hover text-dark-muted">
|
||||
{{ strat.fit_cycle }}
|
||||
</span>
|
||||
</div>
|
||||
<pre class="text-xs text-dark-muted whitespace-pre-wrap leading-relaxed bg-dark-bg rounded p-3">{{ strat.strategy_rule }}</pre>
|
||||
<p class="text-xs text-dark-warning mt-2">
|
||||
⚠ 策略规则仅作参考展示,箱体/点位/突破条件需人工手动确认
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { getRegimes, doMatch } from '../api'
|
||||
|
||||
const cycles = ['日线', '4H', '1H']
|
||||
const strengths = [
|
||||
{ value: '强', label: '强趋势' },
|
||||
{ value: '弱', label: '弱趋势' },
|
||||
{ value: '震荡', label: '震荡' },
|
||||
]
|
||||
|
||||
const regimes = ref([])
|
||||
const selected = ref({ cycle: '', regimeId: null, strength: '' })
|
||||
const result = ref(null)
|
||||
const loading = ref(false)
|
||||
|
||||
const canMatch = computed(() =>
|
||||
selected.value.cycle && selected.value.regimeId && selected.value.strength
|
||||
)
|
||||
|
||||
const statusLabel = computed(() => {
|
||||
if (!result.value) return ''
|
||||
const map = { ok: '可交易', watch: '观望', disabled: '禁用' }
|
||||
return map[result.value.status] || result.value.status
|
||||
})
|
||||
|
||||
const statusIcon = computed(() => {
|
||||
if (!result.value) return ''
|
||||
const map = { ok: '✅', watch: '⏸️', disabled: '🚫' }
|
||||
return map[result.value.status] || '❓'
|
||||
})
|
||||
|
||||
const statusBorderClass = computed(() => {
|
||||
if (!result.value) return ''
|
||||
const map = {
|
||||
ok: 'border-l-dark-success',
|
||||
watch: 'border-l-dark-warning',
|
||||
disabled: 'border-l-dark-danger',
|
||||
}
|
||||
return map[result.value.status] || ''
|
||||
})
|
||||
|
||||
const statusTextColor = computed(() => {
|
||||
if (!result.value) return ''
|
||||
const map = {
|
||||
ok: 'text-dark-success',
|
||||
watch: 'text-dark-warning',
|
||||
disabled: 'text-dark-danger',
|
||||
}
|
||||
return map[result.value.status] || ''
|
||||
})
|
||||
|
||||
const tradeTypeColor = computed(() => {
|
||||
if (!result.value) return ''
|
||||
const map = { '顺势': 'text-dark-success', '反转': 'text-dark-warning', '观望': 'text-dark-muted' }
|
||||
return map[result.value.trade_type] || ''
|
||||
})
|
||||
|
||||
const directionColor = computed(() => {
|
||||
if (!result.value) return ''
|
||||
if (result.value.allow_direction === '禁止') return 'text-dark-danger'
|
||||
if (result.value.allow_direction === '做多') return 'text-dark-success'
|
||||
if (result.value.allow_direction === '做空') return 'text-dark-danger'
|
||||
return 'text-dark-accent'
|
||||
})
|
||||
|
||||
function strengthActiveClass(val) {
|
||||
const map = {
|
||||
'强': 'border-dark-success bg-dark-success/10 text-dark-success',
|
||||
'弱': 'border-dark-accent bg-dark-accent/10 text-dark-accent',
|
||||
'震荡': 'border-dark-warning bg-dark-warning/10 text-dark-warning',
|
||||
}
|
||||
return map[val] || ''
|
||||
}
|
||||
|
||||
async function loadRegimes() {
|
||||
const { data } = await getRegimes()
|
||||
regimes.value = data
|
||||
}
|
||||
|
||||
async function runMatchQuery() {
|
||||
if (!canMatch.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const { data } = await doMatch({
|
||||
market_cycle: selected.value.cycle,
|
||||
market_regime_id: selected.value.regimeId,
|
||||
trend_strength: selected.value.strength,
|
||||
})
|
||||
result.value = data
|
||||
} catch (e) {
|
||||
result.value = {
|
||||
status: 'disabled',
|
||||
message: '匹配请求失败:' + (e.response?.data?.detail || e.message),
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadRegimes)
|
||||
</script>
|
||||
@@ -0,0 +1,23 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./index.html', './src/**/*.{vue,js}'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
dark: {
|
||||
bg: '#0a0a0a',
|
||||
card: '#141414',
|
||||
border: '#262626',
|
||||
hover: '#1f1f1f',
|
||||
text: '#e5e5e5',
|
||||
muted: '#737373',
|
||||
accent: '#3b82f6',
|
||||
success: '#22c55e',
|
||||
warning: '#eab308',
|
||||
danger: '#ef4444',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
port: 1125,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://127.0.0.1:8000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user