I think that's it for now.
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -32,14 +32,22 @@ def _normalize_buy_time(open_dt: str | None) -> str | None:
|
||||
return open_dt
|
||||
|
||||
|
||||
def _compute_cagr_percent(cost_basis, gain_dollars, buy_time: str | None):
|
||||
def _compute_cagr_percent(cost_basis, gain_dollars, market_value, buy_time: str | None):
|
||||
if cost_basis in (None, 0) or gain_dollars is None or not buy_time:
|
||||
return None
|
||||
try:
|
||||
start = datetime.fromisoformat(buy_time).replace(tzinfo=timezone.utc)
|
||||
now = datetime.now(timezone.utc)
|
||||
years = max((now - start).total_seconds() / (365.25 * 24 * 3600), 1e-9)
|
||||
gain_percent = (float(gain_dollars) / abs(float(cost_basis))) * 100.0
|
||||
if years < 1.0:
|
||||
return gain_percent
|
||||
start_value = abs(float(cost_basis))
|
||||
if float(cost_basis) < 0:
|
||||
end_liability = abs(float(market_value)) if market_value is not None else abs(float(cost_basis) + float(gain_dollars))
|
||||
if start_value <= 0 or end_liability <= 0:
|
||||
return None
|
||||
return (((start_value / end_liability) ** (1.0 / years)) - 1.0) * 100.0
|
||||
end_value = start_value + float(gain_dollars)
|
||||
if start_value <= 0 or end_value <= 0:
|
||||
return None
|
||||
@@ -73,6 +81,20 @@ def _instrument_key(attrs: dict) -> str:
|
||||
return (attrs.get("symbol") or "").strip()
|
||||
|
||||
|
||||
def _signed_quantity(raw_position, side: str, cost_basis, market_value):
|
||||
quantity = abs(raw_position or 0.0)
|
||||
if quantity == 0:
|
||||
return 0.0
|
||||
normalized_side = (side or "").strip().lower()
|
||||
if normalized_side == "short" or (raw_position or 0.0) < 0:
|
||||
return -quantity
|
||||
if cost_basis is not None and float(cost_basis) < 0:
|
||||
return -quantity
|
||||
if market_value is not None and float(market_value) < 0:
|
||||
return -quantity
|
||||
return quantity
|
||||
|
||||
|
||||
def parse_flex_lots(xml_path: str | Path) -> list[dict]:
|
||||
root = ET.parse(xml_path).getroot()
|
||||
lots: list[dict] = []
|
||||
@@ -83,27 +105,43 @@ def parse_flex_lots(xml_path: str | Path) -> list[dict]:
|
||||
|
||||
security_type = (attrs.get("assetCategory") or "").upper() or "STK"
|
||||
multiplier = _as_float(attrs.get("multiplier"), 1.0) or 1.0
|
||||
fx_rate_to_base = _as_float(attrs.get("fxRateToBase"), 1.0) or 1.0
|
||||
raw_position = _as_float(attrs.get("position"), 0.0) or 0.0
|
||||
side = (attrs.get("side") or "").strip().lower()
|
||||
qty = -abs(raw_position) if side == "short" or raw_position < 0 else abs(raw_position)
|
||||
|
||||
mark_price = _as_float(attrs.get("markPrice"))
|
||||
market_price = mark_price
|
||||
market_value = _as_float(attrs.get("positionValue"))
|
||||
cost_basis = _as_float(attrs.get("costBasisMoney"))
|
||||
avg_price = _as_float(attrs.get("costBasisPrice"))
|
||||
if avg_price is None:
|
||||
open_price = _as_float(attrs.get("openPrice"))
|
||||
avg_price = open_price
|
||||
if avg_price is None and qty:
|
||||
divisor = qty * multiplier if security_type == "OPT" else qty
|
||||
avg_price = abs(cost_basis / divisor) if cost_basis is not None and divisor else None
|
||||
buy_time = _normalize_buy_time(attrs.get("openDateTime"))
|
||||
cost_basis_base = _as_float(attrs.get("costBasisMoney"))
|
||||
capital_gain_base = _as_float(attrs.get("unrealizedCapitalGainsPnl"))
|
||||
gain_dollars = _as_float(attrs.get("fifoPnlUnrealized"))
|
||||
qty = _signed_quantity(raw_position, side, cost_basis_base, market_value)
|
||||
|
||||
# Flex OpenPosition rows mix currencies for non-base holdings:
|
||||
# mark/positionValue are in instrument currency, while costBasis/openPrice
|
||||
# are in account base. Reconstruct local-cost values so they line up with TWS.
|
||||
cost_basis = cost_basis_base
|
||||
if market_value is not None and capital_gain_base is not None and fx_rate_to_base:
|
||||
cost_basis = market_value - (capital_gain_base / fx_rate_to_base)
|
||||
|
||||
avg_price = None
|
||||
if qty:
|
||||
divisor = abs(qty) * multiplier if security_type == "OPT" else abs(qty)
|
||||
avg_price = abs(cost_basis / divisor) if cost_basis is not None and divisor else None
|
||||
if avg_price is None:
|
||||
avg_price = _as_float(attrs.get("costBasisPrice"))
|
||||
if avg_price is None:
|
||||
avg_price = _as_float(attrs.get("openPrice"))
|
||||
buy_time = _normalize_buy_time(attrs.get("openDateTime"))
|
||||
|
||||
# Prefer local-currency capital gain to keep gain/cost/value in one currency.
|
||||
if capital_gain_base is not None and fx_rate_to_base:
|
||||
gain_dollars = capital_gain_base / fx_rate_to_base
|
||||
|
||||
gain_percent = None
|
||||
if cost_basis not in (None, 0) and gain_dollars is not None:
|
||||
gain_percent = (gain_dollars / abs(cost_basis)) * 100.0
|
||||
cagr_percent = _compute_cagr_percent(cost_basis, gain_dollars, buy_time)
|
||||
cagr_percent = _compute_cagr_percent(cost_basis, gain_dollars, market_value, buy_time)
|
||||
days_since_buy = _compute_days_since_buy(buy_time)
|
||||
ltcg = "X" if days_since_buy is not None and days_since_buy >= 365 else ""
|
||||
|
||||
@@ -112,6 +150,7 @@ def parse_flex_lots(xml_path: str | Path) -> list[dict]:
|
||||
"con_id": _as_float(attrs.get("conid")),
|
||||
"symbol": attrs.get("symbol"),
|
||||
"display_symbol": _display_symbol(attrs),
|
||||
"currency": attrs.get("currency") or None,
|
||||
"security_type": security_type,
|
||||
"underlying_symbol": attrs.get("underlyingSymbol") or attrs.get("symbol"),
|
||||
"expiry": _normalize_expiry(attrs.get("expiry")),
|
||||
|
||||
Reference in New Issue
Block a user