期权新增买卖方向和盈亏算法展示
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -156,6 +156,7 @@ class OptionTrade(Base):
|
|||||||
product_code: Mapped[str] = mapped_column(String(10))
|
product_code: Mapped[str] = mapped_column(String(10))
|
||||||
contract_code: Mapped[str] = mapped_column(String(10), index=True)
|
contract_code: Mapped[str] = mapped_column(String(10), index=True)
|
||||||
option_type: Mapped[str] = mapped_column(String(4))
|
option_type: Mapped[str] = mapped_column(String(4))
|
||||||
|
direction: Mapped[str] = mapped_column(String(4))
|
||||||
strike_price: Mapped[float] = mapped_column(Float)
|
strike_price: Mapped[float] = mapped_column(Float)
|
||||||
open_date: Mapped[date] = mapped_column(Date)
|
open_date: Mapped[date] = mapped_column(Date)
|
||||||
open_price: Mapped[float] = mapped_column(Float)
|
open_price: Mapped[float] = mapped_column(Float)
|
||||||
@@ -170,5 +171,8 @@ class OptionTrade(Base):
|
|||||||
if self.close_price is None:
|
if self.close_price is None:
|
||||||
return None
|
return None
|
||||||
mul = 20 # glass futures point value
|
mul = 20 # glass futures point value
|
||||||
|
if self.direction == "sell":
|
||||||
|
result = (self.open_price - self.close_price) * mul - (self.open_fee or 0) - (self.close_fee or 0)
|
||||||
|
else:
|
||||||
result = (self.close_price - self.open_price) * mul - (self.open_fee or 0) - (self.close_fee or 0)
|
result = (self.close_price - self.open_price) * mul - (self.open_fee or 0) - (self.close_fee or 0)
|
||||||
return round(result, 2)
|
return round(result, 2)
|
||||||
@@ -8,6 +8,7 @@ from app.models import OptionTrade, Contract, Product
|
|||||||
router = APIRouter(prefix="/options", tags=["options"])
|
router = APIRouter(prefix="/options", tags=["options"])
|
||||||
|
|
||||||
OPTION_TYPES = [("C", "C 看涨"), ("P", "P 看跌")]
|
OPTION_TYPES = [("C", "C 看涨"), ("P", "P 看跌")]
|
||||||
|
OPTION_DIRECTIONS = [("buy", "买"), ("sell", "卖")]
|
||||||
|
|
||||||
|
|
||||||
def get_product_contracts(db: Session) -> dict:
|
def get_product_contracts(db: Session) -> dict:
|
||||||
@@ -43,6 +44,7 @@ def option_page(request: Request, db: Session = Depends(get_db)):
|
|||||||
active_nav="options",
|
active_nav="options",
|
||||||
product_contracts=get_product_contracts(db),
|
product_contracts=get_product_contracts(db),
|
||||||
option_types=OPTION_TYPES,
|
option_types=OPTION_TYPES,
|
||||||
|
option_directions=OPTION_DIRECTIONS,
|
||||||
open_trades=open_trades,
|
open_trades=open_trades,
|
||||||
closed_trades=closed_trades,
|
closed_trades=closed_trades,
|
||||||
today=today_str(),
|
today=today_str(),
|
||||||
@@ -55,6 +57,7 @@ def open_trade(
|
|||||||
request: Request,
|
request: Request,
|
||||||
contract_code: str = Form(...),
|
contract_code: str = Form(...),
|
||||||
option_type: str = Form(...),
|
option_type: str = Form(...),
|
||||||
|
direction: str = Form(...),
|
||||||
strike_price: float = Form(...),
|
strike_price: float = Form(...),
|
||||||
open_date: str = Form(...),
|
open_date: str = Form(...),
|
||||||
open_price: float = Form(...),
|
open_price: float = Form(...),
|
||||||
@@ -69,6 +72,7 @@ def open_trade(
|
|||||||
product_code=product_code,
|
product_code=product_code,
|
||||||
contract_code=code,
|
contract_code=code,
|
||||||
option_type=option_type,
|
option_type=option_type,
|
||||||
|
direction=direction,
|
||||||
strike_price=strike_price,
|
strike_price=strike_price,
|
||||||
open_date=date.fromisoformat(open_date),
|
open_date=date.fromisoformat(open_date),
|
||||||
open_price=open_price,
|
open_price=open_price,
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
<option value="">-- 先选品种 --</option>
|
<option value="">-- 先选品种 --</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:80px;">
|
<div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:70px;">
|
||||||
<label>类型</label>
|
<label>类型</label>
|
||||||
<select name="option_type" required>
|
<select name="option_type" required>
|
||||||
{% for v, label in option_types %}
|
{% for v, label in option_types %}
|
||||||
@@ -57,6 +57,14 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:60px;">
|
||||||
|
<label>买卖</label>
|
||||||
|
<select name="direction" required>
|
||||||
|
{% for v, label in option_directions %}
|
||||||
|
<option value="{{ v }}">{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:90px;">
|
<div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:90px;">
|
||||||
<label>行权价</label>
|
<label>行权价</label>
|
||||||
<input type="number" step="any" name="strike_price" required placeholder="1300" style="font-size:0.9rem;">
|
<input type="number" step="any" name="strike_price" required placeholder="1300" style="font-size:0.9rem;">
|
||||||
@@ -84,16 +92,23 @@
|
|||||||
{% if open_trades %}
|
{% if open_trades %}
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table>
|
<table>
|
||||||
<tr><th>品种</th><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><th>开仓日期</th><th>手续费</th><th>操作</th></tr>
|
||||||
{% for t in open_trades %}
|
{% for t in open_trades %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ t.product_code }}</td>
|
<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.option_type == 'C' %}
|
{% if t.option_type == 'C' %}
|
||||||
<span class="badge badge-up">C 看涨</span>
|
<span class="badge badge-up">C</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge badge-down">P 看跌</span>
|
<span class="badge badge-down">P</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if t.direction == 'sell' %}
|
||||||
|
<span class="badge badge-down">卖</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge badge-up">买</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ t.strike_price }}</td>
|
<td>{{ t.strike_price }}</td>
|
||||||
@@ -148,16 +163,23 @@
|
|||||||
<div class="section-title">已平仓 · {{ closed_trades|length }} 笔</div>
|
<div class="section-title">已平仓 · {{ closed_trades|length }} 笔</div>
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table>
|
<table>
|
||||||
<tr><th>品种</th><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>类型</th><th>买卖</th><th>行权价</th><th>开仓</th><th>平仓</th><th>权利金</th><th>平仓价</th><th>开仓费</th><th>平仓费</th><th>盈亏</th><th>操作</th></tr>
|
||||||
{% for t in closed_trades %}
|
{% for t in closed_trades %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ t.product_code }}</td>
|
<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.option_type == 'C' %}
|
{% if t.option_type == 'C' %}
|
||||||
<span class="badge badge-up">C 看涨</span>
|
<span class="badge badge-up">C</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge badge-down">P 看跌</span>
|
<span class="badge badge-down">P</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if t.direction == 'sell' %}
|
||||||
|
<span class="badge badge-down">卖</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge badge-up">买</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ t.strike_price }}</td>
|
<td>{{ t.strike_price }}</td>
|
||||||
@@ -173,6 +195,8 @@
|
|||||||
<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(--success-fg);{% elif p < 0 %}color:var(--danger-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;"
|
||||||
|
onclick="showPnlDrawer('{{ t.product_code }} {{ t.contract_code.replace(t.product_code, '', 1) }} {{ t.option_type }}', '{{ t.direction }}', {{ t.open_price }}, {{ t.close_price }}, {{ t.open_fee or 0 }}, {{ t.close_fee or 0 }}, {{ p }})" title="计算过程">ⓘ</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -233,4 +257,37 @@ function hideCloseForm() {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<div id="pnlOverlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:99;" onclick="hidePnlDrawer()"></div>
|
||||||
|
<div id="pnlBox" 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:100;width:380px;max-width:90vw;">
|
||||||
|
<h3 style="margin-bottom:4px;" id="pnlTitle"></h3>
|
||||||
|
<p style="font-size:0.8rem;color:var(--sub);margin-bottom:14px;" id="pnlFormula"></p>
|
||||||
|
<table class="calc-table" style="width:100%;border-collapse:collapse;font-size:0.84rem;margin-bottom:12px;" id="pnlTable"></table>
|
||||||
|
<div style="font-size:1rem;" id="pnlResult"></div>
|
||||||
|
<button style="position:absolute;top:12px;right:16px;background:none;border:none;font-size:1.3rem;cursor:pointer;color:var(--sub);" onclick="hidePnlDrawer()">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function showPnlDrawer(title, direction, openPrice, closePrice, openFee, closeFee, result) {
|
||||||
|
document.getElementById('pnlTitle').textContent = title;
|
||||||
|
var dirLabel = direction === 'sell' ? '卖' : '买';
|
||||||
|
document.getElementById('pnlFormula').textContent = '方向: ' + dirLabel + ' | 每点 20 元';
|
||||||
|
var diff = direction === 'sell' ? (openPrice - closePrice) : (closePrice - openPrice);
|
||||||
|
var gross = diff * 20;
|
||||||
|
var fees = openFee + closeFee;
|
||||||
|
var html = '<tr style="background:var(--th-bg);"><td>项目</td><td>计算</td><td style="text-align:right;">金额</td></tr>';
|
||||||
|
html += '<tr><td>价差</td><td>' + (direction === 'sell' ? openPrice + ' - ' + closePrice : closePrice + ' - ' + openPrice) + '</td><td style="text-align:right;">' + diff.toFixed(2) + ' 点</td></tr>';
|
||||||
|
html += '<tr><td>毛利</td><td>' + diff.toFixed(2) + ' × 20</td><td style="text-align:right;">' + gross.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>';
|
||||||
|
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('pnlOverlay').style.display = 'block';
|
||||||
|
document.getElementById('pnlBox').style.display = 'block';
|
||||||
|
}
|
||||||
|
function hidePnlDrawer() {
|
||||||
|
document.getElementById('pnlOverlay').style.display = 'none';
|
||||||
|
document.getElementById('pnlBox').style.display = 'none';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -167,6 +167,8 @@
|
|||||||
<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(--success-fg);{% elif p < 0 %}color:var(--danger-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;"
|
||||||
|
onclick="showPnlDrawer('{{ t.product_code }} {{ t.contract_code.replace(t.product_code, '', 1) }}', '{{ t.direction }}', {{ t.open_price }}, {{ t.close_price }}, {{ t.open_fee or 0 }}, {{ t.close_fee or 0 }}, {{ p }})" title="计算过程">ⓘ</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -227,4 +229,37 @@ function hideCloseForm() {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<div id="pnlOverlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:99;" onclick="hidePnlDrawer()"></div>
|
||||||
|
<div id="pnlBox" 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:100;width:380px;max-width:90vw;">
|
||||||
|
<h3 style="margin-bottom:4px;" id="pnlTitle"></h3>
|
||||||
|
<p style="font-size:0.8rem;color:var(--sub);margin-bottom:14px;" id="pnlFormula"></p>
|
||||||
|
<table class="calc-table" style="width:100%;border-collapse:collapse;font-size:0.84rem;margin-bottom:12px;" id="pnlTable"></table>
|
||||||
|
<div style="font-size:1rem;" id="pnlResult"></div>
|
||||||
|
<button style="position:absolute;top:12px;right:16px;background:none;border:none;font-size:1.3rem;cursor:pointer;color:var(--sub);" onclick="hidePnlDrawer()">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function showPnlDrawer(title, direction, openPrice, closePrice, openFee, closeFee, result) {
|
||||||
|
document.getElementById('pnlTitle').textContent = title;
|
||||||
|
var dirLabel = direction === 'short' ? '空' : '多';
|
||||||
|
document.getElementById('pnlFormula').textContent = '方向: ' + dirLabel + ' | 每点 20 元';
|
||||||
|
var diff = direction === 'short' ? (openPrice - closePrice) : (closePrice - openPrice);
|
||||||
|
var gross = diff * 20;
|
||||||
|
var fees = openFee + closeFee;
|
||||||
|
var html = '<tr style="background:var(--th-bg);"><td>项目</td><td>计算</td><td style="text-align:right;">金额</td></tr>';
|
||||||
|
html += '<tr><td>价差</td><td>' + (direction === 'short' ? openPrice + ' - ' + closePrice : closePrice + ' - ' + openPrice) + '</td><td style="text-align:right;">' + diff.toFixed(2) + ' 点</td></tr>';
|
||||||
|
html += '<tr><td>毛利</td><td>' + diff.toFixed(2) + ' × 20</td><td style="text-align:right;">' + gross.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>';
|
||||||
|
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('pnlOverlay').style.display = 'block';
|
||||||
|
document.getElementById('pnlBox').style.display = 'block';
|
||||||
|
}
|
||||||
|
function hidePnlDrawer() {
|
||||||
|
document.getElementById('pnlOverlay').style.display = 'none';
|
||||||
|
document.getElementById('pnlBox').style.display = 'none';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user