feat: UI稿设计

This commit is contained in:
R524809
2025-11-12 18:06:09 +08:00
parent 75ca7f10be
commit 33b5d72461
8 changed files with 3606 additions and 360 deletions

File diff suppressed because it is too large Load Diff

1308
src/investment-record.html Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -480,3 +480,34 @@ COMMIT;
- 支付相关数据需要加密存储 - 支付相关数据需要加密存储
- 交易号等敏感信息需要脱敏处理 - 交易号等敏感信息需要脱敏处理
7. **持仓自动价格更新功能联动**
- 用户购买订阅后,需要批量更新该用户所有持仓的 `auto_price_update = true`
- 用户订阅过期或取消后,需要批量更新该用户所有持仓的 `auto_price_update = false`
- 建议在订阅状态变更时,同步更新 positions 表的 auto_price_update 字段
**示例SQL**
```sql
-- 用户购买订阅后,启用自动价格更新
UPDATE positions
SET auto_price_update = true
WHERE user_id = :user_id
AND asset_type IN ('stock', 'fund', 'bond');
-- 用户订阅过期后,禁用自动价格更新
UPDATE positions
SET auto_price_update = false
WHERE user_id = :user_id;
-- 查询付费用户的股票持仓(用于自动价格更新)
SELECT
p.position_id,
p.symbol,
p.market,
p.asset_type,
p.current_price
FROM positions p
WHERE p.auto_price_update = true
AND p.asset_type = 'stock'
AND p.status = 'active';
```

View File

@@ -1,22 +0,0 @@
# 投资记录模块-产品设计
## 一、核心设计思路
### 1.1 持仓变更设计
1. 把所有的表更分为两种:`用户驱动(主动)``系统驱动(被动)`
2. 系统驱动包含:现金分红、送股、拆股、汇率变动等
3. 用户驱动包含:初始买入、追加买入、卖出;
4. 所有的用户驱动,都只需要变更最终的持仓成本和最新的持仓份额,这两项。避免其他复杂的操作
5. 每次用户主动变更,记录成本价和份额的同时,还需要完成如下记录:
- 反向计算本次交易股价和份额,并记录;
- 统计最新的份额和净值,并记录
- 同时引导填下投资复盘和思考。
6. 系统驱动的变更:(万一无法实现,可以降级为用户驱动变更)
- 分红:收盘后获取每股分红金额,最新成本价=原成本价 - 分红,市场价逻辑保持不变(使用不复权的股价)
- 送股、拆股等都变更最新的成本价和份额,并记录。
### 1.2 收益记录设计
1. 使用基金净值法(时间加权收益率)来统计收益;
2. 每次主动和被动变更,重新计算总体的资产金额和份额,记录到 daily_snapshots 表。
3. 忘记记录的情况:如果用户忘记记录当日交易,过一段时间后再来记录,需要删除期间的快照数据,并保留期间的交易数据。然后删除期间的快照数据,重新生成。
## 想法
- 私密分享:可以将自己的交易计划和复盘,通过小程序私密分享-分享给其他人,这样即保障了裂变属性,有增加了隐私安全。

View File

