From 52522226740d17f1e987ee5d00322c2a524b438e Mon Sep 17 00:00:00 2001 From: OpenClaw Sub-agent Date: Sun, 2 Aug 2026 07:45:09 +1000 Subject: [PATCH] =?UTF-8?q?feat(invest):=20P0=20dashboard=20polish=20?= =?UTF-8?q?=E2=80=94=20intcomma=20amounts,=20ticker-aggregated=20risk=20ta?= =?UTF-8?q?ble,=20unified=20mobile=20grid,=20tx=20source=20badges,=20drop?= =?UTF-8?q?=20dead=20invest/base.html?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add django.contrib.humanize; apply intcomma to every currency amount across invest templates (dashboard, portfolio detail, transactions) - get_risk_summary now aggregates top_positions by ticker (MRVL across MOMO+IBKR merges into one row with Accounts column), keeping Top 1/3/5 cards and table on the same basis - Mobile: metric cards all 2-col (2x2 + 2+1 with Last Snapshot spanning full width), no more 1-col break - Transactions show source badges: purple 🤖 AI (source ai/ocr), grey 手动, amber ⚠ 低置信 when confidence < 0.9 - Delete unused invest/base.html (dead nav; page extends GoLinks base.html) - tailwind.config.js: add invest/jbot/routermon template dirs so their classes are scanned (text-[10px] etc. were silently missing) --- core/settings.py | 1 + invest/services.py | 41 ++++++++------ invest/templates/invest/base.html | 37 ------------- invest/templates/invest/dashboard.html | 55 +++++++++++-------- invest/templates/invest/portfolio_detail.html | 12 ++-- invest/templates/invest/transactions.html | 15 ++++- tailwind.config.js | 3 + 7 files changed, 79 insertions(+), 85 deletions(-) delete mode 100644 invest/templates/invest/base.html 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 %} - - - - - - {% block title %}Portfolio{% endblock %} – Invest - - - - - {% block extra_head %}{% endblock %} - - - - - - -
- {% block content %}{% endblock %} -
- -{% block extra_js %}{% endblock %} - - diff --git a/invest/templates/invest/dashboard.html b/invest/templates/invest/dashboard.html index 48e0c55..2acf6dc 100644 --- a/invest/templates/invest/dashboard.html +++ b/invest/templates/invest/dashboard.html @@ -1,5 +1,5 @@ {% extends "base.html" %} -{% load static i18n %} +{% load static i18n humanize %} {% block extra_head %} @@ -17,7 +17,7 @@

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

@@ -47,14 +47,14 @@
-
+

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 }}

{% for ticker, bench in performance.benchmarks.items %}

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 %}

{% empty %} @@ -137,16 +137,16 @@ Ticker Value Weight - Portfolio + Accounts {% for position in risk.top_positions|slice:":5" %} {{ position.stock_code }} - ${{ position.current_value|floatformat:0 }} + ${{ position.current_value|floatformat:0|intcomma }} {% widthratio position.weight 1 100 %}% - {{ position.portfolio_name }} + {{ position.portfolio_names|join:", " }} {% endfor %} @@ -167,9 +167,9 @@

