Defer P&L until after actual purchase date
This commit is contained in:
@@ -4,19 +4,21 @@ from __future__ import annotations
|
||||
|
||||
import csv
|
||||
from contextlib import closing
|
||||
from datetime import datetime, timezone
|
||||
from datetime import date, datetime, time
|
||||
import math
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sqlite3
|
||||
import zipfile
|
||||
from typing import Any
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
|
||||
DEFAULT_BOOK = Path("data/portfolio/prime_thai_portfolio.sqlite3")
|
||||
MAX_CASH_AMOUNT = 1_000_000_000_000.0
|
||||
_EOD_FILE = re.compile(r"set-history_EOD_(\d{4}-\d{2}-\d{2})\.csv$", re.IGNORECASE)
|
||||
_SYMBOL = re.compile(r"[A-Z0-9]{1,12}$")
|
||||
THAILAND_TZ = ZoneInfo("Asia/Bangkok")
|
||||
|
||||
|
||||
def _connect(database: str | Path = DEFAULT_BOOK) -> sqlite3.Connection:
|
||||
@@ -130,6 +132,29 @@ def _read_ledger(connection: sqlite3.Connection) -> tuple[list[sqlite3.Row], lis
|
||||
return cash_events, trades
|
||||
|
||||
|
||||
def _trade_date(value: object) -> str:
|
||||
"""Return a ledger timestamp's calendar date in the Thai market timezone."""
|
||||
text = str(value or "").strip()
|
||||
try:
|
||||
parsed = datetime.fromisoformat(text)
|
||||
except ValueError:
|
||||
try:
|
||||
return date.fromisoformat(text[:10]).isoformat()
|
||||
except ValueError:
|
||||
return text[:10]
|
||||
if parsed.tzinfo is not None:
|
||||
parsed = parsed.astimezone(THAILAND_TZ)
|
||||
return parsed.date().isoformat()
|
||||
|
||||
|
||||
def _normalize_trade_date(value: object) -> str:
|
||||
text = str(value or "").strip()
|
||||
try:
|
||||
return date.fromisoformat(text).isoformat()
|
||||
except ValueError as exc:
|
||||
raise ValueError("วันที่ซื้อจริงต้องเป็นรูปแบบ YYYY-MM-DD") from exc
|
||||
|
||||
|
||||
def _calculate_book(cash_events: list[sqlite3.Row], trades: list[sqlite3.Row]) -> dict[str, Any]:
|
||||
cash = 0.0
|
||||
net_contributed = 0.0
|
||||
@@ -146,12 +171,17 @@ def _calculate_book(cash_events: list[sqlite3.Row], trades: list[sqlite3.Row]) -
|
||||
elif event["kind"] == "dividend":
|
||||
cash += amount
|
||||
|
||||
positions: dict[str, dict[str, float | int]] = {}
|
||||
positions: dict[str, dict[str, Any]] = {}
|
||||
for trade in trades:
|
||||
if "is_void" in trade.keys() and bool(trade["is_void"]):
|
||||
continue
|
||||
symbol = str(trade["symbol"])
|
||||
position = positions.setdefault(symbol, {"quantity": 0, "cost_basis": 0.0, "realized_pnl": 0.0})
|
||||
position = positions.setdefault(symbol, {
|
||||
"quantity": 0,
|
||||
"cost_basis": 0.0,
|
||||
"realized_pnl": 0.0,
|
||||
"last_buy_date": None,
|
||||
})
|
||||
quantity = int(trade["quantity"])
|
||||
price = float(trade["price"])
|
||||
fees = float(trade["fees"])
|
||||
@@ -160,6 +190,9 @@ def _calculate_book(cash_events: list[sqlite3.Row], trades: list[sqlite3.Row]) -
|
||||
cash -= gross + fees
|
||||
position["quantity"] = int(position["quantity"]) + quantity
|
||||
position["cost_basis"] = float(position["cost_basis"]) + gross + fees
|
||||
buy_date = _trade_date(trade["occurred_at"])
|
||||
if not position["last_buy_date"] or buy_date > str(position["last_buy_date"]):
|
||||
position["last_buy_date"] = buy_date
|
||||
else:
|
||||
held = int(position["quantity"])
|
||||
average_cost = float(position["cost_basis"]) / held if held else 0.0
|
||||
@@ -167,6 +200,8 @@ def _calculate_book(cash_events: list[sqlite3.Row], trades: list[sqlite3.Row]) -
|
||||
position["realized_pnl"] = float(position["realized_pnl"]) + gross - fees - average_cost * quantity
|
||||
position["cost_basis"] = max(0.0, float(position["cost_basis"]) - average_cost * quantity)
|
||||
position["quantity"] = held - quantity
|
||||
if int(position["quantity"]) <= 0:
|
||||
position["last_buy_date"] = None
|
||||
positions = {symbol: position for symbol, position in positions.items()
|
||||
if int(position["quantity"]) > 0}
|
||||
return {"cash": cash, "net_contributed": net_contributed,
|
||||
@@ -228,6 +263,7 @@ def record_trade(symbol: str, side: str, quantity: int, price: float, fees: floa
|
||||
|
||||
|
||||
def update_trade(trade_id: int, quantity: int, price: float, fees: float = 0.0, *,
|
||||
trade_date: str | None = None,
|
||||
database: str | Path = DEFAULT_BOOK) -> None:
|
||||
"""Edit a previously logged signal trade to match the actual fill."""
|
||||
if isinstance(quantity, bool) or int(quantity) != quantity or int(quantity) <= 0:
|
||||
@@ -236,6 +272,8 @@ def update_trade(trade_id: int, quantity: int, price: float, fees: float = 0.0,
|
||||
price, fees = float(price), float(fees)
|
||||
if not math.isfinite(price) or price <= 0 or not math.isfinite(fees) or fees < 0:
|
||||
raise ValueError("ราคาต้องมากกว่า 0 และค่าธรรมเนียมต้องไม่ติดลบ")
|
||||
normalized_trade_date = (_normalize_trade_date(trade_date)
|
||||
if trade_date is not None and str(trade_date).strip() else None)
|
||||
|
||||
with closing(_connect(database)) as connection:
|
||||
connection.execute("BEGIN IMMEDIATE")
|
||||
@@ -257,10 +295,19 @@ def update_trade(trade_id: int, quantity: int, price: float, fees: float = 0.0,
|
||||
resulting_cash = float(book["cash"]) - old_quantity * old_price + old_fees + quantity * price - fees
|
||||
if resulting_cash < -0.005:
|
||||
raise ValueError(f"แก้รายการแล้วเงินสดจะติดลบ {abs(resulting_cash):,.2f} บาท")
|
||||
connection.execute(
|
||||
"UPDATE trades SET quantity=?, price=?, fees=?, is_estimated=0 WHERE id=?",
|
||||
(quantity, price, fees, int(trade_id)),
|
||||
)
|
||||
if normalized_trade_date is None:
|
||||
connection.execute(
|
||||
"UPDATE trades SET quantity=?, price=?, fees=?, is_estimated=0 WHERE id=?",
|
||||
(quantity, price, fees, int(trade_id)),
|
||||
)
|
||||
else:
|
||||
occurred_at = datetime.combine(
|
||||
date.fromisoformat(normalized_trade_date), time(12, 0), tzinfo=THAILAND_TZ,
|
||||
).isoformat()
|
||||
connection.execute(
|
||||
"UPDATE trades SET quantity=?, price=?, fees=?, occurred_at=?, is_estimated=0 WHERE id=?",
|
||||
(quantity, price, fees, occurred_at, int(trade_id)),
|
||||
)
|
||||
connection.execute("COMMIT")
|
||||
except Exception:
|
||||
connection.execute("ROLLBACK")
|
||||
@@ -328,7 +375,7 @@ def portfolio_view(signals: list[dict[str, str]], *, board_lot: int = 100,
|
||||
"allocation_score": score,
|
||||
}
|
||||
|
||||
positions: dict[str, dict[str, float | int]] = book["positions"]
|
||||
positions: dict[str, dict[str, Any]] = book["positions"]
|
||||
missing_prices = sorted(symbol for symbol in positions if symbol not in prices)
|
||||
signal_as_of = signal_as_of or max(
|
||||
(str(row.get("signal_as_of", "")) for row in signals), default=""
|
||||
@@ -339,6 +386,13 @@ def portfolio_view(signals: list[dict[str, str]], *, board_lot: int = 100,
|
||||
if symbol in prices and signal_as_of and prices[symbol]["as_of"] < signal_as_of)
|
||||
incomplete_symbols = sorted(set(missing_prices) | set(stale_prices))
|
||||
complete = not incomplete_symbols and not signal_quote_mismatch
|
||||
# An EOD close from the purchase date can precede the user's actual fill.
|
||||
# Keep it as a reference price, but start unrealized P&L on the next date.
|
||||
valuation_pending_symbols = sorted(
|
||||
symbol for symbol, position in positions.items()
|
||||
if symbol in prices and position.get("last_buy_date")
|
||||
and prices[symbol]["as_of"] <= str(position["last_buy_date"])
|
||||
)
|
||||
total_market_value = 0.0
|
||||
for symbol, position in positions.items():
|
||||
if symbol in prices:
|
||||
@@ -370,7 +424,9 @@ def portfolio_view(signals: list[dict[str, str]], *, board_lot: int = 100,
|
||||
delta = (int(target_quantity) - quantity) if target_quantity is not None else None
|
||||
market_value = quantity * close if close is not None else None
|
||||
average_cost = cost_basis / quantity if quantity else None
|
||||
unrealized = market_value - cost_basis if market_value is not None else None
|
||||
unrealized = (market_value - cost_basis
|
||||
if market_value is not None and symbol not in valuation_pending_symbols
|
||||
else None)
|
||||
if mark is None or symbol in stale_prices:
|
||||
action = "PRICE NEEDED" if quantity else "WAIT"
|
||||
elif not complete and account_exists:
|
||||
@@ -491,11 +547,14 @@ def portfolio_view(signals: list[dict[str, str]], *, board_lot: int = 100,
|
||||
"net_contributed": net_contributed,
|
||||
"market_value": total_market_value,
|
||||
"equity": equity,
|
||||
"profit_loss": equity - net_contributed if complete else None,
|
||||
"profit_loss_pct": (equity / net_contributed - 1.0) if complete and net_contributed > 0 else None,
|
||||
"profit_loss": (equity - net_contributed
|
||||
if complete and not valuation_pending_symbols else None),
|
||||
"profit_loss_pct": (equity / net_contributed - 1.0)
|
||||
if complete and not valuation_pending_symbols and net_contributed > 0 else None,
|
||||
"complete": complete,
|
||||
"unpriced_symbols": missing_prices,
|
||||
"stale_price_symbols": stale_prices,
|
||||
"valuation_pending_symbols": valuation_pending_symbols,
|
||||
"signal_quote_mismatch": signal_quote_mismatch,
|
||||
"price_as_of": quote_as_of or None,
|
||||
"buy_value": buy_value,
|
||||
|
||||
@@ -132,9 +132,10 @@
|
||||
<tbody id="ledger"><tr><td colspan="7" class="empty">ยังไม่มีรายการ</td></tr></tbody>
|
||||
</table></div>
|
||||
<form id="trade-edit-form" class="edit-panel" hidden>
|
||||
<div class="panel-head"><div><h2>แก้รายการในสมุดพอร์ต</h2><p class="sub">ใส่จำนวนและราคาที่ได้จริง ระบบจะคำนวณเงินสดกับจำนวนหุ้นใหม่</p></div></div>
|
||||
<div class="panel-head"><div><h2>แก้รายการในสมุดพอร์ต</h2><p class="sub">ใส่วันที่ซื้อ จำนวน ราคา และค่าธรรมเนียมที่ได้จริง ระบบจะใช้ราคานี้เป็นต้นทุน และเริ่มคิดกำไรจากวันถัดไป</p></div></div>
|
||||
<input id="edit-trade-id" type="hidden">
|
||||
<div class="form-grid">
|
||||
<label>วันที่ซื้อจริง<input id="edit-trade-date" type="date" required></label>
|
||||
<label>จำนวนหุ้นจริง<input id="edit-trade-quantity" type="number" min="1" step="1" required></label>
|
||||
<label>ราคาที่ได้จริง (บาท)<input id="edit-trade-price" type="number" min="0.01" step="0.01" required></label>
|
||||
<label>ค่าธรรมเนียมรวม (บาท)<input id="edit-trade-fees" type="number" min="0" step="0.01" value="0"></label>
|
||||
@@ -169,6 +170,12 @@
|
||||
const number = value => Number(value||0).toLocaleString('th-TH');
|
||||
const price = value => value == null ? '—' : Number(value).toLocaleString('th-TH',{minimumFractionDigits:2,maximumFractionDigits:4});
|
||||
const dateText = value => value ? new Date(value+'T00:00:00').toLocaleDateString('th-TH',{day:'numeric',month:'short',year:'numeric'}) : '—';
|
||||
const localDateInput = value => {
|
||||
if(!value)return '';
|
||||
const parts=new Intl.DateTimeFormat('en-US',{timeZone:'Asia/Bangkok',year:'numeric',month:'2-digit',day:'2-digit'}).formatToParts(new Date(value));
|
||||
const get=type=>parts.find(part=>part.type===type)?.value||'';
|
||||
return `${get('year')}-${get('month')}-${get('day')}`;
|
||||
};
|
||||
const actionText = value => ({BUY:'ซื้อเพิ่ม',SELL:'ลด/ขาย',HOLD:'ถือ',WAIT:'รอเงินลงทุน',REVIEW:'ตรวจราคา', 'PRICE NEEDED':'ขาดราคา','BELOW LOT':'ไม่ถึง 1 lot',BUDGET_LIMIT:'งบไม่พอ'})[value] || value;
|
||||
const setMessage = (id,text,isError=false) => { $(id).textContent=text; $(id).className='help '+(isError?'negative':''); };
|
||||
|
||||
@@ -214,7 +221,7 @@
|
||||
if(row.id && !row.is_void){
|
||||
const group=document.createElement('div');group.className='actions';
|
||||
const edit=document.createElement('button');edit.className='mini';edit.textContent='แก้';edit.addEventListener('click',()=>{
|
||||
$('edit-trade-id').value=row.id;$('edit-trade-quantity').value=row.quantity;$('edit-trade-price').value=row.price;$('edit-trade-fees').value=row.fees||0;
|
||||
$('edit-trade-id').value=row.id;$('edit-trade-date').value=localDateInput(row.occurred_at);$('edit-trade-quantity').value=row.quantity;$('edit-trade-price').value=row.price;$('edit-trade-fees').value=row.fees||0;
|
||||
$('edit-trade-message').textContent=`รายการ ${row.kind==='BUY'?'ซื้อ':'ขาย'} ${row.symbol} #${row.id}`;$('trade-edit-form').hidden=false;$('trade-edit-form').scrollIntoView({behavior:'smooth',block:'center'});
|
||||
});group.append(edit);
|
||||
if(row.is_estimated){const cancel=document.createElement('button');cancel.className='mini';cancel.textContent='ยกเลิก';cancel.addEventListener('click',async()=>{
|
||||
@@ -259,7 +266,7 @@
|
||||
renderRefreshState(d.refresh,d);
|
||||
$('equity').textContent=money(d.equity);$('cash').textContent=money(d.cash);$('contributed').textContent=money(d.net_contributed);
|
||||
$('asof').textContent='ราคาปิดถึง '+dateText(d.price_as_of)+' · สัญญาณ '+dateText(d.signal_as_of);
|
||||
if(d.profit_loss==null){$('pnl').textContent='ยังประเมินไม่ได้';$('pnl').className='value warning';$('pnl-pct').textContent=d.unpriced_symbols?.length?'ราคาหุ้นที่ถือไม่ครบ: '+d.unpriced_symbols.join(', '):'เพิ่มเงินลงทุนและมีราคาครบก่อน';}
|
||||
if(d.profit_loss==null){$('pnl').textContent='ยังประเมินไม่ได้';$('pnl').className='value warning';$('pnl-pct').textContent=d.valuation_pending_symbols?.length?'รอราคาปิดหลังวันที่ซื้อ: '+d.valuation_pending_symbols.join(', '):d.unpriced_symbols?.length?'ราคาหุ้นที่ถือไม่ครบ: '+d.unpriced_symbols.join(', '):'เพิ่มเงินลงทุนและมีราคาครบก่อน';}
|
||||
else {$('pnl').textContent=money(d.profit_loss);$('pnl').className='value '+(d.profit_loss>=0?'positive':'negative');$('pnl-pct').textContent=d.profit_loss_pct==null?'':(d.profit_loss_pct>=0?'+':'')+(d.profit_loss_pct*100).toFixed(2)+'% จากเงินลงทุนสุทธิ';}
|
||||
$('book-state').textContent=d.account_exists?'เริ่มพอร์ตแล้ว · เพิ่มเงิน '+number(d.deposit_count)+' ครั้ง':'ยังไม่มีพอร์ต · เพิ่มเงินครั้งแรกเพื่อเริ่ม';
|
||||
$('deposit-submit').textContent=d.account_exists?'เพิ่มเงินลงทุน':'ลงทุนครั้งแรก';
|
||||
@@ -268,6 +275,7 @@
|
||||
if(!d.account_exists)notes.push('เพิ่มเงินลงทุนครั้งแรกเพื่อคำนวณหุ้นและจำนวนเป้าหมาย');
|
||||
if(d.rows?.some(row=>row.action==='BELOW LOT'))notes.push('บางเป้าหมายมีมูลค่าต่ำกว่าหุ้น 100 หุ้น จึงคงส่วนนั้นไว้เป็นเงินสด');
|
||||
if(d.capital_excluded_count>0)notes.push(`งบปัจจุบันเลือก ${number(d.capital_selected_count)} หุ้นจาก ${number(d.capital_selected_count+d.capital_excluded_count)} หุ้นที่ผ่านสัญญาณ; รายการที่เหลือขึ้น “งบไม่พอ”`);
|
||||
if(d.valuation_pending_symbols?.length)notes.push('รายการที่ซื้อวันนี้ใช้ราคาที่แก้เป็นต้นทุนจริงแล้ว และจะเริ่มคิดกำไรจากราคาปิดของวันถัดไป');
|
||||
if(!d.complete&&d.account_exists)notes.push('ข้อมูลราคาหุ้นในพอร์ตไม่ครบหรือเก่ากว่าสัญญาณ จึงพักคำแนะนำซื้อขายจนกว่าจะตรวจราคาได้');
|
||||
if(d.requires_sell_proceeds>0)notes.push('รายการซื้อเป้าหมายใช้เงินมากกว่าเงินสดปัจจุบัน; ต้องพิจารณารายการขายและกำลังซื้อจากโบรกเกอร์ก่อน');
|
||||
if(d.refresh?.running)notes.push(d.refresh.stage_message||'กำลังอัปเดตข้อมูลและคำนวณสัญญาณ');
|
||||
@@ -287,7 +295,7 @@
|
||||
|
||||
$('deposit-form').addEventListener('submit',async event=>{event.preventDefault();const button=$('deposit-submit');button.disabled=true;try{const response=await fetch('/api/portfolio/deposit',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({amount:Number($('deposit-amount').value),note:$('deposit-note').value})});const result=await response.json();if(!response.ok)throw new Error(result.message);setMessage('deposit-message',result.message);$('deposit-form').reset();await load();}catch(error){setMessage('deposit-message',error.message,true);}finally{button.disabled=false;}});
|
||||
|
||||
$('trade-edit-form').addEventListener('submit',async event=>{event.preventDefault();const button=$('trade-edit-form').querySelector('button[type=submit]');button.disabled=true;try{const body={trade_id:Number($('edit-trade-id').value),quantity:Number($('edit-trade-quantity').value),price:Number($('edit-trade-price').value),fees:Number($('edit-trade-fees').value||0)};const response=await fetch('/api/portfolio/edit-trade',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});const result=await response.json();if(!response.ok)throw new Error(result.message);setMessage('edit-trade-message',result.message);await load();}catch(error){setMessage('edit-trade-message',error.message,true);}finally{button.disabled=false;}});
|
||||
$('trade-edit-form').addEventListener('submit',async event=>{event.preventDefault();const button=$('trade-edit-form').querySelector('button[type=submit]');button.disabled=true;try{const body={trade_id:Number($('edit-trade-id').value),trade_date:$('edit-trade-date').value,quantity:Number($('edit-trade-quantity').value),price:Number($('edit-trade-price').value),fees:Number($('edit-trade-fees').value||0)};const response=await fetch('/api/portfolio/edit-trade',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});const result=await response.json();if(!response.ok)throw new Error(result.message);setMessage('edit-trade-message',result.message);await load();}catch(error){setMessage('edit-trade-message',error.message,true);}finally{button.disabled=false;}});
|
||||
$('edit-trade-cancel').addEventListener('click',()=>{$('trade-edit-form').hidden=true;});
|
||||
|
||||
$('cash-event-form').addEventListener('submit',async event=>{event.preventDefault();const button=$('cash-event-form').querySelector('button[type=submit]');button.disabled=true;try{const body={kind:$('cash-event-kind').value,amount:Number($('cash-event-amount').value),note:$('cash-event-note').value};const response=await fetch('/api/portfolio/cash-event',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});const result=await response.json();if(!response.ok)throw new Error(result.message);setMessage('cash-event-message',result.message);$('cash-event-form').reset();await load();}catch(error){setMessage('cash-event-message',error.message,true);}finally{button.disabled=false;}});
|
||||
|
||||
@@ -974,9 +974,11 @@ def _handler_factory(archives: tuple[Path, ...], report_dir: Path,
|
||||
raise ValueError("รหัสรายการและจำนวนหุ้นต้องเป็นจำนวนเต็ม")
|
||||
update_trade(
|
||||
int(raw_trade_id), int(raw_quantity), float(payload.get("price", 0)),
|
||||
float(payload.get("fees", 0)), database=DEFAULT_BOOK,
|
||||
float(payload.get("fees", 0)),
|
||||
trade_date=(str(payload.get("trade_date", "")).strip() or None),
|
||||
database=DEFAULT_BOOK,
|
||||
)
|
||||
message = f"แก้จำนวน/ราคาจริงของรายการ #{int(raw_trade_id)} แล้ว"
|
||||
message = f"แก้วันที่ซื้อ/จำนวน/ราคาจริงของรายการ #{int(raw_trade_id)} แล้ว"
|
||||
elif path.endswith("/void-trade"):
|
||||
raw_trade_id = float(payload.get("trade_id", 0))
|
||||
if not raw_trade_id.is_integer():
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import unittest
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
from primethai.portfolio_book import add_cash_event, portfolio_view, record_trade, update_price_marks
|
||||
from primethai.portfolio_book import (
|
||||
add_cash_event,
|
||||
portfolio_view,
|
||||
record_trade,
|
||||
update_price_marks,
|
||||
update_trade,
|
||||
)
|
||||
|
||||
|
||||
class PortfolioCapitalSelectionTests(unittest.TestCase):
|
||||
@@ -52,5 +59,43 @@ class PortfolioCapitalSelectionTests(unittest.TestCase):
|
||||
database.unlink(missing_ok=True)
|
||||
|
||||
|
||||
class PortfolioValuationDateTests(unittest.TestCase):
|
||||
def test_purchase_day_close_is_not_counted_until_next_day(self):
|
||||
database = Path(__file__).with_name(f".portfolio_valuation_test_{os.getpid()}.sqlite3")
|
||||
today = datetime.now().astimezone().date()
|
||||
purchase_date = today.isoformat()
|
||||
next_date = (today + timedelta(days=1)).isoformat()
|
||||
signals = [{
|
||||
"symbol": "AAA",
|
||||
"source_model": "donchian_55_20+ema50_200_cross",
|
||||
"target_weight": "1",
|
||||
"model_agreement": "2",
|
||||
"signal_as_of": purchase_date,
|
||||
}]
|
||||
try:
|
||||
add_cash_event("deposit", 10_000, database=database)
|
||||
update_price_marks(purchase_date, {"AAA": 10.0}, database=database)
|
||||
trade_id = record_trade("AAA", "BUY", 100, 10.0, database=database)
|
||||
# The user edits the signal's reference price to the actual fill.
|
||||
update_trade(trade_id, 100, 9.0, trade_date=purchase_date, database=database)
|
||||
|
||||
same_day = portfolio_view(signals, signal_as_of=purchase_date, database=database)
|
||||
self.assertIsNone(same_day["profit_loss"])
|
||||
self.assertEqual(same_day["valuation_pending_symbols"], ["AAA"])
|
||||
self.assertIsNone(same_day["rows"][0]["unrealized_pnl"])
|
||||
|
||||
update_price_marks(next_date, {"AAA": 10.0}, database=database)
|
||||
next_day = portfolio_view(
|
||||
[{**signals[0], "signal_as_of": next_date}],
|
||||
signal_as_of=next_date,
|
||||
database=database,
|
||||
)
|
||||
self.assertEqual(next_day["valuation_pending_symbols"], [])
|
||||
self.assertAlmostEqual(next_day["profit_loss"], 100.0)
|
||||
self.assertAlmostEqual(next_day["rows"][0]["unrealized_pnl"], 100.0)
|
||||
finally:
|
||||
database.unlink(missing_ok=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user