品种增加每点价值字段,PNL计算支持不同品种差异化配置
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ class Product(Base):
|
||||
code: Mapped[str] = mapped_column(String(10), unique=True, index=True)
|
||||
name: Mapped[str] = mapped_column(String(20))
|
||||
exchange: Mapped[str] = mapped_column(String(10), default="CZCE")
|
||||
point_value: Mapped[int] = mapped_column(Integer, default=20)
|
||||
|
||||
contracts: Mapped[list["Contract"]] = relationship(
|
||||
back_populates="product", cascade="all, delete-orphan"
|
||||
@@ -137,12 +138,13 @@ class Trade(Base):
|
||||
close_price: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
close_fee: Mapped[float | None] = mapped_column(Float, nullable=True, default=0)
|
||||
status: Mapped[str] = mapped_column(String(10), default="open")
|
||||
point_value: Mapped[int] = mapped_column(Integer, default=20)
|
||||
|
||||
@property
|
||||
def pnl(self) -> float | None:
|
||||
if self.close_price is None:
|
||||
return None
|
||||
mul = 20 # glass futures point value
|
||||
mul = self.point_value
|
||||
if self.direction == "long":
|
||||
result = (self.close_price - self.open_price) * mul - (self.open_fee or 0) - (self.close_fee or 0)
|
||||
else:
|
||||
@@ -166,12 +168,13 @@ class OptionTrade(Base):
|
||||
close_price: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
close_fee: Mapped[float | None] = mapped_column(Float, nullable=True, default=0)
|
||||
status: Mapped[str] = mapped_column(String(10), default="open")
|
||||
point_value: Mapped[int] = mapped_column(Integer, default=20)
|
||||
|
||||
@property
|
||||
def pnl(self) -> float | None:
|
||||
if self.close_price is None:
|
||||
return None
|
||||
mul = 20 # glass futures point value
|
||||
mul = self.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:
|
||||
|
||||
@@ -48,6 +48,7 @@ def admin_page(request: Request, db: Session = Depends(get_db)):
|
||||
"code": p.code,
|
||||
"name": p.name,
|
||||
"exchange": p.exchange,
|
||||
"point_value": p.point_value,
|
||||
"contracts": contract_list,
|
||||
})
|
||||
|
||||
@@ -71,11 +72,12 @@ def create_product(
|
||||
code: str = Form(...),
|
||||
name: str = Form(...),
|
||||
exchange: str = Form(...),
|
||||
point_value: int = Form(20),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
existing = db.query(Product).filter(Product.code == code.upper()).first()
|
||||
if not existing:
|
||||
p = Product(code=code.upper(), name=name, exchange=exchange)
|
||||
p = Product(code=code.upper(), name=name, exchange=exchange, point_value=point_value)
|
||||
db.add(p)
|
||||
db.commit()
|
||||
return RedirectResponse("/admin/?tab=product", status_code=303)
|
||||
|
||||
@@ -67,7 +67,12 @@ def open_trade(
|
||||
):
|
||||
code = contract_code.upper()
|
||||
contract = db.query(Contract).filter(Contract.code == code).first()
|
||||
product_code = contract.product.code if contract else code[:2]
|
||||
if contract:
|
||||
product = contract.product
|
||||
else:
|
||||
product = db.query(Product).filter(Product.code == code[:2]).first()
|
||||
product_code = product.code if product else code[:2]
|
||||
point_value = product.point_value if product else 20
|
||||
|
||||
t = OptionTrade(
|
||||
product_code=product_code,
|
||||
@@ -78,6 +83,7 @@ def open_trade(
|
||||
open_date=date.fromisoformat(open_date),
|
||||
open_price=open_price,
|
||||
open_fee=open_fee,
|
||||
point_value=point_value,
|
||||
status="open",
|
||||
)
|
||||
db.add(t)
|
||||
@@ -124,7 +130,12 @@ def edit_trade(
|
||||
if t:
|
||||
code = contract_code.upper()
|
||||
contract = db.query(Contract).filter(Contract.code == code).first()
|
||||
t.product_code = contract.product.code if contract else code[:2]
|
||||
if contract:
|
||||
product = contract.product
|
||||
else:
|
||||
product = db.query(Product).filter(Product.code == code[:2]).first()
|
||||
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.option_type = option_type
|
||||
t.direction = direction
|
||||
|
||||
@@ -74,7 +74,12 @@ def open_trade(
|
||||
):
|
||||
code = contract_code.upper()
|
||||
contract = db.query(Contract).filter(Contract.code == code).first()
|
||||
product_code = contract.product.code if contract else code[:2]
|
||||
if contract:
|
||||
product = contract.product
|
||||
else:
|
||||
product = db.query(Product).filter(Product.code == code[:2]).first()
|
||||
product_code = product.code if product else code[:2]
|
||||
point_value = product.point_value if product else 20
|
||||
|
||||
t = Trade(
|
||||
product_code=product_code,
|
||||
@@ -83,6 +88,7 @@ def open_trade(
|
||||
open_date=date.fromisoformat(open_date),
|
||||
open_price=open_price,
|
||||
open_fee=open_fee,
|
||||
point_value=point_value,
|
||||
status="open",
|
||||
)
|
||||
db.add(t)
|
||||
@@ -127,7 +133,12 @@ def edit_trade(
|
||||
if t:
|
||||
code = contract_code.upper()
|
||||
contract = db.query(Contract).filter(Contract.code == code).first()
|
||||
t.product_code = contract.product.code if contract else code[:2]
|
||||
if contract:
|
||||
product = contract.product
|
||||
else:
|
||||
product = db.query(Product).filter(Product.code == code[:2]).first()
|
||||
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.direction = direction
|
||||
t.open_date = date.fromisoformat(open_date)
|
||||
|
||||
@@ -37,6 +37,22 @@ def _migrate(engine):
|
||||
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")
|
||||
|
||||
cur.execute("PRAGMA table_info(products)")
|
||||
cols = {row[1] for row in cur.fetchall()}
|
||||
if "point_value" not in cols:
|
||||
cur.execute("ALTER TABLE products ADD COLUMN point_value INTEGER DEFAULT 20")
|
||||
|
||||
cur.execute("PRAGMA table_info(trades)")
|
||||
cols = {row[1] for row in cur.fetchall()}
|
||||
if "point_value" not in cols:
|
||||
cur.execute("ALTER TABLE trades ADD COLUMN point_value INTEGER DEFAULT 20")
|
||||
|
||||
cur.execute("PRAGMA table_info(option_trades)")
|
||||
cols = {row[1] for row in cur.fetchall()}
|
||||
if "point_value" not in cols:
|
||||
cur.execute("ALTER TABLE option_trades ADD COLUMN point_value INTEGER DEFAULT 20")
|
||||
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
@@ -105,6 +105,10 @@
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:80px;">
|
||||
<label>每点价值</label>
|
||||
<input type="number" name="point_value" value="20" min="1" required style="font-size:0.9rem;">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">新建</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -112,12 +116,13 @@
|
||||
{% if products %}
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<tr><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 p in products %}
|
||||
<tr>
|
||||
<td><strong>{{ p.code }}</strong></td>
|
||||
<td>{{ p.name }}</td>
|
||||
<td>{{ p.exchange }}</td>
|
||||
<td>{{ p.point_value }}</td>
|
||||
<td>{{ p.contracts|length }}</td>
|
||||
<td>
|
||||
<form method="post" action="/admin/product/{{ p.id }}/delete" onsubmit="return confirm('删除品种 {{ p.code }} 及其所有合约?')" style="display:inline;">
|
||||
|
||||
@@ -198,7 +198,7 @@
|
||||
{% if p > 0 %}+{% endif %}{{ p }}
|
||||
</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>
|
||||
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 }}, {{ t.point_value }})" title="计算过程">ⓘ</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
@@ -333,16 +333,16 @@ function hideCloseForm() {
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function showPnlDrawer(title, direction, openPrice, closePrice, openFee, closeFee, result) {
|
||||
function showPnlDrawer(title, direction, openPrice, closePrice, openFee, closeFee, result, pointValue) {
|
||||
document.getElementById('pnlTitle').textContent = title;
|
||||
var dirLabel = direction === 'sell' ? '卖' : '买';
|
||||
document.getElementById('pnlFormula').textContent = '方向: ' + dirLabel + ' | 每点 20 元';
|
||||
document.getElementById('pnlFormula').textContent = '方向: ' + dirLabel + ' | 每点 ' + pointValue + ' 元';
|
||||
var diff = direction === 'sell' ? (openPrice - closePrice) : (closePrice - openPrice);
|
||||
var gross = diff * 20;
|
||||
var gross = diff * pointValue;
|
||||
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>' + diff.toFixed(2) + ' × ' + pointValue + '</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;
|
||||
|
||||
@@ -222,7 +222,7 @@
|
||||
{% if p > 0 %}+{% endif %}{{ p }}
|
||||
</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>
|
||||
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 }}, {{ t.point_value }})" title="计算过程">ⓘ</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
@@ -349,16 +349,16 @@ function hideCloseForm() {
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function showPnlDrawer(title, direction, openPrice, closePrice, openFee, closeFee, result) {
|
||||
function showPnlDrawer(title, direction, openPrice, closePrice, openFee, closeFee, result, pointValue) {
|
||||
document.getElementById('pnlTitle').textContent = title;
|
||||
var dirLabel = direction === 'short' ? '空' : '多';
|
||||
document.getElementById('pnlFormula').textContent = '方向: ' + dirLabel + ' | 每点 20 元';
|
||||
document.getElementById('pnlFormula').textContent = '方向: ' + dirLabel + ' | 每点 ' + pointValue + ' 元';
|
||||
var diff = direction === 'short' ? (openPrice - closePrice) : (closePrice - openPrice);
|
||||
var gross = diff * 20;
|
||||
var gross = diff * pointValue;
|
||||
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>' + diff.toFixed(2) + ' × ' + pointValue + '</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;
|
||||
|
||||
Reference in New Issue
Block a user