@@ -0,0 +1,93 @@
# 投资记录模块-产品设计
## 一、投资记录设计思路
职责清晰:系统负责所有可规则化、基于市场公开事件的处理(被动变更);用户只负责输入自己主动发起的行为结果(主动变更)。
输入极简:对于任何一笔交易,用户最核心、最确定的输入就是最终的持仓成本和最新的持仓份额。让用户只修改这两项,避免了理解复杂规则(如加权平均计算)的负担,也减少了输入错误的可能性。
输入极简:对于任何一笔交易,用户最核心、最确定的输入就是最终的持仓成本和最新的持仓份额。让用户只修改这两项,避免了理解复杂规则(如加权平均计算)的负担,也减少了输入错误的可能性。
### 1.1 持仓变更设计
1. 把所有的表更分为两种:`用户驱动(主动)``系统驱动(被动)`
2. 系统驱动包含:现金分红、送股、拆股、汇率变动等
3. 用户驱动包含:初始买入、追加买入、卖出;
4. 所有的用户驱动,都只需要变更最终的持仓成本和最新的持仓份额,这两项。避免其他复杂的操作
5. 每次用户主动变更,记录成本价和份额的同时,还需要完成如下记录:
- 反向计算本次交易股价和份额,并记录;
- 统计最新的份额和净值,并记录
- 同时引导填下投资复盘和思考。
6. 系统驱动的变更:(万一无法实现,可以降级为用户驱动变更)
- 分红:收盘后获取每股分红金额,最新成本价 = 原成本价 - 分红,市场价逻辑保持不变(使用不复权的股价)
- 送股、拆股等都变更最新的成本价和份额,并记录。
### 1.2 收益记录设计
1. 使用基金净值法(时间加权收益率)来统计收益;
2. 每次主动和被动变更,重新计算总体的资产金额和份额,记录到 daily_snapshots 表。
3. 忘记记录的情况:如果用户忘记记录当日交易,过一段时间后再来记录,需要删除期间的快照数据,并保留期间的交易数据。然后删除期间的快照数据,重新生成。
### 1.3 系统驱动变价记录逻辑
1. 首先需要有一个表记录所有股票的基本信息方便用户在输入code或股票名是进行模糊匹配。这个数据因为不常变更可以放在CDN上。
2. 每日定时把所有用户需要自动变价的持仓(股票/基金)汇总,在收盘后查询这些股票的最新市价、市值、市盈率等信息(所有用的是持仓最新价格应该依赖这个表,减少直接查询外部证券接口的次数)
3. 更新完股票的最新市价后,在定时更新用户持仓表中的持仓数据、收益数据,对应 positions 和 asset_snapshots 表。
## 二、持仓页面详细设计
### 页面概述
页面整体展示三部分内容:
- 资产概览、
- 资产和收益图表(累计资产折线图、收益率折线图)、
- 持仓百分比图
- 我的持仓列表
### 功能需求
#### 资产概览
以卡片形式展示资产概览,左上角显示用户昵称,下边显示记账时长(例如 记账:300天右侧从上到下分别展示总金额大字红色展示、上一个交易日收益、累计收益、累计收益率、年化收益率。
#### 收益图表
- 第一个图表默认展示从记账以来每日总金额的折线图可以切换时间近5日、本月、近一个月、近一年、记账以来、自定义
- 第二个图表默认展示从记账以来每日累计收益率可以切换不同时间段近5日、本月、近一个月、近一年、记账以来、自定义
PS 你在输出设计稿的时候,需要使用 Echarts 来模拟)
#### 持仓百分比
使用环图,展示各个持仓资产项的金额百分比
PS 你在输出设计稿的时候,需要使用 Echarts 来模拟)
#### 我的持仓列表
**展示形式**
title 左侧展示 “我的持仓”,右侧展示一个 圆形的加号(添加资产项) 按钮,可以添加新的资产项。
以卡片形式展示 资产项:
- 左侧依次展示:公司简称(大号字体)、股票代码、市场、持股市场、证券公司简称(这一块你要帮我看看如何排列会更好)
- 中间展示:持股数、持股时长。
- 右侧展示:从上到下依次展示昨日市价、盈亏金额、收益率。
- 右小角展示一个 “更新资产”(名称可以你再帮我想一下) 的按钮点击从底部弹出Popup页变更资产页
**操作形式**
- 点击 圆形的“添加资产项”按钮,从底部弹出 新增资产页。
- 点击 资产项卡片总的“更新资产”的按钮,从底部弹出 变更资产页
## 三、新增资产页/变更资产页
这个页面有两种形态,新增和变更,差别是新增最上面有搜索框,变更没有搜索框。都是冲底部弹出,再上滑,可以变成全屏页面。
**新增资产页**
最上面可以选择资产类型,分别是:股票、基金、现金、其他
股票:
先展示 搜索框用户可以输入code或股票名称自动联想用户选择后确认股票。
然后选择券商,下拉列表展示。
选择后之后依次展示股票名称、股票code、市场。
之后展示成本价输入框和份数输入框。(输入时要考虑便捷性)
基金:
不展示搜索框,仅展示输入框,需要用户自己输入。
之后展示成本价输入框和份数输入框。(输入时要考虑便捷性)
现金和其他:
不展示搜索框,仅展示输入框,需要用户自己输入资产名称。
直接输入金额即可。
确认按钮:点击确认,提交数据到后台,按钮居中展示。
**变更资产页**
最上面不需要在选择资产类型,下边的展示和`新增资产页` 一样。

View File

