品种增加每点价值字段,PNL计算支持不同品种差异化配置

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
fish
2026-07-28 09:49:37 +08:00
parent 5018eb2083
commit 833491b8d0
8 changed files with 66 additions and 18 deletions
+5 -2
View File
@@ -13,6 +13,7 @@ class Product(Base):
code: Mapped[str] = mapped_column(String(10), unique=True, index=True) code: Mapped[str] = mapped_column(String(10), unique=True, index=True)
name: Mapped[str] = mapped_column(String(20)) name: Mapped[str] = mapped_column(String(20))
exchange: Mapped[str] = mapped_column(String(10), default="CZCE") exchange: Mapped[str] = mapped_column(String(10), default="CZCE")
point_value: Mapped[int] = mapped_column(Integer, default=20)
contracts: Mapped[list["Contract"]] = relationship( contracts: Mapped[list["Contract"]] = relationship(
back_populates="product", cascade="all, delete-orphan" 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_price: Mapped[float | None] = mapped_column(Float, nullable=True)
close_fee: Mapped[float | None] = mapped_column(Float, nullable=True, default=0) close_fee: Mapped[float | None] = mapped_column(Float, nullable=True, default=0)
status: Mapped[str] = mapped_column(String(10), default="open") status: Mapped[str] = mapped_column(String(10), default="open")
point_value: Mapped[int] = mapped_column(Integer, default=20)
@property @property
def pnl(self) -> float | None: def pnl(self) -> float | None:
if self.close_price is None: if self.close_price is None:
return None return None
mul = 20 # glass futures point value mul = self.point_value
if self.direction == "long": if self.direction == "long":
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)
else: else:
@@ -166,12 +168,13 @@ class OptionTrade(Base):
close_price: Mapped[float | None] = mapped_column(Float, nullable=True) close_price: Mapped[float | None] = mapped_column(Float, nullable=True)
close_fee: Mapped[float | None] = mapped_column(Float, nullable=True, default=0) close_fee: Mapped[float | None] = mapped_column(Float, nullable=True, default=0)
status: Mapped[str] = mapped_column(String(10), default="open") status: Mapped[str] = mapped_column(String(10), default="open")
point_value: Mapped[int] = mapped_column(Integer, default=20)
@property @property
def pnl(self) -> float | None: def pnl(self) -> float | None:
if self.close_price is None: if self.close_price is None:
return None return None
mul = 20 # glass futures point value mul = self.point_value
if self.direction == "sell": if self.direction == "sell":
result = (self.open_price - self.close_price) * mul - (self.open_fee or 0) - (self.close_fee or 0) result = (self.open_price - self.close_price) * mul - (self.open_fee or 0) - (self.close_fee or 0)
else: else:
+3 -1
View File
@@ -48,6 +48,7 @@ def admin_page(request: Request, db: Session = Depends(get_db)):
"code": p.code, "code": p.code,
"name": p.name, "name": p.name,
"exchange": p.exchange, "exchange": p.exchange,
"point_value": p.point_value,
"contracts": contract_list, "contracts": contract_list,
}) })
@@ -71,11 +72,12 @@ def create_product(
code: str = Form(...), code: str = Form(...),
name: str = Form(...), name: str = Form(...),
exchange: str = Form(...), exchange: str = Form(...),
point_value: int = Form(20),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
existing = db.query(Product).filter(Product.code == code.upper()).first() existing = db.query(Product).filter(Product.code == code.upper()).first()
if not existing: 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.add(p)
db.commit() db.commit()
return RedirectResponse("/admin/?tab=product", status_code=303) return RedirectResponse("/admin/?tab=product", status_code=303)
+13 -2
View File
@@ -67,7 +67,12 @@ def open_trade(
): ):
code = contract_code.upper() code = contract_code.upper()
contract = db.query(Contract).filter(Contract.code == code).first() 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( t = OptionTrade(
product_code=product_code, product_code=product_code,
@@ -78,6 +83,7 @@ def open_trade(
open_date=date.fromisoformat(open_date), open_date=date.fromisoformat(open_date),
open_price=open_price, open_price=open_price,
open_fee=open_fee, open_fee=open_fee,
point_value=point_value,
status="open", status="open",
) )
db.add(t) db.add(t)
@@ -124,7 +130,12 @@ def edit_trade(
if t: if t:
code = contract_code.upper() code = contract_code.upper()
contract = db.query(Contract).filter(Contract.code == code).first() 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.contract_code = code
t.option_type = option_type t.option_type = option_type
t.direction = direction t.direction = direction
+13 -2
View File
@@ -74,7 +74,12 @@ def open_trade(
): ):
code = contract_code.upper() code = contract_code.upper()
contract = db.query(Contract).filter(Contract.code == code).first() 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( t = Trade(
product_code=product_code, product_code=product_code,
@@ -83,6 +88,7 @@ def open_trade(
open_date=date.fromisoformat(open_date), open_date=date.fromisoformat(open_date),
open_price=open_price, open_price=open_price,
open_fee=open_fee, open_fee=open_fee,
point_value=point_value,
status="open", status="open",
) )
db.add(t) db.add(t)
@@ -127,7 +133,12 @@ def edit_trade(
if t: if t:
code = contract_code.upper() code = contract_code.upper()
contract = db.query(Contract).filter(Contract.code == code).first() 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.contract_code = code
t.direction = direction t.direction = direction
t.open_date = date.fromisoformat(open_date) t.open_date = date.fromisoformat(open_date)
+16
View File
@@ -37,6 +37,22 @@ def _migrate(engine):
cols = {row[1] for row in cur.fetchall()} cols = {row[1] for row in cur.fetchall()}
if "max_locks" not in cols: if "max_locks" not in cols:
cur.execute("ALTER TABLE rounds ADD COLUMN max_locks INTEGER DEFAULT 3") 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() conn.commit()
finally: finally:
conn.close() conn.close()
+6 -1
View File
@@ -105,6 +105,10 @@
{% endfor %} {% endfor %}
</select> </select>
</div> </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> <button type="submit" class="btn btn-primary">新建</button>
</form> </form>
</div> </div>
@@ -112,12 +116,13 @@
{% if products %} {% if products %}
<div class="table-wrap"> <div class="table-wrap">
<table> <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 %} {% for p in products %}
<tr> <tr>
<td><strong>{{ p.code }}</strong></td> <td><strong>{{ p.code }}</strong></td>
<td>{{ p.name }}</td> <td>{{ p.name }}</td>
<td>{{ p.exchange }}</td> <td>{{ p.exchange }}</td>
<td>{{ p.point_value }}</td>
<td>{{ p.contracts|length }}</td> <td>{{ p.contracts|length }}</td>
<td> <td>
<form method="post" action="/admin/product/{{ p.id }}/delete" onsubmit="return confirm('删除品种 {{ p.code }} 及其所有合约?')" style="display:inline;"> <form method="post" action="/admin/product/{{ p.id }}/delete" onsubmit="return confirm('删除品种 {{ p.code }} 及其所有合约?')" style="display:inline;">
+5 -5
View File
@@ -198,7 +198,7 @@
{% 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;"
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="计算过程">&#9432;</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="计算过程">&#9432;</span>
{% endif %} {% endif %}
</td> </td>
<td> <td>
@@ -333,16 +333,16 @@ function hideCloseForm() {
</div> </div>
<script> <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; document.getElementById('pnlTitle').textContent = title;
var dirLabel = direction === 'sell' ? '卖' : '买'; 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 diff = direction === 'sell' ? (openPrice - closePrice) : (closePrice - openPrice);
var gross = diff * 20; var gross = diff * pointValue;
var fees = openFee + closeFee; var fees = openFee + closeFee;
var html = '<tr style="background:var(--th-bg);"><td>项目</td><td>计算</td><td style="text-align:right;">金额</td></tr>'; 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>' + (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);">-' + 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;
+5 -5
View File
@@ -222,7 +222,7 @@
{% 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;"
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="计算过程">&#9432;</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="计算过程">&#9432;</span>
{% endif %} {% endif %}
</td> </td>
<td> <td>
@@ -349,16 +349,16 @@ function hideCloseForm() {
</div> </div>
<script> <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; document.getElementById('pnlTitle').textContent = title;
var dirLabel = direction === 'short' ? '空' : '多'; 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 diff = direction === 'short' ? (openPrice - closePrice) : (closePrice - openPrice);
var gross = diff * 20; var gross = diff * pointValue;
var fees = openFee + closeFee; var fees = openFee + closeFee;
var html = '<tr style="background:var(--th-bg);"><td>项目</td><td>计算</td><td style="text-align:right;">金额</td></tr>'; 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>' + (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);">-' + 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;