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
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