@@ -7,10 +7,15 @@
用户相关 用户相关
├── users (用户表) ├── users (用户表)
持仓相关 基础数据相关
├── brokers (券商表) ├── brokers (券商表)
├── stock_info (股票基本信息表)
└── stock_daily_price (股票每日收盘价表)
账户相关
├── positions (持仓表) ├── positions (持仓表)
── position_price_plans (持仓价格目标表) ── asset_snapshots (资产快照表)
└── position_price_plans (持仓价格计划表)
``` ```
## 用户相关表 ## 用户相关表
@@ -100,337 +105,9 @@ COMMENT ON COLUMN users.last_login_at IS '最后登录时间';
``` ```
### daily_snapshots 表设计
**表结构**
| 字段名 | 数据类型 | 约束 | 说明 |
|--------|---------|------|------|
| id | BIGSERIAL | PRIMARY KEY | 快照ID自增 |
| user_id | BIGINT | NOT NULL, FOREIGN KEY | 用户ID关联 users 表 |
| date | DATE | NOT NULL | 快照日期 |
| total_asset | DECIMAL(18, 2) | NOT NULL | 总资产(所有持仓市值 + 现金余额) |
| total_invested | DECIMAL(18, 2) | NOT NULL | 累计投入金额(所有投入的资金总和) |
| time_weighted_return | DECIMAL(10, 6) | NOT NULL | 时间加权收益率(累计收益率) |
| annualized_return | DECIMAL(10, 6) | | 年化收益率 |
| year_to_date_return | DECIMAL(10, 6) | | 当年收益率(年初至今) |
| positions_data | JSONB | | 持仓明细快照JSON格式可选 |
| cash_data | JSONB | | 现金账户明细快照JSON格式可选 |
| created_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 |
| UNIQUE(user_id, date) | | | 同一用户同一日期只能有一条快照 |
**创建语句**
```sql
CREATE TABLE daily_snapshots (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
date DATE NOT NULL,
total_asset DECIMAL(18, 2) NOT NULL,
total_invested DECIMAL(18, 2) NOT NULL,
time_weighted_return DECIMAL(10, 6) NOT NULL DEFAULT 0,
annualized_return DECIMAL(10, 6),
year_to_date_return DECIMAL(10, 6),
positions_data JSONB,
cash_data JSONB,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, date)
);
-- 创建索引
CREATE INDEX idx_daily_snapshots_user_id ON daily_snapshots(user_id);
CREATE INDEX idx_daily_snapshots_date ON daily_snapshots(date);
CREATE INDEX idx_daily_snapshots_user_date ON daily_snapshots(user_id, date DESC);
-- 添加注释
COMMENT ON TABLE daily_snapshots IS '每日资产快照表,用于基金净值法计算收益率';
COMMENT ON COLUMN daily_snapshots.total_asset IS '总资产:所有持仓市值 + 现金余额';
COMMENT ON COLUMN daily_snapshots.total_invested IS '累计投入金额:所有投入的资金总和';
COMMENT ON COLUMN daily_snapshots.time_weighted_return IS '时间加权收益率(累计收益率)';
COMMENT ON COLUMN daily_snapshots.positions_data IS '持仓明细快照JSON格式存储';
COMMENT ON COLUMN daily_snapshots.cash_data IS '现金账户明细快照JSON格式存储';
```
#### 基础字段计算
**total_asset总资产**
```
total_asset = Σ(持仓市值) + 现金余额
其中:
- 持仓市值 = 持仓份额 × 当前价格
- 现金余额 = 所有现金账户余额之和
```
**total_invested累计投入金额**
```
total_invested = 初始投入 + 后续投入 - 提取金额
说明:
- 初始投入:账户创建时的初始资金
- 后续投入:用户手动记录的资金投入
- 提取金额:用户提取的资金(提现等)
- 注意:买入股票的资金不算"投入",因为只是资产形式转换
```
#### 净值计算(不存储,查询时计算)
**net_value单位净值**
```sql
-- 查询时计算
net_value = total_asset / NULLIF(total_invested, 0)
-- 示例 SQL
SELECT
date,
total_asset,
total_invested,
total_asset / NULLIF(total_invested, 0) as net_value
FROM daily_snapshots
WHERE user_id = :user_id
ORDER BY date;
```
**total_profit总收益**
```sql
-- 查询时计算
total_profit = total_asset - total_invested
-- 示例 SQL
SELECT
date,
total_asset,
total_invested,
total_asset - total_invested as total_profit
FROM daily_snapshots
WHERE user_id = :user_id
ORDER BY date;
```
#### 收益率计算
**time_weighted_return时间加权收益率**
基于基金净值法,通过相邻日期的净值变化计算:
```sql
-- 计算逻辑
-- 1. 计算每日净值
net_value_today = total_asset_today / total_invested_today
net_value_yesterday = total_asset_yesterday / total_invested_yesterday
-- 2. 计算日收益率
daily_return = (net_value_today - net_value_yesterday) / net_value_yesterday
-- 3. 累计收益率(复利)
time_weighted_return = (1 + r1) × (1 + r2) × ... × (1 + rn) - 1
-- 实际存储时,存储累计收益率
```
**annualized_return年化收益率**
```sql
-- 计算逻辑
annualized_return = (1 + time_weighted_return)^(365 / 投资天数) - 1
-- 投资天数 = 当前日期 - 首次投入日期
```
**year_to_date_return当年收益率**
```sql
-- 计算逻辑
-- 1. 获取年初快照
year_start_snapshot = SELECT * FROM daily_snapshots
WHERE user_id = :user_id
AND date = DATE_TRUNC('year', CURRENT_DATE)
-- 2. 计算当年收益率
year_to_date_return = (current_net_value - year_start_net_value) / year_start_net_value
-- 如果没有年初快照,使用年初第一个快照
```
#### 快照数据生成规则
**positions_data持仓明细快照**
```json
[
{
"position_id": 1,
"symbol": "600519",
"name": "贵州茅台",
"shares": 100,
"cost_price": 1600.00,
"current_price": 1850.00,
"market_value": 185000.00,
"profit": 25000.00
},
{
"position_id": 2,
"symbol": "00700",
"name": "腾讯控股",
"shares": 200,
"cost_price": 320.00,
"current_price": 300.00,
"market_value": 60000.00,
"profit": -4000.00
}
]
```
**cash_data现金账户明细快照**
```json
[
{
"account_id": 1,
"currency": "CNY",
"balance": 50000.00
},
{
"account_id": 2,
"currency": "USD",
"balance": 1000.00,
"exchange_rate": 7.2,
"balance_cny": 7200.00
}
]
```
#### 每日快照生成流程
```sql
-- 伪代码流程
1. 获取用户所有持仓
SELECT * FROM positions WHERE user_id = :user_id AND status = 'active'
2. 获取用户所有现金账户
SELECT * FROM cash_accounts WHERE account_id IN (用户账户列表)
3. 计算总资产
total_asset = Σ(持仓市值) + Σ(现金余额)
4. 计算累计投入(从资金变动记录表获取)
total_invested = SELECT SUM(amount) FROM cash_flows
WHERE user_id = :user_id AND flow_type = 'deposit'
- SELECT SUM(amount) FROM cash_flows
WHERE user_id = :user_id AND flow_type = 'withdraw'
5. 计算时间加权收益率
- 获取昨日快照
- 计算净值变化
- 更新累计收益率
6. 计算年化收益率和当年收益率
7. 生成持仓和现金明细快照(JSON格式
8. 插入或更新快照记录
INSERT INTO daily_snapshots (...)
ON CONFLICT (user_id, date) DO UPDATE SET ...
```
#### 时间加权收益率更新逻辑
```sql
-- 获取昨日快照
WITH yesterday_snapshot AS (
SELECT * FROM daily_snapshots
WHERE user_id = :user_id
AND date = CURRENT_DATE - INTERVAL '1 day'
),
today_data AS (
SELECT
:total_asset as total_asset,
:total_invested as total_invested
)
-- 计算今日净值
SELECT
(today_data.total_asset / NULLIF(today_data.total_invested, 0)) as today_net_value,
(yesterday_snapshot.total_asset / NULLIF(yesterday_snapshot.total_invested, 0)) as yesterday_net_value
FROM today_data, yesterday_snapshot;
-- 计算日收益率
daily_return = (today_net_value - yesterday_net_value) / yesterday_net_value
-- 更新累计收益率
new_time_weighted_return = (1 + yesterday_snapshot.time_weighted_return) × (1 + daily_return) - 1
```
#### 查询用户净值曲线 ## 基础数据相关表设计
```sql
SELECT
date,
total_asset,
total_invested,
total_asset / NULLIF(total_invested, 0) as net_value,
total_asset - total_invested as total_profit,
time_weighted_return,
annualized_return
FROM daily_snapshots
WHERE user_id = :user_id
ORDER BY date DESC
LIMIT 365; -- 最近一年
```
#### 查询收益率统计
```sql
SELECT
date,
time_weighted_return * 100 as return_rate_percent,
annualized_return * 100 as annualized_return_percent,
year_to_date_return * 100 as ytd_return_percent
FROM daily_snapshots
WHERE user_id = :user_id
ORDER BY date DESC
LIMIT 30; -- 最近30天
```
#### 查询持仓明细历史
```sql
SELECT
date,
positions_data
FROM daily_snapshots
WHERE user_id = :user_id
AND positions_data IS NOT NULL
ORDER BY date DESC
LIMIT 10;
```
#### 注意事项
1. **数据一致性**
- `total_invested` 必须大于 0否则净值计算会出错
- 使用 `NULLIF(total_invested, 0)` 避免除零错误
2. **性能优化**
- 每日快照在收盘后批量生成(如 18:00
- 使用索引加速查询user_id, date
- positions_data 和 cash_data 使用 JSONB 类型,支持高效查询
3. **数据完整性**
- 确保每日都有快照(即使没有交易)
- 如果某日没有快照,使用最近一次快照的数据
4. **净值计算**
- 净值不存储,查询时计算,保证数据一致性
- 总收益也不存储,查询时计算
5. **收益率计算**
- 时间加权收益率需要每日更新
- 年化收益率需要定期重新计算(因为投资天数在变化)
- 当年收益率需要每年重置计算基准
---
## 持仓相关表设计
### brokers 券商表 ### brokers 券商表
@@ -528,6 +205,238 @@ INSERT INTO brokers (broker_code, broker_name, region, sort_order) VALUES
--- ---
### stock_info 股票基本信息表
**表结构**
| 字段名 | 数据类型 | 约束 | 说明 |
|--------|---------|------|------|
| id | BIGSERIAL | PRIMARY KEY | 主键ID自增 |
| stock_code | VARCHAR(20) | NOT NULL | 股票代码600519, 00700.HK |
| stock_name | VARCHAR(100) | NOT NULL | 股票名称 |
| market | VARCHAR(20) | NOT NULL | 市场标识A股: sh/sz/bj, 港股: hk, 美股: us等 |
| full_name | VARCHAR(200) | | 公司全称 |
| industry | VARCHAR(100) | | 所属行业 |
| listing_date | DATE | | 上市日期 |
| status | VARCHAR(20) | NOT NULL, DEFAULT 'active', CHECK | 状态active(正常)/suspended(停牌)/delisted(退市) |
| created_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 |
| updated_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 更新时间 |
| UNIQUE(stock_code, market) | | | 同一市场股票代码唯一 |
**创建语句**
```sql
CREATE TABLE stock_info (
id BIGSERIAL PRIMARY KEY,
stock_code VARCHAR(20) NOT NULL,
stock_name VARCHAR(100) NOT NULL,
market VARCHAR(20) NOT NULL,
full_name VARCHAR(200),
industry VARCHAR(100),
listing_date DATE,
status VARCHAR(20) NOT NULL DEFAULT 'active',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- 同一市场股票代码唯一
UNIQUE(stock_code, market),
CONSTRAINT check_stock_status CHECK (status IN ('active', 'suspended', 'delisted'))
);
-- 创建索引
CREATE INDEX idx_stock_info_code ON stock_info(stock_code);
CREATE INDEX idx_stock_info_market ON stock_info(market);
CREATE INDEX idx_stock_info_name ON stock_info(stock_name);
CREATE INDEX idx_stock_info_status ON stock_info(status);
CREATE INDEX idx_stock_info_code_market ON stock_info(stock_code, market);
-- 全文搜索索引(用于股票名称模糊匹配,需要先启用 pg_trgm 扩展)
-- CREATE EXTENSION IF NOT EXISTS pg_trgm;
-- CREATE INDEX idx_stock_info_name_trgm ON stock_info USING gin(stock_name gin_trgm_ops);
-- 创建触发器自动更新 updated_at
CREATE TRIGGER update_stock_info_updated_at
BEFORE UPDATE ON stock_info
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- 添加注释
COMMENT ON TABLE stock_info IS '股票基本信息表,存储所有市场的股票基本静态信息';
COMMENT ON COLUMN stock_info.stock_code IS '股票代码600519A股、00700.HK港股、AAPL美股';
COMMENT ON COLUMN stock_info.stock_name IS '股票名称,用于显示和搜索';
COMMENT ON COLUMN stock_info.market IS '市场标识sh(上海)/sz(深圳)/bj(北京)/hk(香港)/us(美国)等';
COMMENT ON COLUMN stock_info.status IS '状态active(正常交易)/suspended(停牌)/delisted(退市)';
```
**说明:**
- 此表存储所有市场的股票基本信息,用于用户输入时的模糊匹配
- 数据不常变更,可以定期从外部数据源同步
- 支持全文搜索,方便用户通过股票名称快速查找
---
### stock_daily_price 股票每日收盘价表
**表结构**
| 字段名 | 数据类型 | 约束 | 说明 |
|--------|---------|------|------|
| id | BIGSERIAL | PRIMARY KEY | 主键ID自增 |
| stock_code | VARCHAR(20) | NOT NULL | 股票代码 |
| market | VARCHAR(20) | NOT NULL | 市场标识 |
| trade_date | DATE | NOT NULL | 交易日期 |
| open_price | DECIMAL(18, 4) | | 开盘价 |
| close_price | DECIMAL(18, 4) | NOT NULL | 收盘价 |
| high_price | DECIMAL(18, 4) | | 最高价 |
| low_price | DECIMAL(18, 4) | | 最低价 |
| volume | BIGINT | | 成交量(单位:手) |
| amount | DECIMAL(20, 2) | | 成交额(单位:元) |
| change_amount | DECIMAL(18, 4) | | 涨跌额 |
| change_percent | DECIMAL(10, 6) | | 涨跌幅(% |
| turnover_rate | DECIMAL(10, 6) | | 换手率(% |
| pe_ratio | DECIMAL(12, 4) | | 市盈率 |
| pb_ratio | DECIMAL(12, 4) | | 市净率 |
| market_cap | DECIMAL(20, 2) | | 总市值(单位:元) |
| created_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 |
| UNIQUE(stock_code, market, trade_date) | | | 同一股票同一日期只能有一条记录 |
**创建语句**
```sql
CREATE TABLE stock_daily_price (
id BIGSERIAL PRIMARY KEY,
stock_code VARCHAR(20) NOT NULL,
market VARCHAR(20) NOT NULL,
trade_date DATE NOT NULL,
open_price DECIMAL(18, 4),
close_price DECIMAL(18, 4) NOT NULL,
high_price DECIMAL(18, 4),
low_price DECIMAL(18, 4),
volume BIGINT,
amount DECIMAL(20, 2),
change_amount DECIMAL(18, 4),
change_percent DECIMAL(10, 6),
turnover_rate DECIMAL(10, 6),
pe_ratio DECIMAL(12, 4),
pb_ratio DECIMAL(12, 4),
market_cap DECIMAL(20, 2),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- 同一股票同一日期只能有一条记录
UNIQUE(stock_code, market, trade_date),
-- 外键关联股票基本信息表(可选,如果数据源可靠可以不加)
CONSTRAINT fk_stock_price_info FOREIGN KEY (stock_code, market)
REFERENCES stock_info(stock_code, market) ON DELETE CASCADE
);
-- 创建索引
CREATE INDEX idx_stock_daily_price_code ON stock_daily_price(stock_code);
CREATE INDEX idx_stock_daily_price_market ON stock_daily_price(market);
CREATE INDEX idx_stock_daily_price_date ON stock_daily_price(trade_date);
CREATE INDEX idx_stock_daily_price_code_market ON stock_daily_price(stock_code, market);
CREATE INDEX idx_stock_daily_price_code_date ON stock_daily_price(stock_code, market, trade_date DESC);
-- 用于查询最新价格的索引
CREATE INDEX idx_stock_daily_price_latest ON stock_daily_price(stock_code, market, trade_date DESC);
-- 添加注释
COMMENT ON TABLE stock_daily_price IS '股票每日收盘价表,存储所有市场的股票每日收盘价及相关交易数据';
COMMENT ON COLUMN stock_daily_price.stock_code IS '股票代码,关联 stock_info 表';
COMMENT ON COLUMN stock_daily_price.close_price IS '收盘价,用于更新持仓的 current_price';
COMMENT ON COLUMN stock_daily_price.trade_date IS '交易日期,用于查询历史价格';
COMMENT ON COLUMN stock_daily_price.market_cap IS '总市值,单位:元';
```
**使用说明:**
1. **查询最新价格(用于更新持仓)**
```sql
-- 查询指定股票的最新收盘价
SELECT
stock_code,
market,
close_price,
trade_date
FROM stock_daily_price
WHERE stock_code = :stock_code
AND market = :market
ORDER BY trade_date DESC
LIMIT 1;
-- 批量查询多个股票的最新价格
SELECT DISTINCT ON (stock_code, market)
stock_code,
market,
close_price,
trade_date
FROM stock_daily_price
WHERE (stock_code, market) IN (
('600519', 'sh'),
('00700', 'hk'),
('AAPL', 'us')
)
ORDER BY stock_code, market, trade_date DESC;
```
2. **查询历史价格(用于图表展示)**
```sql
-- 查询指定股票的历史价格
SELECT
trade_date,
open_price,
close_price,
high_price,
low_price,
volume,
change_percent
FROM stock_daily_price
WHERE stock_code = :stock_code
AND market = :market
AND trade_date >= :start_date
ORDER BY trade_date;
```
3. **每日价格更新流程**
```sql
-- 步骤1从外部数据源获取最新价格数据
-- 步骤2批量插入或更新价格数据
INSERT INTO stock_daily_price (
stock_code, market, trade_date, open_price, close_price,
high_price, low_price, volume, amount, change_amount,
change_percent, turnover_rate, pe_ratio, pb_ratio, market_cap
)
VALUES (...)
ON CONFLICT (stock_code, market, trade_date)
DO UPDATE SET
open_price = EXCLUDED.open_price,
close_price = EXCLUDED.close_price,
high_price = EXCLUDED.high_price,
low_price = EXCLUDED.low_price,
volume = EXCLUDED.volume,
amount = EXCLUDED.amount,
change_amount = EXCLUDED.change_amount,
change_percent = EXCLUDED.change_percent,
turnover_rate = EXCLUDED.turnover_rate,
pe_ratio = EXCLUDED.pe_ratio,
pb_ratio = EXCLUDED.pb_ratio,
market_cap = EXCLUDED.market_cap;
-- 步骤3更新持仓表的 current_price仅更新 auto_price_update = true 的持仓)
UPDATE positions p
SET current_price = (
SELECT close_price
FROM stock_daily_price sdp
WHERE sdp.stock_code = p.symbol
AND sdp.market = p.market
ORDER BY sdp.trade_date DESC
LIMIT 1
),
updated_at = CURRENT_TIMESTAMP
WHERE p.auto_price_update = true
AND p.asset_type IN ('stock', 'fund')
AND p.status = 'active';
```
---
## 账户相关表设计
### positions 持仓表 ### positions 持仓表
记录用户持仓情况,包含数量、成本价、最新市场价、券商等 记录用户持仓情况,包含数量、成本价、最新市场价、券商等
**表结构** **表结构**
@@ -546,6 +455,7 @@ INSERT INTO brokers (broker_code, broker_name, region, sort_order) VALUES
| current_price | DECIMAL(18, 4) | | 最新市场价(系统自动更新) | | current_price | DECIMAL(18, 4) | | 最新市场价(系统自动更新) |
| currency | VARCHAR(10) | NOT NULL, DEFAULT 'CNY' | 货币类型 | | currency | VARCHAR(10) | NOT NULL, DEFAULT 'CNY' | 货币类型 |
| exchange_rate | DECIMAL(10, 6) | DEFAULT 1 | 汇率(用于多货币) | | exchange_rate | DECIMAL(10, 6) | DEFAULT 1 | 汇率(用于多货币) |
| auto_price_update | BOOLEAN | NOT NULL, DEFAULT false | 是否自动更新价格(付费用户功能) |
| status | VARCHAR(20) | NOT NULL, DEFAULT 'active', CHECK | 状态active/suspended/delisted | | status | VARCHAR(20) | NOT NULL, DEFAULT 'active', CHECK | 状态active/suspended/delisted |
| created_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 | | created_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 |
| updated_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 更新时间 | | updated_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 更新时间 |
@@ -566,6 +476,7 @@ CREATE TABLE positions (
current_price DECIMAL(18, 4), current_price DECIMAL(18, 4),
currency VARCHAR(10) NOT NULL DEFAULT 'CNY', currency VARCHAR(10) NOT NULL DEFAULT 'CNY',
exchange_rate DECIMAL(10, 6) DEFAULT 1, exchange_rate DECIMAL(10, 6) DEFAULT 1,
auto_price_update BOOLEAN NOT NULL DEFAULT false,
status VARCHAR(20) NOT NULL DEFAULT 'active', status VARCHAR(20) NOT NULL DEFAULT 'active',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
@@ -585,6 +496,8 @@ CREATE INDEX idx_positions_asset_type ON positions(asset_type);
CREATE INDEX idx_positions_status ON positions(status); CREATE INDEX idx_positions_status ON positions(status);
CREATE INDEX idx_positions_user_status ON positions(user_id, status); CREATE INDEX idx_positions_user_status ON positions(user_id, status);
CREATE INDEX idx_positions_user_broker ON positions(user_id, broker_id); CREATE INDEX idx_positions_user_broker ON positions(user_id, broker_id);
CREATE INDEX idx_positions_auto_price_update ON positions(auto_price_update);
CREATE INDEX idx_positions_auto_price_asset ON positions(auto_price_update, asset_type, status);
CREATE INDEX idx_positions_updated_at ON positions(updated_at); CREATE INDEX idx_positions_updated_at ON positions(updated_at);
-- 创建触发器自动更新 updated_at -- 创建触发器自动更新 updated_at
@@ -600,12 +513,258 @@ COMMENT ON COLUMN positions.asset_type IS '资产类型stock(股票)/fund(基
COMMENT ON COLUMN positions.symbol IS '资产代码如股票代码600519、基金代码等'; COMMENT ON COLUMN positions.symbol IS '资产代码如股票代码600519、基金代码等';
COMMENT ON COLUMN positions.shares IS '持仓份额/数量,股票为股数,基金为份数,现金为金额'; COMMENT ON COLUMN positions.shares IS '持仓份额/数量,股票为股数,基金为份数,现金为金额';
COMMENT ON COLUMN positions.cost_price IS '成本价,用户直接修改,系统不自动计算'; COMMENT ON COLUMN positions.cost_price IS '成本价,用户直接修改,系统不自动计算';
COMMENT ON COLUMN positions.current_price IS '最新市场价,系统每日自动更新'; COMMENT ON COLUMN positions.current_price IS '最新市场价,系统每日自动更新仅auto_price_update=true的持仓';
COMMENT ON COLUMN positions.exchange_rate IS '汇率,用于多货币资产,如港股、美股'; COMMENT ON COLUMN positions.exchange_rate IS '汇率,用于多货币资产,如港股、美股';
COMMENT ON COLUMN positions.auto_price_update IS '是否自动更新价格true表示系统每日自动更新市场价格付费用户功能';
``` ```
--- ---
### asset_snapshots (资产快照表)
**表结构(新版)**
| 字段名 | 数据类型 | 约束 | 说明 |
|---------------------|----------------|----------------------------------|---------------------------------------------------------|
| id | BIGSERIAL | PRIMARY KEY | 快照ID自增 |
| user_id | BIGINT | NOT NULL, FOREIGN KEY | 用户ID关联 users 表 |
| snapshot_date | DATE | NOT NULL | 快照日期 |
| total_asset | DECIMAL(18,2) | NOT NULL | 总资产(所有持仓市值 + 现金余额) |
| total_invested | DECIMAL(18,2) | NOT NULL | 累计投入金额(初始投入 + 后续投入 - 提取金额) |
| time_weighted_return| DECIMAL(12,8) | | 时间加权收益率(复利累计收益率) |
| annualized_return | DECIMAL(10,6) | | 年化收益率 |
| year_to_date_return | DECIMAL(10,6) | | 当年收益率(年初至今) |
| positions_data | JSONB | | 持仓明细快照JSON格式可选 |
| cash_data | JSONB | | 现金账户明细快照JSON格式可选 |
| created_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 |
| updated_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 更新时间 |
| UNIQUE(user_id, snapshot_date) | | | 同一用户同一日期只能有一条快照 |
**创建语句**
```sql
CREATE TABLE asset_snapshots (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
snapshot_date DATE NOT NULL,
total_asset DECIMAL(18,2) NOT NULL,
total_invested DECIMAL(18,2) NOT NULL,
time_weighted_return DECIMAL(12,8),
annualized_return DECIMAL(10,6),
year_to_date_return DECIMAL(10,6),
positions_data JSONB,
cash_data JSONB,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, snapshot_date)
);
```
-- 创建索引
```sql
CREATE INDEX idx_as_user_id ON asset_snapshots(user_id);
CREATE INDEX idx_as_snapshot_date ON asset_snapshots(snapshot_date);
CREATE INDEX idx_as_user_date ON asset_snapshots(user_id, snapshot_date DESC);
```
-- 添加注释
```sql
COMMENT ON TABLE asset_snapshots IS '每日资产快照表,记录总资产、累计投入、收益率等,用于计算净值、收益';
COMMENT ON COLUMN asset_snapshots.user_id IS '用户ID关联 users 表';
COMMENT ON COLUMN asset_snapshots.snapshot_date IS '快照日期';
COMMENT ON COLUMN asset_snapshots.total_asset IS '总资产:所有持仓市值 + 现金余额';
COMMENT ON COLUMN asset_snapshots.total_invested IS '累计投入金额:初始资金 + 后续投入 - 提取金额';
COMMENT ON COLUMN asset_snapshots.time_weighted_return IS '时间加权收益率(复利累计收益率)';
COMMENT ON COLUMN asset_snapshots.annualized_return IS '年化收益率';
COMMENT ON COLUMN asset_snapshots.year_to_date_return IS '当年收益率';
COMMENT ON COLUMN asset_snapshots.positions_data IS '持仓明细快照JSON格式存储';
COMMENT ON COLUMN asset_snapshots.cash_data IS '现金账户明细快照JSON格式存储';
COMMENT ON COLUMN asset_snapshots.created_at IS '快照创建时间';
COMMENT ON COLUMN asset_snapshots.updated_at IS '快照更新时间';
```
#### 字段计算说明与用法
**total_asset总资产**
```
total_asset = Σ(持仓市值) + 现金余额
其中:
- 持仓市值 = 持仓份额 × 当前价格
- 现金余额 = 所有现金账户余额之和
```
**total_invested累计投入金额**
```
total_invested = 初始投入 + 后续投入 - 提取金额
说明:
- 初始投入:账户创建时的初始资金
- 后续投入:用户手动记录的资金投入
- 提取金额:用户提取的资金(提现、转出)
- 注意:买入证券只是资金流转,非"投入"
```
**net_value单位净值及 total_profit总收益**
- 两者不再存储字段,需通过查询实时计算(避免冗余数据和不一致):
```sql
-- 单位净值(需要时动态计算)
net_value = total_asset / NULLIF(total_invested, 0)
-- 总收益(需要时动态计算)
total_profit = total_asset - total_invested
```
**时间加权收益率time_weighted_return逻辑**
```sql
-- 1. 计算每日净值
net_value_today = total_asset_today / NULLIF(total_invested_today, 0)
net_value_yesterday = total_asset_yesterday / NULLIF(total_invested_yesterday, 0)
-- 2. 日收益率
daily_return = (net_value_today - net_value_yesterday) / NULLIF(net_value_yesterday, 0)
-- 3. 累计收益率(复利)
new_twr = (1 + 昨日 time_weighted_return) × (1 + daily_return) - 1
```
**年化收益率annualized_return**
```sql
annualized_return = (1 + time_weighted_return) ^ (365 / 投资天数) - 1
```
**当年收益率year_to_date_return**
```sql
-- 获取年初快照,计算净值
year_to_date_return = (current_net_value - year_start_net_value) / year_start_net_value
```
#### 数据结构示例
**positions_data持仓明细快照**
```json
[
{
"position_id": 1,
"symbol": "600519",
"name": "贵州茅台",
"shares": 100,
"cost_price": 1600.00,
"current_price": 1850.00,
"market_value": 185000.00,
"profit": 25000.00
}
]
```
**cash_data现金账户明细快照**
```json
[
{
"account_id": 1,
"currency": "CNY",
"balance": 50000.00
}
]
```
#### 每日快照主要流程(伪代码)
```sql
1. 获取用户所有持仓
SELECT * FROM positions WHERE user_id = :user_id AND status = 'active';
2. 获取用户现金账户
SELECT * FROM cash_accounts WHERE user_id = :user_id;
3. 计算总资产
total_asset = Σ(持仓市值) + Σ(现金余额)
4. 汇总累计投入
total_invested =
SELECT COALESCE(SUM(amount),0) FROM cash_flows
WHERE user_id = :user_id AND flow_type = 'deposit'
- SELECT COALESCE(SUM(amount),0) FROM cash_flows
WHERE user_id = :user_id AND flow_type = 'withdraw'
5. 计算净值、总收益(不入库,仅查询时用)
6. 计算时间加权收益率
- 获取昨日快照
- 计算净值变化
- 复利累计
7. 计算年化和当年收益率
8. 组装positions_data, cash_dataJSONB
9. 插入或更新快照
INSERT INTO asset_snapshots (...)
ON CONFLICT (user_id, snapshot_date) DO UPDATE SET ...
```
#### 查询与统计示例
**查询用户净值曲线**
```sql
SELECT
snapshot_date,
total_asset,
total_invested,
(total_asset / NULLIF(total_invested, 0)) AS net_value,
(total_asset - total_invested) AS total_profit,
time_weighted_return,
annualized_return
FROM asset_snapshots
WHERE user_id = :user_id
ORDER BY snapshot_date DESC
LIMIT 365; -- 最近一年
```
**查询收益率统计**
```sql
SELECT
snapshot_date,
time_weighted_return * 100 as return_rate_percent,
annualized_return * 100 as annualized_return_percent,
year_to_date_return * 100 as ytd_return_percent
FROM asset_snapshots
WHERE user_id = :user_id
ORDER BY snapshot_date DESC
LIMIT 30;
```
**查询持仓明细历史**
```sql
SELECT
snapshot_date,
positions_data
FROM asset_snapshots
WHERE user_id = :user_id
AND positions_data IS NOT NULL
ORDER BY snapshot_date DESC
LIMIT 10;
```
#### 注意事项
1. **数据一致性**
- total_invested 必须大于 0否则净值计算会除零出错
- 使用 NULLIF 防止除零
2. **性能优化**
- 定时批量生成快照
- 索引user_id, snapshot_date
- JSONB 字段支持结构化、高效存储
3. **数据完整性**
- 每日有且仅有一条快照,断档时补最近快照
4. **净值及收益实时计算,不入库**,仅金额、收益率相关存库
5. **收益率需每日、每年维护和更新**
---
### position_price_plans 持仓价格计划表 ### position_price_plans 持仓价格计划表
**表结构** **表结构**
@@ -672,9 +831,9 @@ COMMENT ON COLUMN position_price_plans.step_order IS '步骤顺序默认3个
--- ---
## 持仓表使用说明 ### 持仓表使用说明
### 1. 资产类型说明 #### 1. 资产类型说明
**stock股票** **stock股票**
- symbol: 股票代码600519 - symbol: 股票代码600519
@@ -700,7 +859,7 @@ COMMENT ON COLUMN position_price_plans.step_order IS '步骤顺序默认3个
- shares: 债券数量 - shares: 债券数量
- cost_price: 每张成本价 - cost_price: 每张成本价
### 2. 价格计划使用示例 #### 2. 价格计划使用示例
**创建买入计划3个买点** **创建买入计划3个买点**
```sql ```sql
@@ -822,7 +981,7 @@ WHERE p.user_id = :user_id
AND p.status = 'active'; AND p.status = 'active';
``` ```
### 3. 持仓更新流程 #### 3. 持仓更新流程
**用户修改持仓(主动变更)** **用户修改持仓(主动变更)**
```sql ```sql
@@ -841,16 +1000,28 @@ WHERE position_id = :position_id
**系统更新市场价格(被动变更)** **系统更新市场价格(被动变更)**
```sql ```sql
-- 每日收盘后自动更新市场价格 -- 每日收盘后自动更新市场价格(仅更新启用自动更新的持仓)
UPDATE positions UPDATE positions
SET SET
current_price = :market_price, current_price = :market_price,
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE status = 'active' WHERE status = 'active'
AND auto_price_update = true
AND asset_type IN ('stock', 'fund', 'bond'); AND asset_type IN ('stock', 'fund', 'bond');
-- 查询需要自动更新价格的持仓(用于批量更新)
SELECT
p.position_id,
p.symbol,
p.market,
p.asset_type
FROM positions p
WHERE p.status = 'active'
AND p.auto_price_update = true
AND p.asset_type IN ('stock', 'fund', 'bond');
``` ```
### 4. 注意事项 #### 4. 注意事项
1. **唯一性约束** 1. **唯一性约束**
- 同一用户同一券商同一资产user_id + broker_id + symbol + market + asset_type只能有一条持仓 - 同一用户同一券商同一资产user_id + broker_id + symbol + market + asset_type只能有一条持仓
@@ -883,3 +1054,10 @@ WHERE status = 'active'
- 买入current_price <= plan_price - 买入current_price <= plan_price
- 卖出current_price >= plan_price - 卖出current_price >= plan_price
6. **自动价格更新功能**
- `auto_price_update` 字段控制是否启用自动价格更新
- 付费用户创建持仓时,自动设置为 true
- 用户订阅过期时,批量更新为 false
- 未付费用户创建持仓时,默认为 false
- 系统每日收盘后,仅更新 `auto_price_update = true` 的持仓价格

View File

@@ -0,0 +1,26 @@
# 整体产品设计思路
---
## 心智设计
### 应用名思考
- 投小记-记录、计划、复盘你的投资
投小记 - 专业投资记账助手,助您记录每一笔交易、制定投资计划、进行投资复盘,轻松管理股票、基金等资产收益。
### 页面Title设计
每个页面除了标题外,下边附带一句话
首页 - 买股票就是买公司
交易计划 - 计划你的交易,交易你的计划
复盘页 - 回顾过去是为了更好应对将来
## 页面设计原则
### 配置方案
主题色:`#8b5cf6`,紫色主题
### 设计原则
- 简洁又不失个性
- 体现 安静与思考 原则
-
## 想法
- 私密分享:可以将自己的交易计划和复盘,通过小程序私密分享-分享给其他人,这样即保障了裂变属性,有增加了隐私安全。

View File

@@ -0,0 +1,6 @@
# 计划模块-产品设计
## 二、交易计划模块
### 1. 创建计划
核心思想:计划应当和估值合并,计划依赖于估值。
1. 交易计划列表页: