File size: 15,443 Bytes
8639143
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
# StockReplay 项目技术文档

## 1. 项目概述

**StockReplay** 是一个基于 A 股历史行情的模拟交易复盘系统。用户可以对随机抽取的股票历史 K 线进行模拟交易,验证自己的盘感与交易策略,并在游戏结束后查看详细的交易统计与收益曲线。

### 1.1 核心功能
- **盲盒选股**:系统随机抽取一只符合条件的 A 股(上市满 3 年),隐藏股票名称和代码
- **模拟交易**:用户可以使用初始资金(100 万)进行买入、卖出操作
- **行情推演**:用户可以手动或自动播放 K 线,逐步揭示后续行情
- **复盘分析**:游戏结束后,展示总资产、年化收益、最大回撤、胜率等详细统计数据
- **多市场支持**:支持主板、创业板、科创板、北交所、ETF、LOF、REITs、可转债等多个市场

### 1.2 技术栈
- **前端**:Next.js 14 (App Router), React, TypeScript, Tailwind CSS, Zustand (状态管理), KLineCharts (图表库)
- **后端**:FastAPI (Python), DuckDB (嵌入式分析数据库), Akshare (金融数据接口)
- **数据存储**:Parquet 文件 (按月分区), Hugging Face Dataset (云端存储)
- **部署**:Hugging Face Spaces (Docker 容器)

---

## 2. 系统架构

### 2.1 整体架构图

```
┌──────────────────────────────────────────────────────────────┐
│                      Hugging Face Spaces                      │
│  ┌───────────────────────┐      ┌──────────────────────────┐ │
│  │   Frontend (Next.js)  │──────▶│   Backend (FastAPI)      │ │
│  │   - React Components  │◀──────│   - REST API Endpoints   │ │
│  │   - Zustand Store     │      │   - Core Game Logic      │ │
│  └───────────────────────┘      │   - Database Manager     │ │
│                                 └──────────────┬───────────┘ │
│                                                │              │
│                                                ▼              │
│                                 ┌──────────────────────────┐ │
│                                 │   DuckDB (In-Memory)     │ │
│                                 │   - stock_list (Table)   │ │
│                                 │   - stock_daily (View)   │ │
│                                 └──────────────┬───────────┘ │
│                                                │              │
└────────────────────────────────────────────────│──────────────┘


                                  ┌──────────────────────────────┐
                                  │   Hugging Face Dataset       │
                                  │   - data/stock_list.parquet  │
                                  │   - data/parquet/YYYY-MM.parquet │
                                  └──────────────────────────────┘
```

### 2.2 数据流
1. **数据同步**:本地运行 `sync_data.py`,从 Akshare 抓取数据,保存为 Parquet 文件,并上传至 HF Dataset
2. **应用启动**:HF Space 启动时,`DatabaseManager` 从 HF Dataset 下载 Parquet 文件,并在 DuckDB 中创建视图
3. **游戏开始**:前端调用 `/api/game/start`,后端从 `stock_list` 中随机筛选股票,查询 `stock_daily` 获取 K 线数据,返回给前端
4. **交易过程**:前端维护交易状态,后端仅提供数据查询服务

---

## 3. 核心模块详解

### 3.1 数据同步模块 (`backend/scripts/sync_data.py`)

#### 3.1.1 功能
负责从 Akshare 接口抓取全市场(A股、ETF、LOF、REITs、可转债)的列表和日线数据,并以 Parquet 格式存储。

#### 3.1.2 关键技术点

**增量同步**
- 查询 `stock_daily` 视图中每个标的的最新日期
- 如果最新日期等于最近一个交易日,则跳过
- 否则,只抓取缺失日期的数据

**多数据源适配**- A股:`ak.stock_zh_a_hist` (东方财富)
- ETF:`ak.fund_etf_hist_em` (东方财富)
- LOF:`ak.fund_lof_hist_em` (东方财富)
- REITs:`ak.reits_hist_em` (东方财富)
- 可转债:`ak.bond_zh_hs_cov_daily` (新浪)

