仓位管理支持多合约同时开轮

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-07-26 10:34:25 +08:00
parent 58d99259e8
commit 51be04e5d2
3 changed files with 138 additions and 203 deletions
+20 -15
View File
@@ -20,11 +20,11 @@ def get_active_contracts(db: Session) -> list[str]:
@router.get("/", response_class=HTMLResponse)
def positions_page(request: Request, db: Session = Depends(get_db)):
active_round = (
active_rounds = (
db.query(Round)
.filter(Round.status == "active")
.order_by(Round.started_at.desc())
.first()
.all()
)
template = request.app.state.templates.get_template("positions.html")
@@ -33,7 +33,7 @@ def positions_page(request: Request, db: Session = Depends(get_db)):
request=request,
active_nav="positions",
contracts=get_active_contracts(db),
active_round=active_round,
active_rounds=active_rounds,
)
)
@@ -45,12 +45,15 @@ def create_round(
daily_hands: int = Form(1),
db: Session = Depends(get_db),
):
existing = db.query(Round).filter(Round.status == "active").first()
code = contract_code.upper()
existing = db.query(Round).filter(
Round.status == "active", Round.contract_code == code
).first()
if existing:
return RedirectResponse("/positions/?error=已有活跃轮次", status_code=303)
return RedirectResponse(f"/positions/?error={code} 已有活跃轮次", status_code=303)
r = Round(
contract_code=contract_code.upper(),
contract_code=code,
daily_hands=daily_hands,
status="active",
started_at=date.today(),
@@ -60,21 +63,22 @@ def create_round(
return RedirectResponse("/positions/", status_code=303)
@router.post("/add")
@router.post("/{round_id}/add")
def add_position(
request: Request,
round_id: int,
open_price: int = Form(...),
amp_threshold: int = Form(...),
hands: int = Form(1),
db: Session = Depends(get_db),
):
active_round = db.query(Round).filter(Round.status == "active").first()
if not active_round:
return RedirectResponse("/positions/?error=无活跃轮次", status_code=303)
r = db.query(Round).filter(Round.id == round_id).first()
if not r or r.status != "active":
return RedirectResponse("/positions/?error=轮次不存在或已结束", status_code=303)
lock_price = open_price + amp_threshold
p = Position(
round_id=active_round.id,
round_id=r.id,
open_price=open_price,
amp_threshold=amp_threshold,
lock_price=lock_price,
@@ -104,14 +108,15 @@ def unlock_position(position_id: int, db: Session = Depends(get_db)):
return RedirectResponse("/positions/", status_code=303)
@router.post("/close")
@router.post("/{round_id}/close")
def close_round(
request: Request,
round_id: int,
result: str = Form(...),
db: Session = Depends(get_db),
):
active_round = db.query(Round).filter(Round.status == "active").first()
if active_round:
active_round.status = result
r = db.query(Round).filter(Round.id == round_id).first()
if r and r.status == "active":
r.status = result
db.commit()
return RedirectResponse("/positions/", status_code=303)
+6 -24
View File
@@ -59,12 +59,6 @@
border-left-color: var(--accent); font-weight: 600;
}
.sidebar-nav .icon { font-size: 1.1rem; width: 22px; text-align: center; }
.nav-parent { position: relative; }
.nav-arrow { font-size: 0.65rem; margin-left: auto; transition: transform .2s; color: var(--sub); }
.nav-group.open .nav-arrow { transform: rotate(-180deg); }
.nav-children { display: none; }
.nav-group.open .nav-children { display: block; }
.nav-children a { padding-left: 48px !important; font-size: 0.84rem !important; }
.sidebar-footer {
padding: 16px 20px; border-top: 1px solid var(--border);
}
@@ -201,20 +195,12 @@
<aside class="sidebar">
<div class="sidebar-logo">📊 期货量化</div>
<nav class="sidebar-nav">
<div class="nav-group{% if active_nav in ('contracts', 'positions') %} open{% endif %}" id="nav-market">
<a href="/contracts/" class="nav-parent{% if active_nav in ('contracts', 'positions') %} active{% endif %}" onclick="toggleNavGroup(event, 'nav-market')">
<span class="icon">📈</span> 行情数据
<span class="nav-arrow"></span>
</a>
<div class="nav-children">
<a href="/contracts/" class="{% if active_nav == 'contracts' %}active{% endif %}">
<span class="icon">📋</span> 合约列表
</a>
<a href="/positions/" class="{% if active_nav == 'positions' %}active{% endif %}">
<span class="icon">📐</span> 仓位管理
</a>
</div>
</div>
<a href="/contracts/" class="{% if active_nav == 'contracts' %}active{% endif %}">
<span class="icon">📈</span> 行情数据
</a>
<a href="/positions/" class="{% if active_nav == 'positions' %}active{% endif %}">
<span class="icon">📐</span> 仓位管理
</a>
<a href="/admin/" class="{% if active_nav == 'admin' %}active{% endif %}">
<span class="icon">⚙️</span> 系统管理
</a>
@@ -278,10 +264,6 @@ function closeDrawer() {
document.getElementById('drawer').classList.remove('show');
if (activeRow) { activeRow.classList.remove('active'); activeRow = null; }
}
function toggleNavGroup(e, id) {
e.preventDefault();
document.getElementById(id).classList.toggle('open');
}
</script>
</body>
</html>
+112 -164
View File
@@ -11,7 +11,6 @@
<div style="background:var(--danger-bg);color:var(--danger-fg);padding:10px 16px;border-radius:6px;margin-bottom:16px;font-size:0.88rem;">{{ error }}</div>
{% endif %}
{% if not active_round %}
{# ═══════════════ 新建轮次 ═══════════════ #}
<div class="section-title">新建轮次</div>
<div class="form-card" style="margin-bottom:28px;">
@@ -35,179 +34,128 @@
</form>
</div>
{% if not active_rounds %}
<div style="text-align:center;padding:60px;color:var(--sub);">
<p style="font-size:1.1rem;margin-bottom:8px;">暂无活跃轮次</p>
<p style="font-size:0.82rem;">创建一轮新的交易,开始跟踪仓位和锁仓状态</p>
</div>
{% endif %}
{% else %}
{# ═══════════════ 活跃轮次信息 ═══════════════ #}
{% set locks = active_round.total_locks %}
<div class="stat-grid">
<div class="stat-card">
<div class="label">合约</div>
<div class="value">{{ active_round.contract_code }}</div>
</div>
<div class="stat-card">
<div class="label">每日手数</div>
<div class="value">{{ active_round.daily_hands }} 手/天</div>
</div>
<div class="stat-card">
<div class="label">已开仓天数</div>
<div class="value">{{ active_round.positions|length }} 天</div>
</div>
<div class="stat-card" style="{% if locks >= 3 %}border-color:var(--danger);background:var(--danger-bg);{% endif %}">
<div class="label">累计锁仓</div>
<div class="value" style="{% if locks >= 3 %}color:var(--danger-fg);{% endif %}">
{{ locks }} / 3
{% if locks >= 3 %}⚠ 熔断{% endif %}
</div>
</div>
<div class="stat-card">
<div class="label">开始日期</div>
<div class="value" style="font-size:1rem;">{{ active_round.started_at }}</div>
</div>
</div>
{% for round in active_rounds %}
{# ═══════════════ 轮次卡片 ═══════════════ #}
{% set locks = round.total_locks %}
<div style="margin-bottom:32px;border:1px solid var(--border);border-radius:10px;overflow:hidden;">
{# ═══════════════ 止盈阈值提示 ═══════════════ #}
{% if active_round.positions|length > 0 %}
{% set latest = active_round.positions[-1] %}
<div style="background:var(--accent-light);border:1px solid var(--accent);border-radius:8px;padding:14px 18px;margin-bottom:24px;display:flex;align-items:center;gap:24px;flex-wrap:wrap;">
<div>
{# ── 轮次头部 ── #}
<div style="display:flex;align-items:center;gap:16px;padding:14px 20px;background:var(--th-bg);flex-wrap:wrap;">
<strong style="font-size:1rem;">{{ round.contract_code }}</strong>
<span class="badge badge-up">{{ round.daily_hands }} 手/天</span>
<span style="font-size:0.82rem;color:var(--sub);">已开 {{ round.positions|length }} 天</span>
<span style="font-size:0.82rem;color:var(--sub);">开始 {{ round.started_at }}</span>
<span style="font-size:0.82rem;font-weight:600;{% if locks >= 3 %}color:var(--danger-fg);{% endif %}">
锁仓 {{ locks }}/3
{% if locks >= 3 %} ⚠ 熔断{% endif %}
</span>
<span style="margin-left:auto;display:flex;gap:8px;">
<form method="post" action="/positions/{{ round.id }}/close" style="display:inline;">
<input type="hidden" name="result" value="profit_taken">
<button class="btn" style="background:var(--success);color:#fff;font-size:0.78rem;padding:4px 14px;" onclick="return confirm('确认 {{ round.contract_code }} 止盈清仓?')">止盈</button>
</form>
<form method="post" action="/positions/{{ round.id }}/close" style="display:inline;">
<input type="hidden" name="result" value="meltdown">
<button class="btn" style="background:var(--danger);color:#fff;font-size:0.78rem;padding:4px 14px;" onclick="return confirm('确认 {{ round.contract_code }} 熔断清仓?')">熔断</button>
</form>
</span>
</div>
<div style="padding:16px 20px;">
{# ── 止盈阈值 ── #}
{% if round.positions %}
{% set latest = round.positions[-1] %}
<div style="background:var(--accent-light);border:1px solid var(--accent);border-radius:6px;padding:10px 16px;margin-bottom:16px;display:flex;align-items:center;gap:20px;flex-wrap:wrap;">
<span style="font-size:0.78rem;color:var(--sub);">止盈阈值</span>
<span style="font-weight:700;font-size:1.1rem;margin-left:8px;">{{ active_round.daily_hands }} × {{ latest.amp_threshold }} = {{ active_round.daily_hands * latest.amp_threshold }} 点</span>
<span style="font-weight:700;">{{ round.daily_hands }} × {{ latest.amp_threshold }} = {{ round.daily_hands * latest.amp_threshold }} 点</span>
<span style="color:var(--sub);">|</span>
<span style="font-size:0.78rem;color:var(--sub);">当前 A = {{ latest.amp_threshold }}</span>
<span style="color:var(--sub);">|</span>
<span style="font-size:0.78rem;color:var(--sub);">总手数 {{ round.daily_hands * round.positions|length }}</span>
</div>
<div>
<span style="font-size:0.78rem;color:var(--sub);">当前振幅 A</span>
<span style="font-weight:700;font-size:1.1rem;margin-left:8px;">{{ latest.amp_threshold }} 点</span>
{% endif %}
{# ── 添加仓位 ── #}
<div style="margin-bottom:16px;">
<form method="post" action="/positions/{{ round.id }}/add">
<div style="display:flex;gap:10px;align-items:flex-end;flex-wrap:wrap;">
<div class="form-group" style="margin-bottom:0;flex:1;min-width:100px;">
<label>开仓价</label>
<input type="number" name="open_price" required placeholder="如 1300" style="font-size:0.9rem;">
</div>
<div class="form-group" style="margin-bottom:0;flex:1;min-width:100px;">
<label>振幅 A</label>
<input type="number" name="amp_threshold" required placeholder="如 18" style="font-size:0.9rem;">
</div>
<div class="form-group" style="margin-bottom:0;flex:1;min-width:70px;">
<label>手数</label>
<input type="number" name="hands" value="{{ round.daily_hands }}" min="1" max="10" required style="font-size:0.9rem;">
</div>
<button type="submit" class="btn btn-primary" style="font-size:0.82rem;padding:8px 18px;">+ 添加</button>
</div>
</form>
</div>
<div>
<span style="font-size:0.78rem;color:var(--sub);">总手数</span>
<span style="font-weight:700;font-size:1.1rem;margin-left:8px;">{{ active_round.daily_hands * active_round.positions|length }}</span>
{# ── 仓位表格 ── #}
{% if round.positions %}
<div class="table-wrap" style="margin-bottom:0;">
<table style="font-size:0.84rem;">
<tr>
<th>日期</th><th>开仓价</th><th>A</th><th>锁仓价</th><th>手数</th><th>已锁</th><th>状态</th><th>操作</th>
</tr>
{% for p in round.positions|reverse %}
<tr>
<td><strong>{{ p.opened_at }}</strong></td>
<td>{{ p.open_price }}</td>
<td>{{ p.amp_threshold }}</td>
<td style="color:var(--danger-fg);font-weight:600;">{{ p.lock_price }}</td>
<td>{{ p.hands }}</td>
<td>
{% if p.locked_count > 0 %}
<span class="badge badge-warn">{{ p.locked_count }}/{{ p.hands }}</span>
{% else %}
<span class="na">0/{{ p.hands }}</span>
{% endif %}
</td>
<td>
{% if p.locked_count >= p.hands %}
<span class="badge badge-down">已全锁</span>
{% elif p.locked_count > 0 %}
<span class="badge badge-warn">部分锁</span>
{% else %}
<span class="badge badge-up">活跃</span>
{% endif %}
</td>
<td>
<div style="display:flex;gap:6px;justify-content:center;">
<form method="post" action="/positions/{{ p.id }}/lock" style="display:inline;">
<button style="background:var(--warn-bg);color:var(--warn-fg);border:1px solid var(--warn);padding:2px 8px;border-radius:4px;cursor:pointer;font-size:0.75rem;font-weight:600;"
{% if p.locked_count >= p.hands %}disabled style="opacity:0.4;cursor:default;"{% endif %}>锁</button>
</form>
<form method="post" action="/positions/{{ p.id }}/unlock" style="display:inline;">
<button style="background:var(--surface);color:var(--sub);border:1px solid var(--border);padding:2px 8px;border-radius:4px;cursor:pointer;font-size:0.75rem;"
{% if p.locked_count == 0 %}disabled style="opacity:0.4;cursor:default;"{% endif %}></button>
</form>
</div>
</td>
</tr>
{% endfor %}
</table>
</div>
{% else %}
<div style="text-align:center;padding:24px;color:var(--sub);font-size:0.84rem;">暂无仓位,请添加。</div>
{% endif %}
</div>
</div>
{% endif %}
{% endfor %}
{# ═══════════════ 添加仓位 ═══════════════ #}
<div class="section-title">添加当日仓位</div>
<div class="form-card" style="margin-bottom:28px;">
<form method="post" action="/positions/add" id="addForm">
<div style="display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap;">
<div class="form-group" style="margin-bottom:0;flex:1;min-width:120px;">
<label>开仓价</label>
<input type="number" name="open_price" id="openPrice" required placeholder="如 1300" style="font-size:1rem;">
</div>
<div class="form-group" style="margin-bottom:0;flex:1;min-width:120px;">
<label>振幅阈值 A</label>
<input type="number" name="amp_threshold" id="ampThreshold" required placeholder="如 18" style="font-size:1rem;">
</div>
<div class="form-group" style="margin-bottom:0;flex:1;min-width:80px;">
<label>手数</label>
<input type="number" name="hands" value="1" min="1" max="10" required>
</div>
<div style="display:flex;flex-direction:column;align-items:center;min-width:100px;">
<span style="font-size:0.75rem;color:var(--sub);margin-bottom:4px;">锁仓价位</span>
<span id="lockPricePreview" style="font-size:1.3rem;font-weight:700;color:var(--danger-fg);"></span>
</div>
<button type="submit" class="btn btn-primary">添加仓位</button>
</div>
</form>
</div>
{# ═══════════════ 仓位列表 ═══════════════ #}
<div class="section-title">本轮仓位</div>
{% if active_round.positions %}
<div class="table-wrap">
<table>
<tr>
<th>日期</th>
<th>开仓价</th>
<th>振幅 A</th>
<th>锁仓价位</th>
<th>手数</th>
<th>已锁</th>
<th>状态</th>
<th>操作</th>
</tr>
{% for p in active_round.positions|reverse %}
<tr>
<td><strong>{{ p.opened_at }}</strong></td>
<td>{{ p.open_price }}</td>
<td>{{ p.amp_threshold }}</td>
<td style="color:var(--danger-fg);font-weight:600;">{{ p.lock_price }}</td>
<td>{{ p.hands }}</td>
<td>
{% if p.locked_count > 0 %}
<span class="badge badge-warn">{{ p.locked_count }}/{{ p.hands }}</span>
{% else %}
<span class="na">0/{{ p.hands }}</span>
{% endif %}
</td>
<td>
{% if p.locked_count >= p.hands %}
<span class="badge badge-down">已全锁</span>
{% elif p.locked_count > 0 %}
<span class="badge badge-warn">部分锁仓</span>
{% else %}
<span class="badge badge-up">活跃</span>
{% endif %}
</td>
<td>
<div style="display:flex;gap:8px;justify-content:center;">
<form method="post" action="/positions/{{ p.id }}/lock" style="display:inline;">
<button style="background:var(--warn-bg);color:var(--warn-fg);border:1px solid var(--warn);padding:3px 10px;border-radius:4px;cursor:pointer;font-size:0.78rem;font-weight:600;"
{% if p.locked_count >= p.hands %}disabled style="opacity:0.4;cursor:default;"{% endif %}>
🔒 锁
</button>
</form>
<form method="post" action="/positions/{{ p.id }}/unlock" style="display:inline;">
<button style="background:var(--surface);color:var(--sub);border:1px solid var(--border);padding:3px 10px;border-radius:4px;cursor:pointer;font-size:0.78rem;"
{% if p.locked_count == 0 %}disabled style="opacity:0.4;cursor:default;"{% endif %}>
撤销
</button>
</form>
</div>
</td>
</tr>
{% endfor %}
</table>
</div>
{% else %}
<div style="text-align:center;padding:40px;color:var(--sub);">暂无仓位,请添加当日仓位。</div>
{% endif %}
{# ═══════════════ 结束轮次 ═══════════════ #}
<div style="margin-top:28px;padding:20px;background:var(--surface);border:1px solid var(--border);border-radius:10px;display:flex;align-items:center;gap:16px;">
<span style="font-size:0.88rem;font-weight:600;">结束本轮:</span>
<form method="post" action="/positions/close" style="display:inline;">
<input type="hidden" name="result" value="profit_taken">
<button type="submit" class="btn" style="background:var(--success);color:#fff;" onclick="return confirm('确认止盈清仓?')">🟢 止盈</button>
</form>
<form method="post" action="/positions/close" style="display:inline;">
<input type="hidden" name="result" value="meltdown">
<button type="submit" class="btn" style="background:var(--danger);color:#fff;" onclick="return confirm('确认熔断清仓?')">🔴 熔断</button>
</form>
</div>
{% endif %}
<script>
var openPrice = document.getElementById('openPrice');
var ampThreshold = document.getElementById('ampThreshold');
var preview = document.getElementById('lockPricePreview');
function updatePreview() {
var op = parseInt(openPrice.value) || 0;
var at = parseInt(ampThreshold.value) || 0;
if (op > 0 && at > 0) {
preview.textContent = (op + at) + ' 点';
} else {
preview.textContent = '—';
}
}
if (openPrice && ampThreshold) {
openPrice.addEventListener('input', updatePreview);
ampThreshold.addEventListener('input', updatePreview);
}
</script>
{% endblock %}