品种增加每点价值字段,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)
|
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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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;">
|
||||||
|
|||||||
@@ -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="计算过程">ⓘ</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 %}
|
{% 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;
|
||||||
|
|||||||
@@ -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="计算过程">ⓘ</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 %}
|
{% 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;
|
||||||
|
|||||||
Reference in New Issue
Block a user