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

View File

@@ -7,10 +7,15 @@
用户相关
├── users (用户表)
持仓相关
基础数据相关
├── brokers (券商表)
├── stock_info (股票基本信息表)
└── stock_daily_price (股票每日收盘价表)
账户相关
├── 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 券商表
@@ -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 持仓表
记录用户持仓情况,包含数量、成本价、最新市场价、券商等
**表结构**
@@ -546,6 +455,7 @@ INSERT INTO brokers (broker_code, broker_name, region, sort_order) VALUES
| current_price | DECIMAL(18, 4) | | 最新市场价(系统自动更新) |
| currency | VARCHAR(10) | NOT NULL, DEFAULT 'CNY' | 货币类型 |
| 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 |
| created_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),
currency VARCHAR(10) NOT NULL DEFAULT 'CNY',
exchange_rate DECIMAL(10, 6) DEFAULT 1,
auto_price_update BOOLEAN NOT NULL DEFAULT false,
status VARCHAR(20) NOT NULL DEFAULT 'active',
created_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_user_status ON positions(user_id, status);
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);
-- 创建触发器自动更新 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.shares 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.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 持仓价格计划表
**表结构**
@@ -672,9 +831,9 @@ COMMENT ON COLUMN position_price_plans.step_order IS '步骤顺序默认3个
---
## 持仓表使用说明
### 持仓表使用说明
### 1. 资产类型说明
#### 1. 资产类型说明
**stock股票**
- symbol: 股票代码600519
@@ -700,7 +859,7 @@ COMMENT ON COLUMN position_price_plans.step_order IS '步骤顺序默认3个
- shares: 债券数量
- cost_price: 每张成本价
### 2. 价格计划使用示例
#### 2. 价格计划使用示例
**创建买入计划3个买点**
```sql
@@ -822,7 +981,7 @@ WHERE p.user_id = :user_id
AND p.status = 'active';
```
### 3. 持仓更新流程
#### 3. 持仓更新流程
**用户修改持仓(主动变更)**
```sql
@@ -841,16 +1000,28 @@ WHERE position_id = :position_id
**系统更新市场价格(被动变更)**
```sql
-- 每日收盘后自动更新市场价格
-- 每日收盘后自动更新市场价格(仅更新启用自动更新的持仓)
UPDATE positions
SET
current_price = :market_price,
updated_at = CURRENT_TIMESTAMP
WHERE status = 'active'
AND auto_price_update = true
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. **唯一性约束**
- 同一用户同一券商同一资产user_id + broker_id + symbol + market + asset_type只能有一条持仓
@@ -883,3 +1054,10 @@ WHERE status = 'active'
- 买入current_price <= plan_price
- 卖出current_price >= plan_price
6. **自动价格更新功能**
- `auto_price_update` 字段控制是否启用自动价格更新
- 付费用户创建持仓时,自动设置为 true
- 用户订阅过期时,批量更新为 false
- 未付费用户创建持仓时,默认为 false
- 系统每日收盘后,仅更新 `auto_price_update = true` 的持仓价格