first commit

This commit is contained in:
dekun
2026-05-28 21:43:23 +08:00
commit 1d5c97904f
33 changed files with 5250 additions and 0 deletions
+24
View File
@@ -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/
+193
View File
@@ -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)。
+195
View File
@@ -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**
View File
+173
View File
@@ -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
+175
View File
@@ -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()
+74
View File
@@ -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()
+138
View File
@@ -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
+66
View File
@@ -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")
View File
+35
View File
@@ -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}
+31
View File
@@ -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)
+61
View File
@@ -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}
+35
View File
@@ -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}
+35
View File
@@ -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}
+171
View File
@@ -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] = []
+50
View File
@@ -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);
+4
View File
@@ -0,0 +1,4 @@
fastapi==0.115.6
uvicorn[standard]==0.34.0
sqlalchemy==2.0.36
pydantic==2.10.4
+113
View File
@@ -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 ""
+26
View File
@@ -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',
},
],
};
+12
View File
@@ -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>
+2773
View File
File diff suppressed because it is too large Load Diff
+23
View File
@@ -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"
}
}
+6
View File
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
+37
View File
@@ -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>
+35
View File
@@ -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
+6
View File
@@ -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')
+13
View File
@@ -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,
})
+51
View File
@@ -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;
}
+367
View File
@@ -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>
+290
View File
@@ -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>
+23
View File
@@ -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: [],
}
+15
View File
@@ -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,
},
},
},
})