{{ 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 %} {{ stock.stock_code }} - {{ stock.quantity|floatformat:0 }} - {% if stock.current_price %}${{ stock.current_price|floatformat:2 }}{% else %}{% endif %} - {% if stock.value_change is not None %}{% if stock.value_change >= 0 %}+{% endif %}${{ stock.value_change|floatformat:0 }} ({% if stock.price_change_pct >= 0 %}+{% endif %}{{ stock.price_change_pct|floatformat:1 }}%){% else %}—{% endif %} - {% if stock.current_value %}${{ stock.current_value|floatformat:0 }}{% else %}{% endif %} + {{ stock.quantity|floatformat:0|intcomma }} + {% if stock.current_price %}${{ stock.current_price|floatformat:2|intcomma }}{% else %}{% endif %} + {% if stock.value_change is not None %}{% if stock.value_change >= 0 %}+{% endif %}${{ stock.value_change|floatformat:0|intcomma }} ({% if stock.price_change_pct >= 0 %}+{% endif %}{{ stock.price_change_pct|floatformat:1 }}%){% else %}—{% endif %} + {% if stock.current_value %}${{ stock.current_value|floatformat:0|intcomma }}{% else %}{% endif %} {% endfor %} @@ -245,6 +245,7 @@ Qty Price Fee + Source @@ -254,9 +255,19 @@ {{ tx.portfolio.name }} {{ tx.action }} {{ tx.stock_code }} - {{ tx.quantity|floatformat:0 }} - {% if tx.price_per_share %}${{ tx.price_per_share|floatformat:2 }}{% else %}optional{% endif %} - {% if tx.fee %}${{ tx.fee|floatformat:2 }}{% else %}{% endif %} + {{ tx.quantity|floatformat:0|intcomma }} + {% if tx.price_per_share %}${{ tx.price_per_share|floatformat:2|intcomma }}{% else %}optional{% endif %} + {% if tx.fee %}${{ tx.fee|floatformat:2|intcomma }}{% else %}{% endif %} + + {% if tx.source %} + + {% if tx.source|lower == 'ai' or tx.source|lower == 'ocr' %}🤖 AI{% else %}手动{% endif %} + + {% if tx.confidence is not None and tx.confidence < 0.9 %} + ⚠ 低置信 + {% endif %} + {% endif %} + {% endfor %} diff --git a/invest/templates/invest/portfolio_detail.html b/invest/templates/invest/portfolio_detail.html index 325c3e9..390e0e9 100644 --- a/invest/templates/invest/portfolio_detail.html +++ b/invest/templates/invest/portfolio_detail.html @@ -1,5 +1,5 @@ {% extends "base.html" %} -{% load static i18n %} +{% load static i18n humanize %} {% block title %}{{ portfolio.name }} - Portfolio{% endblock %} @@ -16,7 +16,7 @@

Live Total Value

-

${{ summary.total_value|floatformat:0 }}

+

${{ summary.total_value|floatformat:0|intcomma }}

{{ summary.holdings|length }} position{{ summary.holdings|length|pluralize }}

@@ -47,18 +47,18 @@ {% for stock in summary.holdings %} {{ stock.stock_code }} - {{ stock.quantity|floatformat:2 }} + {{ stock.quantity|floatformat:2|intcomma }} - {% if stock.current_price > 0 %}${{ stock.current_price|floatformat:2 }}{% else %}{% endif %} + {% if stock.current_price > 0 %}${{ stock.current_price|floatformat:2|intcomma }}{% else %}{% endif %} - ${{ stock.current_value|floatformat:0 }} + ${{ stock.current_value|floatformat:0|intcomma }} {% endfor %} Total - ${{ summary.total_value|floatformat:0 }} + ${{ summary.total_value|floatformat:0|intcomma }} diff --git a/invest/templates/invest/transactions.html b/invest/templates/invest/transactions.html index 575d592..8f177b2 100644 --- a/invest/templates/invest/transactions.html +++ b/invest/templates/invest/transactions.html @@ -1,5 +1,5 @@ {% extends "base.html" %} -{% load static i18n %} +{% load static i18n humanize %} {% block title %}{{ portfolio.name }} - Transactions{% endblock %} @@ -31,6 +31,7 @@ Action Stock Quantity + Source @@ -43,7 +44,17 @@ {{ tx.stock_code }} - {{ tx.quantity|floatformat:2 }} + {{ tx.quantity|floatformat:2|intcomma }} + + {% if tx.source %} + + {% if tx.source|lower == 'ai' or tx.source|lower == 'ocr' %}🤖 AI{% else %}手动{% endif %} + + {% if tx.confidence is not None and tx.confidence < 0.9 %} + ⚠ 低置信 + {% endif %} + {% endif %} + {% endfor %} diff --git a/tailwind.config.js b/tailwind.config.js index 7ef8fc2..7081e88 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -5,6 +5,9 @@ module.exports = { './new_theme/templates/**/*.html', './netscan/templates/**/*.html', './nginxmon/templates/**/*.html', + './routermon/templates/**/*.html', + './invest/templates/**/*.html', + './jbot/templates/**/*.html', './static_src/**/*.{js,jsx}', './**/*.js', ],