Compare commits

..

11 Commits

Author SHA1 Message Date
fish 05d0b7524f 更新数据库文件
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 17:39:13 +08:00
fish 8ed87a23e9 更新数据库文件
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 17:15:34 +08:00
fish c9a7dee7ff 持仓全部视图卡片标题恢复分割线
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 15:11:46 +08:00
fish 794ca61358 持仓全部视图卡片标题间距优化
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 15:11:07 +08:00
fish 11aff71415 更新数据库文件
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 15:09:09 +08:00
fish e1dc4595a2 持仓列表增加品种子tab切换
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 15:08:34 +08:00
fish fa763bd968 更新数据库文件
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 15:03:12 +08:00
fish 9bce381497 已平仓盈亏颜色改为红涨绿跌
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 15:01:25 +08:00
fish 227d8b515f 对冲管理轮次移除日期显示
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 14:59:33 +08:00
fish c40b4b919c 更新数据库文件
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 13:00:31 +08:00
fish 434acb0470 锁仓上限改为可配置,默认3次不变,止盈按钮左侧增加修改入口
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 12:57:34 +08:00
7 changed files with 111 additions and 19 deletions
+3 -3
View File
@@ -28,6 +28,6 @@ def check_lock(
return (current_price - open_price) >= lock_threshold return (current_price - open_price) >= lock_threshold
def should_meltdown(lock_count: int) -> bool: def should_meltdown(lock_count: int, max_locks: int = 3) -> bool:
"""3锁熔断""" """仓次数达到上限时熔断"""
return lock_count >= 3 return lock_count >= max_locks
+1
View File
@@ -95,6 +95,7 @@ class Round(Base):
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
contract_code: Mapped[str] = mapped_column(String(10), index=True) contract_code: Mapped[str] = mapped_column(String(10), index=True)
daily_hands: Mapped[int] = mapped_column(Integer, default=1) 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") status: Mapped[str] = mapped_column(String(20), default="active")
started_at: Mapped[date] = mapped_column(Date) started_at: Mapped[date] = mapped_column(Date)
+13
View File
@@ -139,6 +139,19 @@ def update_lock_price(
return RedirectResponse("/positions/", status_code=303) 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") @router.post("/{round_id}/close")
def close_round( def close_round(
request: Request, request: Request,
+16
View File
@@ -27,8 +27,24 @@ SEED_BARS: list[dict] = [
{"date": "2026-07-24", "open": 900, "close": 908, "high": 912, "low": 891}, {"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(): def seed():
Base.metadata.create_all(bind=engine) Base.metadata.create_all(bind=engine)
_migrate(engine)
db = SessionLocal() db = SessionLocal()
try: try:
+38 -7
View File
@@ -56,13 +56,13 @@
<div style="display:flex;align-items:center;gap:16px;padding:14px 20px;background:var(--th-bg);flex-wrap:wrap;"> <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> <strong style="font-size:1rem;">{{ round.contract_code }}</strong>
<span class="badge badge-up">{{ round.daily_hands }} 手/天</span> <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;font-weight:600;{% if locks >= round.max_locks %}color:var(--danger-fg);{% endif %}">
<span style="font-size:0.82rem;color:var(--sub);">开始 {{ round.started_at }}</span> 锁仓 {{ locks }}/{{ round.max_locks }}
<span style="font-size:0.82rem;font-weight:600;{% if locks >= 3 %}color:var(--danger-fg);{% endif %}"> {% if locks >= round.max_locks %} ⚠ 熔断{% endif %}
锁仓 {{ locks }}/3
{% if locks >= 3 %} ⚠ 熔断{% endif %}
</span> </span>
<span style="margin-left:auto;display:flex;gap:8px;"> <span style="margin-left:auto;display:flex;gap:8px;">
<button style="background:var(--surface);color:var(--fg);border:1px solid var(--border);padding:3px 12px;border-radius:4px;cursor:pointer;font-size:0.78rem;"
onclick="showMaxLocksModal({{ round.id }}, {{ round.max_locks }})">修改锁仓数</button>
<form method="post" action="/positions/{{ round.id }}/close" style="display:inline;"> <form method="post" action="/positions/{{ round.id }}/close" style="display:inline;">
<input type="hidden" name="result" value="profit_taken"> <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="confirmDelete(event, '确认 {{ round.contract_code }} 止盈清仓?', this.closest('form').action)">止盈</button> <button class="btn" style="background:var(--success);color:#fff;font-size:0.78rem;padding:4px 14px;" onclick="confirmDelete(event, '确认 {{ round.contract_code }} 止盈清仓?', this.closest('form').action)">止盈</button>
@@ -118,11 +118,10 @@
<div class="table-wrap" style="margin-bottom:0;"> <div class="table-wrap" style="margin-bottom:0;">
<table style="font-size:0.84rem;"> <table style="font-size:0.84rem;">
<tr> <tr>
<th>日期</th><th>开仓价</th><th>A</th><th>锁仓价</th><th>手数</th><th>已锁</th><th>状态</th><th>操作</th> <th>开仓价</th><th>A</th><th>锁仓价</th><th>手数</th><th>已锁</th><th>状态</th><th>操作</th>
</tr> </tr>
{% for p in round.positions|reverse %} {% for p in round.positions|reverse %}
<tr> <tr>
<td><strong>{{ p.opened_at }}</strong></td>
<td>{{ p.open_price }}</td> <td>{{ p.open_price }}</td>
<td>{{ p.amp_threshold }}</td> <td>{{ p.amp_threshold }}</td>
<td style="color:var(--danger-fg);font-weight:600;">{{ p.lock_price }}</td> <td style="color:var(--danger-fg);font-weight:600;">{{ p.lock_price }}</td>
@@ -180,6 +179,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) { function showLockConfirm(pid, lockPrice) {
document.getElementById('lockPid').value = pid; document.getElementById('lockPid').value = pid;
document.getElementById('lockPriceInput').value = lockPrice; document.getElementById('lockPriceInput').value = lockPrice;
@@ -200,6 +217,20 @@ function filterContracts() {
} }
</script> </script>
<div id="maxLocksModalOverlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:159;" onclick="hideMaxLocksModal()"></div>
<div id="maxLocksModal" style="display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:24px;z-index:160;width:300px;max-width:90vw;">
<h3 style="margin-bottom:16px;">修改锁仓上限</h3>
<input type="hidden" id="maxLocksRid">
<div class="form-group">
<label>锁仓次数上限(熔断阈值)</label>
<input type="number" id="maxLocksInput" min="1" max="10" required style="font-size:1rem;text-align:center;font-weight:600;">
</div>
<div style="display:flex;gap:10px;justify-content:flex-end;margin-top:8px;">
<button class="btn" style="background:var(--th-bg);color:var(--fg);" onclick="hideMaxLocksModal()">取消</button>
<button class="btn" style="background:var(--accent);color:#fff;" onclick="doUpdateMaxLocks()">确认修改</button>
</div>
</div>
<div id="lockModalOverlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:149;" onclick="hideLockModal()"></div> <div id="lockModalOverlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:149;" onclick="hideLockModal()"></div>
<div id="lockModal" style="display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:24px;z-index:150;width:300px;max-width:90vw;"> <div id="lockModal" style="display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:24px;z-index:150;width:300px;max-width:90vw;">
<h3 style="margin-bottom:16px;">确认锁仓</h3> <h3 style="margin-bottom:16px;">确认锁仓</h3>
+39 -8
View File
@@ -5,6 +5,12 @@
{% block content %} {% block content %}
{% set view = request.query_params.get('view', '') %} {% set view = request.query_params.get('view', '') %}
{% set product_tab = request.query_params.get('tab', '全部') %}
{% set product_codes = product_contracts.keys()|list %}
{% set grouped_open = {} %}
{% for t in open_trades %}
{% set _ = grouped_open.setdefault(t.product_code, []).append(t) %}
{% endfor %}
<style> <style>
.tabs { display: flex; gap: 0; margin-bottom: 24px; border-bottom: 2px solid var(--border); } .tabs { display: flex; gap: 0; margin-bottom: 24px; border-bottom: 2px solid var(--border); }
@@ -18,6 +24,14 @@
.tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; } .tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; }
.tab-panel { display: none; } .tab-panel { display: none; }
.tab-panel.active { display: block; } .tab-panel.active { display: block; }
.sub-tabs { display: flex; gap: 6px; margin-bottom: 16px; flex-wrap: wrap; }
.sub-tab-btn {
padding: 4px 14px; border: 1px solid var(--border); background: var(--surface);
font-size: 0.78rem; color: var(--sub); cursor: pointer; text-decoration: none;
border-radius: 4px; transition: color .12s, border-color .12s;
}
.sub-tab-btn:hover { color: var(--fg); border-color: var(--accent); }
.sub-tab-btn.active { color: var(--accent); border-color: var(--accent); font-weight: 600; background: var(--accent-light); }
</style> </style>
<div class="tabs"> <div class="tabs">
@@ -76,13 +90,27 @@
{# ── 持仓列表 ── #} {# ── 持仓列表 ── #}
<div class="section-title">持仓中 · {{ open_trades|length }} 笔</div> <div class="section-title">持仓中 · {{ open_trades|length }} 笔</div>
<div class="sub-tabs">
<a href="/trades/?tab=全部" class="sub-tab-btn{% if product_tab == '全部' %} active{% endif %}">全部</a>
{% for pc in product_codes %}
{% set count = grouped_open.get(pc, [])|length %}
<a href="/trades/?tab={{ pc }}" class="sub-tab-btn{% if product_tab == pc %} active{% endif %}">{{ pc }}{% if count %} · {{ count }}{% endif %}</a>
{% endfor %}
</div>
{% if open_trades %} {% if open_trades %}
<div class="table-wrap"> {% for pc in product_codes %}
{% if product_tab == '全部' or product_tab == pc %}
{% set trades = grouped_open.get(pc, []) %}
{% if trades %}
<div class="table-wrap" style="margin-bottom:24px;">
{% if product_tab == '全部' %}
<div class="section-title" style="font-size:0.88rem;margin-bottom:0;padding:6px 12px;">{{ pc }} · {{ trades|length }} 笔</div>
{% endif %}
<table> <table>
<tr><th>品种</th><th>合约</th><th>方向</th><th>开仓日期</th><th>开仓价</th><th>手续费</th><th>操作</th></tr> <tr><th>合约</th><th>方向</th><th>开仓日期</th><th>开仓价</th><th>手续费</th><th>操作</th></tr>
{% for t in open_trades %} {% for t in trades %}
<tr> <tr>
<td>{{ t.product_code }}</td>
<td><strong>{{ t.contract_code.replace(t.product_code, '', 1) }}</strong></td> <td><strong>{{ t.contract_code.replace(t.product_code, '', 1) }}</strong></td>
<td> <td>
{% if t.direction == 'short' %} {% if t.direction == 'short' %}
@@ -99,7 +127,7 @@
onclick="showCloseForm({{ t.id }})">平仓</button> onclick="showCloseForm({{ t.id }})">平仓</button>
<button style="background:var(--success-bg);color:var(--success-fg);border:1px solid var(--success);padding:3px 10px;border-radius:4px;cursor:pointer;font-size:0.78rem;margin-left:8px;" <button style="background:var(--success-bg);color:var(--success-fg);border:1px solid var(--success);padding:3px 10px;border-radius:4px;cursor:pointer;font-size:0.78rem;margin-left:8px;"
onclick="showEditForm('trade', {{ t.id }}, '{{ t.product_code }}', '{{ t.contract_code }}', '{{ t.direction }}', {{ t.open_price }}, {{ t.open_fee or 0 }}, '{{ t.open_date }}', null, null, null, '{{ t.status }}')">编辑</button> onclick="showEditForm('trade', {{ t.id }}, '{{ t.product_code }}', '{{ t.contract_code }}', '{{ t.direction }}', {{ t.open_price }}, {{ t.open_fee or 0 }}, '{{ t.open_date }}', null, null, null, '{{ t.status }}')">编辑</button>
<form method="post" action="/trades/{{ t.id }}/delete" style="display:inline;margin-left:8px;"> <form method="post" action="/trades/{{ t.id }}/delete" style="display:inline;margin-left:8px;">
<button style="background:transparent;color:#dc2626;border:1px solid #ef4444;padding:3px 10px;border-radius:4px;cursor:pointer;font-size:0.78rem;" onclick="confirmDelete(event, '确定删除此记录?', this.closest('form').action)">删除</button> <button style="background:transparent;color:#dc2626;border:1px solid #ef4444;padding:3px 10px;border-radius:4px;cursor:pointer;font-size:0.78rem;" onclick="confirmDelete(event, '确定删除此记录?', this.closest('form').action)">删除</button>
</form> </form>
</td> </td>
@@ -107,6 +135,9 @@
{% endfor %} {% endfor %}
</table> </table>
</div> </div>
{% endif %}
{% endif %}
{% endfor %}
{% else %} {% else %}
<div style="text-align:center;padding:40px;color:var(--sub);">暂无持仓</div> <div style="text-align:center;padding:40px;color:var(--sub);">暂无持仓</div>
{% endif %} {% endif %}
@@ -166,7 +197,7 @@
<td> <td>
{% set p = t.pnl %} {% set p = t.pnl %}
{% if p is not none %} {% if p is not none %}
<span style="font-weight:700;{% if p > 0 %}color:var(--success-fg);{% elif p < 0 %}color:var(--danger-fg);{% else %}color:var(--sub);{% endif %}"> <span style="font-weight:700;{% if p > 0 %}color:var(--danger-fg);{% elif p < 0 %}color:var(--success-fg);{% else %}color:var(--sub);{% endif %}">
{% if p > 0 %}+{% endif %}{{ p }} {% if p > 0 %}+{% endif %}{{ p }}
</span> </span>
<span style="color:var(--sub);cursor:pointer;font-size:0.75rem;margin-left:4px;" <span style="color:var(--sub);cursor:pointer;font-size:0.75rem;margin-left:4px;"
@@ -192,7 +223,7 @@
{% set ns.total_close_fee = ns.total_close_fee + (t.close_fee or 0) %} {% set ns.total_close_fee = ns.total_close_fee + (t.close_fee or 0) %}
{% endfor %} {% endfor %}
<div style="display:flex;gap:24px;margin-bottom:24px;padding:12px 18px;background:var(--surface);border:1px solid var(--border);border-radius:8px;font-size:0.88rem;flex-wrap:wrap;"> <div style="display:flex;gap:24px;margin-bottom:24px;padding:12px 18px;background:var(--surface);border:1px solid var(--border);border-radius:8px;font-size:0.88rem;flex-wrap:wrap;">
<span>盈亏合计 <b style="{% if ns.total_pnl > 0 %}color:var(--success-fg);{% elif ns.total_pnl < 0 %}color:var(--danger-fg);{% endif %}">{% if ns.total_pnl > 0 %}+{% endif %}{{ '%.2f'|format(ns.total_pnl) }}</b></span> <span>盈亏合计 <b style="{% if ns.total_pnl > 0 %}color:var(--danger-fg);{% elif ns.total_pnl < 0 %}color:var(--success-fg);{% endif %}">{% if ns.total_pnl > 0 %}+{% endif %}{{ '%.2f'|format(ns.total_pnl) }}</b></span>
<span style="color:var(--sub);">|</span> <span style="color:var(--sub);">|</span>
<span>开仓手续费 <b>{{ '%.2f'|format(ns.total_open_fee) }}</b></span> <span>开仓手续费 <b>{{ '%.2f'|format(ns.total_open_fee) }}</b></span>
<span style="color:var(--sub);">|</span> <span style="color:var(--sub);">|</span>
@@ -306,7 +337,7 @@ function showPnlDrawer(title, direction, openPrice, closePrice, openFee, closeFe
html += '<tr><td>开仓手续费</td><td></td><td style="text-align:right;color:var(--danger-fg);">-' + openFee.toFixed(2) + '</td></tr>'; html += '<tr><td>开仓手续费</td><td></td><td style="text-align:right;color:var(--danger-fg);">-' + openFee.toFixed(2) + '</td></tr>';
html += '<tr><td>平仓手续费</td><td></td><td style="text-align:right;color:var(--danger-fg);">-' + closeFee.toFixed(2) + '</td></tr>'; html += '<tr><td>平仓手续费</td><td></td><td style="text-align:right;color:var(--danger-fg);">-' + closeFee.toFixed(2) + '</td></tr>';
document.getElementById('pnlTable').innerHTML = html; document.getElementById('pnlTable').innerHTML = html;
document.getElementById('pnlResult').innerHTML = '<b>盈亏 = ' + gross.toFixed(2) + ' - ' + openFee.toFixed(2) + ' - ' + closeFee.toFixed(2) + ' = <span style="color:' + (result > 0 ? 'var(--success-fg)' : result < 0 ? 'var(--danger-fg)' : 'var(--sub)') + ';">' + (result > 0 ? '+' : '') + result.toFixed(2) + '</span></b>'; document.getElementById('pnlResult').innerHTML = '<b>盈亏 = ' + gross.toFixed(2) + ' - ' + openFee.toFixed(2) + ' - ' + closeFee.toFixed(2) + ' = <span style="color:' + (result > 0 ? 'var(--danger-fg)' : result < 0 ? 'var(--success-fg)' : 'var(--sub)') + ';">' + (result > 0 ? '+' : '') + result.toFixed(2) + '</span></b>';
document.getElementById('pnlOverlay').style.display = 'block'; document.getElementById('pnlOverlay').style.display = 'block';
document.getElementById('pnlBox').style.display = 'block'; document.getElementById('pnlBox').style.display = 'block';
} }
BIN
View File
Binary file not shown.