first commit
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user