diff --git a/ft-app/app/engine/lock_strategy.py b/ft-app/app/engine/lock_strategy.py index ce4b2d6..32e492b 100644 --- a/ft-app/app/engine/lock_strategy.py +++ b/ft-app/app/engine/lock_strategy.py @@ -28,6 +28,6 @@ def check_lock( return (current_price - open_price) >= lock_threshold -def should_meltdown(lock_count: int) -> bool: - """3锁熔断""" - return lock_count >= 3 +def should_meltdown(lock_count: int, max_locks: int = 3) -> bool: + """锁仓次数达到上限时熔断""" + return lock_count >= max_locks diff --git a/ft-app/app/models.py b/ft-app/app/models.py index 99dffb0..41e021f 100644 --- a/ft-app/app/models.py +++ b/ft-app/app/models.py @@ -95,6 +95,7 @@ class Round(Base): id: Mapped[int] = mapped_column(primary_key=True) contract_code: Mapped[str] = mapped_column(String(10), index=True) daily_hands: Mapped[int] = mapped_column(Integer, default=1) + max_locks: Mapped[int] = mapped_column(Integer, default=3) status: Mapped[str] = mapped_column(String(20), default="active") started_at: Mapped[date] = mapped_column(Date) diff --git a/ft-app/app/routers/positions.py b/ft-app/app/routers/positions.py index a16e63a..1f54211 100644 --- a/ft-app/app/routers/positions.py +++ b/ft-app/app/routers/positions.py @@ -139,6 +139,19 @@ def update_lock_price( return RedirectResponse("/positions/", status_code=303) +@router.post("/{round_id}/max-locks") +def update_max_locks( + round_id: int, + max_locks: int = Form(...), + db: Session = Depends(get_db), +): + r = db.query(Round).filter(Round.id == round_id).first() + if r and r.status == "active" and 1 <= max_locks <= 10: + r.max_locks = max_locks + db.commit() + return RedirectResponse("/positions/", status_code=303) + + @router.post("/{round_id}/close") def close_round( request: Request, diff --git a/ft-app/app/seed.py b/ft-app/app/seed.py index 9c7a657..03108cf 100644 --- a/ft-app/app/seed.py +++ b/ft-app/app/seed.py @@ -27,8 +27,24 @@ SEED_BARS: list[dict] = [ {"date": "2026-07-24", "open": 900, "close": 908, "high": 912, "low": 891}, ] +def _migrate(engine): + """Add missing columns to existing tables (SQLite doesn't auto-migrate).""" + import sqlite3 + conn = engine.raw_connection() + try: + cur = conn.cursor() + cur.execute("PRAGMA table_info(rounds)") + cols = {row[1] for row in cur.fetchall()} + if "max_locks" not in cols: + cur.execute("ALTER TABLE rounds ADD COLUMN max_locks INTEGER DEFAULT 3") + conn.commit() + finally: + conn.close() + + def seed(): Base.metadata.create_all(bind=engine) + _migrate(engine) db = SessionLocal() try: diff --git a/ft-app/app/templates/positions.html b/ft-app/app/templates/positions.html index 7f94803..d49ab77 100644 --- a/ft-app/app/templates/positions.html +++ b/ft-app/app/templates/positions.html @@ -58,11 +58,13 @@ {{ round.daily_hands }} 手/天 已开 {{ round.positions|length }} 天 开始 {{ round.started_at }} - - 锁仓 {{ locks }}/3 - {% if locks >= 3 %} ⚠ 熔断{% endif %} + + 锁仓 {{ locks }}/{{ round.max_locks }} + {% if locks >= round.max_locks %} ⚠ 熔断{% endif %} +
@@ -180,6 +182,24 @@ function filterContracts() { }); } } + function showMaxLocksModal(rid, currentMax) { + document.getElementById('maxLocksRid').value = rid; + document.getElementById('maxLocksInput').value = currentMax; + document.getElementById('maxLocksModal').style.display = 'block'; + document.getElementById('maxLocksModalOverlay').style.display = 'block'; + document.getElementById('maxLocksInput').focus(); + } + function hideMaxLocksModal() { + document.getElementById('maxLocksModal').style.display = 'none'; + document.getElementById('maxLocksModalOverlay').style.display = 'none'; + } + function doUpdateMaxLocks() { + var rid = document.getElementById('maxLocksRid').value; + var fd = new FormData(); + fd.append('max_locks', document.getElementById('maxLocksInput').value); + fetch('/positions/' + rid + '/max-locks', {method:'POST', body:fd}) + .then(function() { location.reload(); }); + } function showLockConfirm(pid, lockPrice) { document.getElementById('lockPid').value = pid; document.getElementById('lockPriceInput').value = lockPrice; @@ -200,6 +220,20 @@ function filterContracts() { } + + +