**字段标准化**- 不同接口返回的字段名不同(如 `日期` vs `date``收盘` vs `close`- 使用 `rename_map` 统一转换为标准字段:`trade_date`, `open`, `high`, `low`, `close`, `volume`, `amount`, `pct_chg`, `turnover_rate`

**并发控制**- 使用 `ThreadPoolExecutor` 并发抓取
- `MAX_WORKERS` 设为 5,避免触发接口频率限制
- `max_retries` 设为 3,增强网络容错

#### 3.1.3 核心代码片段

```python
# 增量同步逻辑
existing_latest = db.conn.execute(
    "SELECT code, CAST(MAX(trade_date) AS VARCHAR) FROM stock_daily GROUP BY code"
).fetchall()
latest_map = {row[0]: row[1] for row in existing_latest}

pending = []
for t in targets:
    code = t['code']
    if code in latest_map:
        if latest_map[code] >= last_trade_day:
            continue
        start_dt = (pd.to_datetime(latest_map[code]) + timedelta(days=1)).strftime('%Y-%m-%d')
    else:
        start_dt = (datetime.now() - timedelta(days=YEARS_OF_DATA * 365)).strftime('%Y-%m-%d')
    t['start_dt'] = start_dt
    pending.append(t)
```

---

### 3.2 数据库管理模块 (`backend/app/database.py`)

#### 3.2.1 功能
管理 DuckDB 连接,处理本地与云端两种模式下的数据加载。

#### 3.2.2 关键技术点

**双模式支持**- **本地母本模式**:如果存在 `DUCKDB_PATH` 环境变量且文件存在,则直接连接本地 DuckDB 文件。适用于本地开发和全量数据初始化
- **云端无盘模式**:否则,从 HF Dataset 下载 Parquet 文件,并在内存中创建 DuckDB 视图。适用于 HF Spaces 部署

**视图创建与冲突解决**- 在创建 `stock_daily` 视图前,必须先删除可能存在的同名表或视图,避免 `Catalog Error`
- 代码:`conn.execute("DROP VIEW IF EXISTS stock_daily"); conn.execute("DROP TABLE IF EXISTS stock_daily")`

#### 3.2.3 核心代码片段

```python
# 云端模式:下载 Parquet 并创建视图
parquet_files = [f for f in all_files if f.startswith("data/parquet/") and f.endswith(".parquet")]
if parquet_files:
    local_paths = []
    for f in parquet_files:
        path = hf_hub_download(repo_id=DATASET_REPO_ID, filename=f, repo_type="dataset")
        local_paths.append(f"'{path}'")
    
    files_sql = ", ".join(local_paths)
    conn.execute("DROP VIEW IF EXISTS stock_daily")
    conn.execute("DROP TABLE IF EXISTS stock_daily")
    conn.execute(f"CREATE OR REPLACE VIEW stock_daily AS SELECT * FROM read_parquet([{files_sql}])")
```

---

### 3.3 游戏核心逻辑 (`backend/app/core.py`)

#### 3.3.1 功能
处理游戏开始、股票筛选、K线数据获取等核心业务逻辑。

#### 3.3.2 关键技术点

**股票筛选**- 上市满 3 年(`MIN_LISTING_YEARS = 3`- 必须存在日线数据:`EXISTS (SELECT 1 FROM stock_daily sd WHERE sd.code = sl.code)`

**K线截断**- 隐藏最后 100 个交易日的数据,作为"未来"行情
- 用户从第 `len(klines) - 100` 根 K 线开始交易

**市场分类筛选**- `全部`:所有符合条件的股票
- `全A股`:主板、创业板、科创板、北交所
- `基金`:ETF、LOF、REITs
- `ETF``LOF``REITs``可转债`:单独筛选

---

### 3.4 前端状态管理 (`frontend/src/store/gameStore.ts`)

#### 3.4.1 功能
使用 Zustand 管理全局游戏状态。

#### 3.4.2 状态结构

```typescript
interface GameState {
  isPlaying: boolean;      // 游戏是否进行中
  isRevealed: boolean;     // 是否已揭晓股票名称
  isFinished: boolean;     // 回测是否结束
  realCode: string;        // 股票代码
  realName: string;        // 股票名称
  allKlines: KLine[];      // 所有K线数据
  currentIndex: number;    // 当前K线索引
  cash: number;            // 可用现金
  holdings: number;        // 持仓数量
  history: Trade[];        // 交易历史
  // ... actions
}
```

#### 3.4.3 Actions

- `startGame(data)`:初始化游戏状态,加载 K 线数据
- `nextCandle()`:推进到下一根 K 线
- `buy(volume)`:执行买入操作
- `sell(volume)`:执行卖出操作
- `reveal()`:揭晓股票名称
- `finish()`:结束回测
- `reset()`:重置所有状态回到首页

---

### 3.5 前端组件 (`frontend/src/components/`)

#### 3.5.1 `TradePanel.tsx`

**功能**:交易面板,包含买入、卖出、下一天、自动播放、揭晓、重开等功能。

**关键逻辑**- **重开按钮**:点击后调用 `reset()`,将状态重置回首页,不自动开始新游戏
- **自动播放**:使用 `setInterval` 每秒调用一次 `nextCandle()`
- **仓位控制**:提供 1/4、1/2、3/4、全仓快捷按钮

**代码示例**```typescript
// 重开按钮(已优化)
<button onClick={() => { setAutoPlay(false); reset(); }}>
  <RotateCcw size={12} />
  <span>重开</span>
</button>
```

#### 3.5.2 `Chart.tsx`

**功能**:K线图表,使用 `klinecharts` 库。

**关键逻辑**- **数据加载**:监听 `currentIndex` 变化,更新图表显示范围
- **指标计算**:支持 MA, VOL, MACD 等指标
- **交易标记**:在图表上显示买入/卖出标记

#### 3.5.3 `StockHeader.tsx`

**功能**:顶部股票信息栏,显示当前股票名称、代码、价格等信息。

---

## 4. 关键问题与解决方案(基于历史对话)

### 4.1 ETF/LOF/REITs 数据缺失

**问题描述**:日志显示 `ETF: 1436 / 0`,列表中有数据但日线数据为空

**原因分析**1. 使用了不稳定或过时的 Akshare 接口(如 `fund_etf_category_sina`2. REITs 接口返回的字段名(`今开`, `最新价`)与预期不符,导致解析失败

**解决方案**1. 切换到稳定的东方财富接口:`fund_etf_spot_em`, `fund_etf_hist_em`, `reits_realtime_em`, `reits_hist_em`
2. 扩展 `rename_map`,增加 `今开` -> `open`, `最新价` -> `close` 的映射

---

### 4.2 DuckDB Table/View 冲突

**问题描述**`Catalog Error: Existing object stock_daily is of type Table, trying to replace with type View`

**原因分析**:本地 DuckDB 文件中 `stock_daily` 是一个表,但云端模式试图创建同名视图,DuckDB 不允许直接覆盖

**解决方案**:在创建视图前,先执行 `DROP VIEW IF EXISTS stock_daily``DROP TABLE IF EXISTS stock_daily`

---

### 4.3 数据同步网络超时

**问题描述**:大量 `Read timed out``ConnectionResetError`

**原因分析**:并发过高,触发数据源频率限制

**解决方案**1. 降低并发数:`MAX_WORKERS` 从 10 降为 5
2. 增加重试次数:`max_retries` 从 2 增为 3

---

### 4.4 可转债数据解析失败

**问题描述**`Failed to fetch sh110805 (可转债): 'date'`

**原因分析**:部分可转债代码格式或接口返回异常,缺少 `date`**解决方案**1. 增加代码格式处理:`cov_symbol = code[-6:] if len(code) > 6 else code`
2. 增强字段兼容性:尝试将索引转为列 `df.reset_index()`
3. 静默跳过无效数据,避免日志噪音

---

### 4.5 重开按钮逻辑优化

**需求**:点击"重开"或"再来一局"后,回到首页,不自动开始

**解决方案**:修改 `TradePanel.tsx` 中的按钮点击事件,移除 `handleStartGame()` 调用,仅调用 `reset()`

**修改前**```typescript
onClick={() => { reset(); handleStartGame(); }}
```

**修改后**```typescript
onClick={() => { setAutoPlay(false); reset(); }}
```

---

## 5. 部署与运维

### 5.1 环境变量

| 变量名 | 说明 | 示例 |
|--------|------|------|
| `HF_TOKEN` | Hugging Face Token | `hf_xxxxx` |
| `DATASET_REPO_ID` | HF Dataset 仓库 ID | `superxu520/Paper_Trading_Data` |
| `DUCKDB_PATH` | 本地 DuckDB 路径(可选) | `/app/data/stock_data.duckdb` |

### 5.2 数据同步流程

1. 本地配置 `HF_TOKEN``DATASET_REPO_ID`
2. 运行 `python backend/scripts/sync_data.py`
3. 脚本会自动抓取数据、保存为 Parquet、上传至 HF Dataset

### 5.3 Hugging Face Spaces 部署

- 项目根目录需包含 `Dockerfile`
- `Dockerfile` 中需设置环境变量、安装依赖、启动命令
- HF Space 启动后,会自动从 Dataset 下载最新数据

---

## 6. 未来优化方向

1. **数据源稳定性**:寻找更稳定的可转债数据源,或增加多源切换机制
2. **自动化同步**:使用 GitHub Actions 定时触发 `sync_data.py`,实现每日自动更新
3. **更多指标**:支持 BOLL, KDJ, RSI 等更多技术指标
4. **策略回测**:支持用户编写策略脚本进行自动化回测
5. **用户系统**:增加用户注册、登录,保存历史战绩
6. **社交分享**:优化分享卡片,支持更多社交平台

---

## 7. 项目目录结构

```
Paper_Trading/
├── backend/
│   ├── app/
│   │   ├── __init__.py
│   │   ├── core.py              # 游戏核心逻辑
│   │   ├── database.py          # DuckDB 数据库管理
│   │   └── main.py              # FastAPI 主入口
│   ├── scripts/
│   │   └── sync_data.py         # 数据同步脚本
│   └── data/                    # 本地数据目录
├── frontend/
│   ├── src/
│   │   ├── app/
│   │   │   ├── page.tsx         # 主页面
│   │   │   └── layout.tsx       # 布局组件
│   │   ├── components/
│   │   │   ├── TradePanel.tsx   # 交易面板
│   │   │   ├── Chart.tsx        # K线图表
│   │   │   └── StockHeader.tsx  # 股票信息栏
│   │   ├── store/
│   │   │   └── gameStore.ts     # Zustand 状态管理
│   │   └── lib/
│   │       ├── api.ts           # API 封装
│   │       └── resample.ts      # K线重采样
│   └── public/                  # 静态资源
├── Dockerfile                   # HF Spaces 部署配置
├── README.md                    # 项目说明
└── TECHNICAL_DOCUMENTATION.md   # 本文档
```

---

## 8. 参考资料

- [FastAPI 官方文档](https://fastapi.tiangolo.com/)
- [DuckDB 官方文档](https://duckdb.org/docs/)
- [Akshare 官方文档](https://akshare.akfamily.xyz/)
- [Next.js 官方文档](https://nextjs.org/docs)
- [Zustand 官方文档](https://docs.pmnd.rs/zustand)
- [Hugging Face Spaces 文档](https://huggingface.co/docs/hub/spaces)

---

*文档版本:2025.02.18*
*最后更新:优化重开按钮逻辑、修复 DuckDB 冲突、增强 ETF/LOF/REITs 数据同步*