双买交易行权价拆分为看涨行权价和看跌行权价,支持不同价位

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
fish
2026-07-28 14:37:53 +08:00
parent af3e359d24
commit 39c5e22ed6
4 changed files with 53 additions and 28 deletions
+4 -3
View File
@@ -188,7 +188,8 @@ class DualOptionTrade(Base):
id: Mapped[int] = mapped_column(primary_key=True)
product_code: Mapped[str] = mapped_column(String(10))
contract_code: Mapped[str] = mapped_column(String(10), index=True)
strike_price: Mapped[float] = mapped_column(Float)
call_strike_price: Mapped[float] = mapped_column(Float)
put_strike_price: Mapped[float] = mapped_column(Float)
call_open_price: Mapped[float] = mapped_column(Float)
put_open_price: Mapped[float] = mapped_column(Float)
call_open_fee: Mapped[float | None] = mapped_column(Float, nullable=True, default=0)
@@ -225,8 +226,8 @@ class DualOptionTrade(Base):
@property
def breakeven_upper(self) -> float:
return self.strike_price + self.total_premium
return self.call_strike_price + self.total_premium
@property
def breakeven_lower(self) -> float:
return self.strike_price - self.total_premium
return self.put_strike_price - self.total_premium
+8 -4
View File
@@ -52,7 +52,8 @@ def dual_option_page(request: Request, db: Session = Depends(get_db)):
def open_trade(
request: Request,
contract_code: str = Form(...),
strike_price: float = Form(...),
call_strike_price: float = Form(...),
put_strike_price: float = Form(...),
call_open_price: float = Form(...),
put_open_price: float = Form(...),
call_open_fee: float = Form(0.0),
@@ -72,7 +73,8 @@ def open_trade(
t = DualOptionTrade(
product_code=product_code,
contract_code=code,
strike_price=strike_price,
call_strike_price=call_strike_price,
put_strike_price=put_strike_price,
call_open_price=call_open_price,
put_open_price=put_open_price,
call_open_fee=call_open_fee,
@@ -114,7 +116,8 @@ def edit_trade(
request: Request,
trade_id: int,
contract_code: str = Form(...),
strike_price: float = Form(...),
call_strike_price: float = Form(...),
put_strike_price: float = Form(...),
call_open_price: float = Form(...),
put_open_price: float = Form(...),
call_open_fee: float = Form(0.0),
@@ -138,7 +141,8 @@ def edit_trade(
t.product_code = product.code if product else code[:2]
t.point_value = product.point_value if product else 20
t.contract_code = code
t.strike_price = strike_price
t.call_strike_price = call_strike_price
t.put_strike_price = put_strike_price
t.call_open_price = call_open_price
t.put_open_price = put_open_price
t.call_open_fee = call_open_fee
+9
View File
@@ -53,6 +53,15 @@ def _migrate(engine):
if "point_value" not in cols:
cur.execute("ALTER TABLE option_trades ADD COLUMN point_value INTEGER DEFAULT 20")
cur.execute("PRAGMA table_info(dual_option_trades)")
cols = {row[1] for row in cur.fetchall()}
if "call_strike_price" not in cols:
cur.execute("ALTER TABLE dual_option_trades ADD COLUMN call_strike_price FLOAT DEFAULT 0")
if "put_strike_price" not in cols:
cur.execute("ALTER TABLE dual_option_trades ADD COLUMN put_strike_price FLOAT DEFAULT 0")
if "strike_price" in cols:
cur.execute("ALTER TABLE dual_option_trades DROP COLUMN strike_price")
conn.commit()
finally:
conn.close()
+32 -21
View File
@@ -48,9 +48,13 @@
<option value="">-- 先选品种 --</option>
</select>
</div>
<div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:90px;">
<label>行权价</label>
<input type="number" step="any" name="strike_price" required placeholder="1300" style="font-size:0.9rem;">
<div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:100px;">
<label>看涨行权价</label>
<input type="number" step="any" name="call_strike_price" required placeholder="1300" style="font-size:0.9rem;">
</div>
<div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:100px;">
<label>看跌行权价</label>
<input type="number" step="any" name="put_strike_price" required placeholder="1280" style="font-size:0.9rem;">
</div>
<div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:100px;">
<label>看涨开仓价</label>
@@ -83,12 +87,13 @@
{% if open_trades %}
<div class="table-wrap">
<table>
<tr><th>品种</th><th>合约</th><th>行权价</th><th>开仓日期</th><th>看涨开仓价</th><th>看跌开仓价</th><th>C手续费</th><th>P手续费</th><th>总成本</th><th>操作</th></tr>
<tr><th>品种</th><th>合约</th><th>C行权价</th><th>P行权价</th><th>开仓日期</th><th>看涨开仓价</th><th>看跌开仓价</th><th>C手续费</th><th>P手续费</th><th>总成本</th><th>操作</th></tr>
{% for t in open_trades %}
<tr>
<td>{{ t.product_code }}</td>
<td><strong>{{ t.contract_code.replace(t.product_code, '', 1) }}</strong></td>
<td>{{ t.strike_price }}</td>
<td>{{ t.call_strike_price }}</td>
<td>{{ t.put_strike_price }}</td>
<td>{{ t.open_date }} {{ weekdays[t.open_date.weekday()] }}</td>
<td>{{ t.call_open_price }}</td>
<td>{{ t.put_open_price }}</td>
@@ -99,7 +104,7 @@
<button style="background:var(--accent);color:#fff;border:none;padding:3px 10px;border-radius:4px;cursor:pointer;font-size:0.78rem;"
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;"
onclick="showEditForm({{ t.id }}, '{{ t.product_code }}', '{{ t.contract_code }}', {{ t.strike_price }}, {{ t.call_open_price }}, {{ t.put_open_price }}, {{ t.call_open_fee or 0 }}, {{ t.put_open_fee or 0 }}, '{{ t.open_date }}', null, null, null, null, null, 'open')">编辑</button>
onclick="showEditForm({{ t.id }}, '{{ t.product_code }}', '{{ t.contract_code }}', {{ t.call_strike_price }}, {{ t.put_strike_price }}, {{ t.call_open_price }}, {{ t.put_open_price }}, {{ t.call_open_fee or 0 }}, {{ t.put_open_fee or 0 }}, '{{ t.open_date }}', null, null, null, null, null, 'open')">编辑</button>
<form method="post" action="/dual-options/{{ 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>
</form>
@@ -153,12 +158,13 @@
<div class="section-title">已平仓 · {{ closed_trades|length }} 笔</div>
<div class="table-wrap">
<table>
<tr><th>品种</th><th>合约</th><th>行权价</th><th>开仓日</th><th>平仓日</th><th>持仓</th><th>看涨(开/平)</th><th>看跌(开/平)</th><th>总成本</th><th>盈亏</th><th>操作</th></tr>
<tr><th>品种</th><th>合约</th><th>C行权价</th><th>P行权价</th><th>开仓日</th><th>平仓日</th><th>持仓</th><th>看涨(开/平)</th><th>看跌(开/平)</th><th>总成本</th><th>盈亏</th><th>操作</th></tr>
{% for t in closed_trades %}
<tr>
<td>{{ t.product_code }}</td>
<td><strong>{{ t.contract_code.replace(t.product_code, '', 1) }}</strong></td>
<td>{{ t.strike_price }}</td>
<td>{{ t.call_strike_price }}</td>
<td>{{ t.put_strike_price }}</td>
<td style="font-size:0.8rem;">{{ t.open_date }} {{ weekdays[t.open_date.weekday()] }}</td>
<td style="font-size:0.8rem;">{{ t.close_date }} {{ weekdays[t.close_date.weekday()] }}</td>
<td>{{ (t.close_date - t.open_date).days }}天</td>
@@ -172,12 +178,12 @@
{% if p > 0 %}+{% endif %}{{ p }}
</span>
<span style="color:var(--sub);cursor:pointer;font-size:0.75rem;margin-left:4px;"
onclick="showDualPnlDrawer('{{ t.product_code }} {{ t.contract_code.replace(t.product_code, '', 1) }} {{ t.strike_price }}', {{ t.strike_price }}, {{ t.call_open_price }}, {{ t.put_open_price }}, {{ t.call_close_price }}, {{ t.put_close_price }}, {{ t.call_open_fee or 0 }}, {{ t.put_open_fee or 0 }}, {{ t.call_close_fee or 0 }}, {{ t.put_close_fee or 0 }}, {{ p }}, {{ t.point_value }})" title="盈亏分析">&#9432;</span>
onclick="showDualPnlDrawer('{{ t.product_code }} {{ t.contract_code.replace(t.product_code, '', 1) }}', {{ t.call_strike_price }}, {{ t.put_strike_price }}, {{ t.call_open_price }}, {{ t.put_open_price }}, {{ t.call_close_price }}, {{ t.put_close_price }}, {{ t.call_open_fee or 0 }}, {{ t.put_open_fee or 0 }}, {{ t.call_close_fee or 0 }}, {{ t.put_close_fee or 0 }}, {{ p }}, {{ t.point_value }})" title="盈亏分析">&#9432;</span>
{% endif %}
</td>
<td>
<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;"
onclick="showEditForm({{ t.id }}, '{{ t.product_code }}', '{{ t.contract_code }}', {{ t.strike_price }}, {{ t.call_open_price }}, {{ t.put_open_price }}, {{ t.call_open_fee or 0 }}, {{ t.put_open_fee or 0 }}, '{{ t.open_date }}', {{ t.call_close_price }}, {{ t.put_close_price }}, {{ t.call_close_fee or 0 }}, {{ t.put_close_fee or 0 }}, '{{ t.close_date }}', 'closed')">编辑</button>
onclick="showEditForm({{ t.id }}, '{{ t.product_code }}', '{{ t.contract_code }}', {{ t.call_strike_price }}, {{ t.put_strike_price }}, {{ t.call_open_price }}, {{ t.put_open_price }}, {{ t.call_open_fee or 0 }}, {{ t.put_open_fee or 0 }}, '{{ t.open_date }}', {{ t.call_close_price }}, {{ t.put_close_price }}, {{ t.call_close_fee or 0 }}, {{ t.put_close_fee or 0 }}, '{{ t.close_date }}', 'closed')">编辑</button>
<form method="post" action="/dual-options/{{ 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>
</form>
@@ -256,8 +262,12 @@ function hideCloseForm() {
<input type="text" name="contract_code" id="editContract" required style="font-size:0.9rem;">
</div>
<div class="form-group">
<label>行权价</label>
<input type="number" step="any" name="strike_price" id="editStrike" required style="font-size:0.9rem;">
<label>看涨行权价</label>
<input type="number" step="any" name="call_strike_price" id="editCallStrike" required style="font-size:0.9rem;">
</div>
<div class="form-group">
<label>看跌行权价</label>
<input type="number" step="any" name="put_strike_price" id="editPutStrike" required style="font-size:0.9rem;">
</div>
<div class="form-group">
<label>开仓日期</label>
@@ -319,9 +329,9 @@ function hideCloseForm() {
</div>
<script>
function showDualPnlDrawer(title, strikePrice, callOpen, putOpen, callClose, putClose, callOpenFee, putOpenFee, callCloseFee, putCloseFee, pnl, pointValue) {
function showDualPnlDrawer(title, callStrike, putStrike, callOpen, putOpen, callClose, putClose, callOpenFee, putOpenFee, callCloseFee, putCloseFee, pnl, pointValue) {
document.getElementById('pnlTitle').textContent = title + ' 双买分析';
document.getElementById('pnlFormula').textContent = '行权价: ' + strikePrice + ' | 每点 ' + pointValue + ' 元';
document.getElementById('pnlFormula').textContent = 'C行权价: ' + callStrike + ' P行权价: ' + putStrike + ' | 每点 ' + pointValue + ' 元';
var totalPremium = callOpen + putOpen;
var totalOpenFee = callOpenFee + putOpenFee;
@@ -329,8 +339,8 @@ function showDualPnlDrawer(title, strikePrice, callOpen, putOpen, callClose, put
var totalFee = totalOpenFee + totalCloseFee;
var premiumRmb = totalPremium * pointValue;
var totalCost = premiumRmb + totalOpenFee;
var beUpper = strikePrice + totalPremium;
var beLower = strikePrice - totalPremium;
var beUpper = callStrike + totalPremium;
var beLower = putStrike - totalPremium;
var callPnl = (callClose - callOpen) * pointValue - callOpenFee - callCloseFee;
var putPnl = (putClose - putOpen) * pointValue - putOpenFee - putCloseFee;
@@ -347,15 +357,15 @@ function showDualPnlDrawer(title, strikePrice, callOpen, putOpen, callClose, put
html += '<tr><td>开仓手续费</td><td>C:' + callOpenFee + ' + P:' + putOpenFee + '</td><td style="text-align:right;color:var(--danger-fg);">-' + totalOpenFee.toFixed(2) + '</td></tr>';
html += '<tr><td>平仓手续费</td><td>C:' + callCloseFee + ' + P:' + putCloseFee + '</td><td style="text-align:right;color:var(--danger-fg);">-' + totalCloseFee.toFixed(2) + '</td></tr>';
html += '<tr style="font-weight:600;"><td>总成本</td><td>' + premiumRmb.toFixed(2) + ' + ' + totalOpenFee.toFixed(2) + '</td><td style="text-align:right;">' + totalCost.toFixed(2) + '</td></tr>';
html += '<tr><td>盈亏平衡点(上)</td><td>' + strikePrice + ' + ' + totalPremium.toFixed(2) + '</td><td style="text-align:right;">' + beUpper.toFixed(2) + '</td></tr>';
html += '<tr><td>盈亏平衡点(下)</td><td>' + strikePrice + ' - ' + totalPremium.toFixed(2) + '</td><td style="text-align:right;">' + beLower.toFixed(2) + '</td></tr>';
html += '<tr><td>盈亏平衡点(上)</td><td>' + callStrike + ' + ' + totalPremium.toFixed(2) + '</td><td style="text-align:right;">' + beUpper.toFixed(2) + '</td></tr>';
html += '<tr><td>盈亏平衡点(下)</td><td>' + putStrike + ' - ' + totalPremium.toFixed(2) + '</td><td style="text-align:right;">' + beLower.toFixed(2) + '</td></tr>';
html += '<tr style="background:var(--th-bg);"><td colspan="3"></td></tr>';
html += '<tr><td>看涨盈亏</td><td>(' + callClose + ' - ' + callOpen + ') × ' + pointValue + ' - ' + callOpenFee + ' - ' + callCloseFee + '</td><td style="text-align:right;font-weight:600;' + colorFn(callPnl) + '">' + signed(callPnl) + '</td></tr>';
html += '<tr><td>看跌盈亏</td><td>(' + putClose + ' - ' + putOpen + ') × ' + pointValue + ' - ' + putOpenFee + ' - ' + putCloseFee + '</td><td style="text-align:right;font-weight:600;' + colorFn(putPnl) + '">' + signed(putPnl) + '</td></tr>';
document.getElementById('pnlTable').innerHTML = html;
var insight = '<b style="' + colorFn(pnl) + '">组合盈亏 = ' + signed(pnl) + '</b><br>';
insight += '最大亏损 = ' + totalCost.toFixed(2) + ' 元(标的价格 = ' + strikePrice + ' 时)<br>';
insight += '最大亏损 = ' + totalCost.toFixed(2) + ' 元(标的价格在 ' + putStrike + ' ~ ' + callStrike + ' 区间时)<br>';
insight += '盈利区间: 标的价格 &gt; ' + beUpper.toFixed(2) + ' 或 &lt; ' + beLower.toFixed(2);
document.getElementById('pnlInsight').innerHTML = insight;
@@ -363,10 +373,11 @@ function showDualPnlDrawer(title, strikePrice, callOpen, putOpen, callClose, put
document.getElementById('pnlBox').style.display = 'block';
}
function showEditForm(id, productCode, contractCode, strike, callOpen, putOpen, callOpenFee, putOpenFee, openDate, callClose, putClose, callCloseFee, putCloseFee, closeDate, status) {
function showEditForm(id, productCode, contractCode, callStrike, putStrike, callOpen, putOpen, callOpenFee, putOpenFee, openDate, callClose, putClose, callCloseFee, putCloseFee, closeDate, status) {
document.getElementById('editForm').action = '/dual-options/' + id + '/edit';
document.getElementById('editContract').value = contractCode;
document.getElementById('editStrike').value = strike;
document.getElementById('editCallStrike').value = callStrike;
document.getElementById('editPutStrike').value = putStrike;
document.getElementById('editOpenDate').value = openDate;
document.getElementById('editCallOpenPrice').value = callOpen;
document.getElementById('editPutOpenPrice').value = putOpen;