diff --git a/core/settings.py b/core/settings.py index 0d70e5f..44931cd 100644 --- a/core/settings.py +++ b/core/settings.py @@ -13,6 +13,7 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', # Make sure this is here only once + 'django.contrib.humanize', # intcomma etc. for invest dashboard amounts 'widget_tweaks', 'core.apps.CoreConfig', 'links', diff --git a/invest/services.py b/invest/services.py index 8addba2..afe365a 100644 --- a/invest/services.py +++ b/invest/services.py @@ -8,7 +8,6 @@ Design goals: """ import json import logging -from collections import defaultdict from datetime import date as date_cls from datetime import datetime, timedelta from decimal import Decimal @@ -518,24 +517,30 @@ def get_risk_summary() -> dict: }) total_value = sum(h['current_value'] for h in holdings) - holdings.sort(key=lambda h: h['current_value'], reverse=True) - for holding in holdings: - holding['weight'] = round(holding['current_value'] / total_value, 6) if total_value else 0 + # 集中度口径统一:按 ticker 聚合(同一股票跨账户合并), + # 与 Top 1/3/5 权重卡片一致,避免 MRVL 等跨账户持仓在表格里重复出现。 + by_ticker: dict[str, dict] = {} + for h in holdings: + agg = by_ticker.setdefault(h['stock_code'], { + 'stock_code': h['stock_code'], + 'current_value': 0.0, + 'portfolio_names': [], + }) + agg['current_value'] += h['current_value'] + if h['portfolio_name'] not in agg['portfolio_names']: + agg['portfolio_names'].append(h['portfolio_name']) - top_1 = holdings[0]['weight'] if holdings else 0 - top_3 = sum(h['weight'] for h in holdings[:3]) - top_5 = sum(h['weight'] for h in holdings[:5]) + top_positions = sorted(by_ticker.values(), key=lambda r: r['current_value'], reverse=True) + for row in top_positions: + row['weight'] = round(row['current_value'] / total_value, 6) if total_value else 0 - by_ticker = defaultdict(float) - for holding in holdings: - by_ticker[holding['stock_code']] += holding['current_value'] - ticker_weights = { - ticker: value / total_value for ticker, value in by_ticker.items() - } if total_value else {} + top_1 = top_positions[0]['weight'] if top_positions else 0 + top_3 = sum(r['weight'] for r in top_positions[:3]) + top_5 = sum(r['weight'] for r in top_positions[:5]) - semi_weight = sum(weight for ticker, weight in ticker_weights.items() if ticker in SEMI_TICKERS) - ai_cloud_weight = sum(weight for ticker, weight in ticker_weights.items() if ticker in AI_CLOUD_TICKERS) + semi_weight = sum(r['weight'] for r in top_positions if r['stock_code'] in SEMI_TICKERS) + ai_cloud_weight = sum(r['weight'] for r in top_positions if r['stock_code'] in AI_CLOUD_TICKERS) concentration_level = 'LOW' if top_1 >= 0.25 or top_5 >= 0.70: @@ -545,13 +550,13 @@ def get_risk_summary() -> dict: return { 'total_value': round(total_value, 2), - 'position_count': len(holdings), + 'position_count': len(top_positions), 'top_1_weight': round(top_1, 6), 'top_3_weight': round(top_3, 6), 'top_5_weight': round(top_5, 6), 'concentration_level': concentration_level, - 'max_position': holdings[0] if holdings else None, - 'top_positions': holdings[:10], + 'max_position': top_positions[0] if top_positions else None, + 'top_positions': top_positions[:10], 'theme_exposure': { 'semiconductors': round(semi_weight, 6), 'ai_cloud': round(ai_cloud_weight, 6), diff --git a/invest/templates/invest/base.html b/invest/templates/invest/base.html deleted file mode 100644 index 9e1644f..0000000 --- a/invest/templates/invest/base.html +++ /dev/null @@ -1,37 +0,0 @@ -{% load static %} - - -
- - -Total Value
{% if overview.this_week_total is not None %} -${{ overview.this_week_total|floatformat:0 }}
+${{ overview.this_week_total|floatformat:0|intcomma }}
{% else %}—
{% endif %} @@ -26,14 +26,14 @@Net Contributions
-${{ net_contributions|floatformat:0 }}
+${{ net_contributions|floatformat:0|intcomma }}
External deposits minus withdrawals
Investment Gain
- {% if performance.cash_adjusted_gain >= 0 %}+{% endif %}${{ performance.cash_adjusted_gain|floatformat:0 }} + {% if performance.cash_adjusted_gain >= 0 %}+{% endif %}${{ performance.cash_adjusted_gain|floatformat:0|intcomma }}
Cash-flow adjusted
This Week
{% if overview.week_gain is not None %}- {% if overview.week_gain >= 0 %}+{% endif %}${{ overview.week_gain|floatformat:0 }} + {% if overview.week_gain >= 0 %}+{% endif %}${{ overview.week_gain|floatformat:0|intcomma }}
-{% if overview.week_gain >= 0 %}+{% endif %}{{ overview.week_change_pct|floatformat:2 }}% vs last snapshot
+{% if overview.week_change_pct >= 0 %}+{% endif %}{{ overview.week_change_pct|floatformat:2 }}% vs last snapshot
{% else %}—
No prior snapshot
@@ -71,7 +71,7 @@IRR based on cash flows
Last Snapshot
{% if overview.this_week_date %}{{ overview.this_week_date|date:"M j" }}
@@ -89,12 +89,12 @@Actual end value
-${{ performance.end_value|floatformat:0 }}
+${{ performance.end_value|floatformat:0|intcomma }}
Same cash flows into {{ ticker }}
-${{ bench.end_value|floatformat:0 }}
+${{ bench.end_value|floatformat:0|intcomma }}
Return {% if bench.simple_return is not None %}{% widthratio bench.simple_return 1 100 %}%{% else %}—{% endif %}
{{ group.position_count }} position{{ group.position_count|pluralize }}{% if overview.this_week_date %} · Snapshot {{ overview.this_week_date|date:"j M Y" }}{% endif %}
{% if group.this_week_value is not None %}${{ group.this_week_value|floatformat:0 }}{% else %}—{% endif %}
+{% if group.this_week_value is not None %}${{ group.this_week_value|floatformat:0|intcomma }}{% else %}—{% endif %}
{% if group.change is not None %} -{% if group.change >= 0 %}+{% endif %}${{ group.change|floatformat:0 }} ({% if group.change_pct >= 0 %}+{% endif %}{{ group.change_pct|floatformat:1 }}%)
+{% if group.change >= 0 %}+{% endif %}${{ group.change|floatformat:0|intcomma }} ({% if group.change_pct >= 0 %}+{% endif %}{{ group.change_pct|floatformat:1 }}%)
{% else %}No prior snapshot
{% endif %} @@ -189,10 +189,10 @@ {% for stock in group.holdings %}Live Total Value
-${{ summary.total_value|floatformat:0 }}
+${{ summary.total_value|floatformat:0|intcomma }}
{{ summary.holdings|length }} position{{ summary.holdings|length|pluralize }}