superxuu commited on
Commit
8639143
·
1 Parent(s): d25ae45

添加项目技术文档:详细记录系统架构、核心模块、问题解决方案和未来优化方向

Browse files
Files changed (1) hide show
  1. TECHNICAL_DOCUMENTATION.md +393 -0
TECHNICAL_DOCUMENTATION.md ADDED
@@ -0,0 +1,393 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # StockReplay 项目技术文档
2
+
3
+ ## 1. 项目概述
4
+
5
+ **StockReplay** 是一个基于 A 股历史行情的模拟交易复盘系统。用户可以对随机抽取的股票历史 K 线进行模拟交易,验证自己的盘感与交易策略,并在游戏结束后查看详细的交易统计与收益曲线。
6
+
7
+ ### 1.1 核心功能
8
+ - **盲盒选股**:系统随机抽取一只符合条件的 A 股(上市满 3 年),隐藏股票名称和代码
9
+ - **模拟交易**:用户可以使用初始资金(100 万)进行买入、卖出操作
10
+ - **行情推演**:用户可以手动或自动播放 K 线,逐步揭示后续行情
11
+ - **复盘分析**:游戏结束后,展示总资产、年化收益、最大回撤、胜率等详细统计数据
12
+ - **多市场支持**:支持主板、创业板、科创板、北交所、ETF、LOF、REITs、可转债等多个市场
13
+
14
+ ### 1.2 技术栈
15
+ - **前端**:Next.js 14 (App Router), React, TypeScript, Tailwind CSS, Zustand (状态管理), KLineCharts (图表库)
16
+ - **后端**:FastAPI (Python), DuckDB (嵌入式分析数据库), Akshare (金融数据接口)
17
+ - **数据存储**:Parquet 文件 (按月分区), Hugging Face Dataset (云端存储)
18
+ - **部署**:Hugging Face Spaces (Docker 容器)
19
+
20
+ ---
21
+
22
+ ## 2. 系统架构
23
+
24
+ ### 2.1 整体架构图
25
+
26
+ ```
27
+ ┌──────────────────────────────────────────────────────────────┐
28
+ │ Hugging Face Spaces │
29
+ │ ┌───────────────────────┐ ┌──────────────────────────┐ │
30
+ │ │ Frontend (Next.js) │──────▶│ Backend (FastAPI) │ │
31
+ │ │ - React Components │◀──────│ - REST API Endpoints │ │
32
+ │ │ - Zustand Store │ │ - Core Game Logic │ │
33
+ │ └───────────────────────┘ │ - Database Manager │ │
34
+ │ └──────────────┬───────────┘ │
35
+ │ │ │
36
+ │ ▼ │
37
+ │ ┌──────────────────────────┐ │
38
+ │ │ DuckDB (In-Memory) │ │
39
+ │ │ - stock_list (Table) │ │
40
+ │ │ - stock_daily (View) │ │
41
+ │ └──────────────┬───────────┘ │
42
+ │ │ │
43
+ └────────────────────────────────────────────────│──────────────┘
44
+
45
+
46
+ ┌──────────────────────────────┐
47
+ │ Hugging Face Dataset │
48
+ │ - data/stock_list.parquet │
49
+ │ - data/parquet/YYYY-MM.parquet │
50
+ └──────────────────────────────┘
51
+ ```
52
+
53
+ ### 2.2 数据流
54
+ 1. **数据同步**:本地运行 `sync_data.py`,从 Akshare 抓取数据,保存为 Parquet 文件,并上传至 HF Dataset
55
+ 2. **应用启动**:HF Space 启动时,`DatabaseManager` 从 HF Dataset 下载 Parquet 文件,并在 DuckDB 中创建视图
56
+ 3. **游戏开始**:前端调用 `/api/game/start`,后端从 `stock_list` 中随机筛选股票,查询 `stock_daily` 获取 K 线数据,返回给前端
57
+ 4. **交易过程**:前端维护交易状态,后端仅提供数据查询服务
58
+
59
+ ---
60
+
61
+ ## 3. 核心模块详解
62
+
63
+ ### 3.1 数据同步模块 (`backend/scripts/sync_data.py`)
64
+
65
+ #### 3.1.1 功能
66
+ 负责从 Akshare 接口抓取全市场(A股、ETF、LOF、REITs、可转债)的列表和日线数据,并以 Parquet 格式存储。
67
+
68
+ #### 3.1.2 关键技术点
69
+
70
+ **增量同步**:
71
+ - 查询 `stock_daily` 视图中每个标的的最新日期
72
+ - 如果最新日期等于最近一个交易日,则跳过
73
+ - 否则,只抓取缺失日期的数据
74
+
75
+ **多数据源适配**:
76
+ - A股:`ak.stock_zh_a_hist` (东方财富)
77
+ - ETF:`ak.fund_etf_hist_em` (东方财富)
78
+ - LOF:`ak.fund_lof_hist_em` (东方财富)
79
+ - REITs:`ak.reits_hist_em` (东方财富)
80
+ - 可转债:`ak.bond_zh_hs_cov_daily` (新浪)
81
+
82
+ **字段标准化**:
83
+ - 不同接口返回的字段名不同(如 `日期` vs `date`,`收盘` vs `close`)
84
+ - 使用 `rename_map` 统一转换为标准字段:`trade_date`, `open`, `high`, `low`, `close`, `volume`, `amount`, `pct_chg`, `turnover_rate`
85
+
86
+ **并发控制**:
87
+ - 使用 `ThreadPoolExecutor` 并发抓取
88
+ - `MAX_WORKERS` 设为 5,避免触发接口频率限制
89
+ - `max_retries` 设为 3,增强网络容错
90
+
91
+ #### 3.1.3 核心代码片段
92
+
93
+ ```python
94
+ # 增量同步逻辑
95
+ existing_latest = db.conn.execute(
96
+ "SELECT code, CAST(MAX(trade_date) AS VARCHAR) FROM stock_daily GROUP BY code"
97
+ ).fetchall()
98
+ latest_map = {row[0]: row[1] for row in existing_latest}
99
+
100
+ pending = []
101
+ for t in targets:
102
+ code = t['code']
103
+ if code in latest_map:
104
+ if latest_map[code] >= last_trade_day:
105
+ continue
106
+ start_dt = (pd.to_datetime(latest_map[code]) + timedelta(days=1)).strftime('%Y-%m-%d')
107
+ else:
108
+ start_dt = (datetime.now() - timedelta(days=YEARS_OF_DATA * 365)).strftime('%Y-%m-%d')
109
+ t['start_dt'] = start_dt
110
+ pending.append(t)
111
+ ```
112
+
113
+ ---
114
+
115
+ ### 3.2 数据库管理模块 (`backend/app/database.py`)
116
+
117
+ #### 3.2.1 功能
118
+ 管理 DuckDB 连接,处理本地与云端两种模式下的数据加载。
119
+
120
+ #### 3.2.2 关键技术点
121
+
122
+ **双模式支持**:
123
+ - **本地母本模式**:如果存在 `DUCKDB_PATH` 环境变量且文件存在,则直接连接本地 DuckDB 文件。适用于本地开发和全量数据初始化
124
+ - **云端无盘模式**:否则,从 HF Dataset 下载 Parquet 文件,并在内存中创建 DuckDB 视图。适用于 HF Spaces 部署
125
+
126
+ **视图创建与冲突解决**:
127
+ - 在创建 `stock_daily` 视图前,必须先删除可能存在的同名表或视图,避免 `Catalog Error`
128
+ - 代码:`conn.execute("DROP VIEW IF EXISTS stock_daily"); conn.execute("DROP TABLE IF EXISTS stock_daily")`
129
+
130
+ #### 3.2.3 核心代码片段
131
+
132
+ ```python
133
+ # 云端模式:下载 Parquet 并创建视图
134
+ parquet_files = [f for f in all_files if f.startswith("data/parquet/") and f.endswith(".parquet")]
135
+ if parquet_files:
136
+ local_paths = []
137
+ for f in parquet_files:
138
+ path = hf_hub_download(repo_id=DATASET_REPO_ID, filename=f, repo_type="dataset")
139
+ local_paths.append(f"'{path}'")
140
+
141
+ files_sql = ", ".join(local_paths)
142
+ conn.execute("DROP VIEW IF EXISTS stock_daily")
143
+ conn.execute("DROP TABLE IF EXISTS stock_daily")
144
+ conn.execute(f"CREATE OR REPLACE VIEW stock_daily AS SELECT * FROM read_parquet([{files_sql}])")
145
+ ```
146
+
147
+ ---
148
+
149
+ ### 3.3 游戏核心逻辑 (`backend/app/core.py`)
150
+
151
+ #### 3.3.1 功能
152
+ 处理游戏开始、股票筛选、K线数据获取等核心业务逻辑。
153
+
154
+ #### 3.3.2 关键技术点
155
+
156
+ **股票筛选**:
157
+ - 上市满 3 年(`MIN_LISTING_YEARS = 3`)
158
+ - 必须存在日线数据:`EXISTS (SELECT 1 FROM stock_daily sd WHERE sd.code = sl.code)`
159
+
160
+ **K线截断**:
161
+ - 隐藏最后 100 个交易日的数据,作为"未来"行情
162
+ - 用户从第 `len(klines) - 100` 根 K 线开始交易
163
+
164
+ **市场分类筛选**:
165
+ - `全部`:所有符合条件的股票
166
+ - `全A股`:主板、创业板、科创板、北交所
167
+ - `基金`:ETF、LOF、REITs
168
+ - `ETF`、`LOF`、`REITs`、`可转债`:单独筛选
169
+
170
+ ---
171
+
172
+ ### 3.4 前端状态管理 (`frontend/src/store/gameStore.ts`)
173
+
174
+ #### 3.4.1 功能
175
+ 使用 Zustand 管理全局游戏状态。
176
+
177
+ #### 3.4.2 状态结构
178
+
179
+ ```typescript
180
+ interface GameState {
181
+ isPlaying: boolean; // 游戏是否进行中
182
+ isRevealed: boolean; // 是否已揭晓股票名称
183
+ isFinished: boolean; // 回测是否结束
184
+ realCode: string; // 股票代码
185
+ realName: string; // 股票名称
186
+ allKlines: KLine[]; // 所有K线数据
187
+ currentIndex: number; // 当前K线索引
188
+ cash: number; // 可用现金
189
+ holdings: number; // 持仓数量
190
+ history: Trade[]; // 交易历史
191
+ // ... actions
192
+ }
193
+ ```
194
+
195
+ #### 3.4.3 Actions
196
+
197
+ - `startGame(data)`:初始化游戏状态,加载 K 线数据
198
+ - `nextCandle()`:推进到下一根 K 线
199
+ - `buy(volume)`:执行买入操作
200
+ - `sell(volume)`:执行卖出操作
201
+ - `reveal()`:揭晓股票名称
202
+ - `finish()`:结束回测
203
+ - `reset()`:重置所有状态回到首页
204
+
205
+ ---
206
+
207
+ ### 3.5 前端组件 (`frontend/src/components/`)
208
+
209
+ #### 3.5.1 `TradePanel.tsx`
210
+
211
+ **功能**:交易面板,包含买入、卖出、下一天、自动播放、揭晓、重开等功能。
212
+
213
+ **关键逻辑**:
214
+ - **重开按钮**:点击后调用 `reset()`,将状态重置回首页,不自动开始新游戏
215
+ - **自动播放**:使用 `setInterval` 每秒调用一次 `nextCandle()`
216
+ - **仓位控制**:提供 1/4、1/2、3/4、全仓快捷按钮
217
+
218
+ **代码示例**:
219
+ ```typescript
220
+ // 重开按钮(已优化)
221
+ <button onClick={() => { setAutoPlay(false); reset(); }}>
222
+ <RotateCcw size={12} />
223
+ <span>重开</span>
224
+ </button>
225
+ ```
226
+
227
+ #### 3.5.2 `Chart.tsx`
228
+
229
+ **功能**:K线图表,使用 `klinecharts` 库。
230
+
231
+ **关键逻辑**:
232
+ - **数据加载**:监听 `currentIndex` 变化,更新图表显示范围
233
+ - **指标计算**:支持 MA, VOL, MACD 等指标
234
+ - **交易标记**:在图表上显示买入/卖出标记
235
+
236
+ #### 3.5.3 `StockHeader.tsx`
237
+
238
+ **功能**:顶部股票信息栏,显示当前股票名称、代码、价格等信息。
239
+
240
+ ---
241
+
242
+ ## 4. 关键问题与解决方案(基于历史对话)
243
+
244
+ ### 4.1 ETF/LOF/REITs 数据缺失
245
+
246
+ **问题描述**:日志显示 `ETF: 1436 / 0`,列表中有数据但日线数据为空
247
+
248
+ **原因分析**:
249
+ 1. 使用了不稳定或过时的 Akshare 接口(如 `fund_etf_category_sina`)
250
+ 2. REITs 接口返回的字段名(`今开`, `最新价`)与预期不符,导致解析失败
251
+
252
+ **解决方案**:
253
+ 1. 切换到稳定的东方财富接口:`fund_etf_spot_em`, `fund_etf_hist_em`, `reits_realtime_em`, `reits_hist_em`
254
+ 2. 扩展 `rename_map`,增加 `今开` -> `open`, `最新价` -> `close` 的映射
255
+
256
+ ---
257
+
258
+ ### 4.2 DuckDB Table/View 冲突
259
+
260
+ **问题描述**:`Catalog Error: Existing object stock_daily is of type Table, trying to replace with type View`
261
+
262
+ **原因分析**:本地 DuckDB 文件中 `stock_daily` 是一个表,但云端模式试图创建同名视图,DuckDB 不允许直接覆盖
263
+
264
+ **解决方案**:在创建视图前,先执行 `DROP VIEW IF EXISTS stock_daily` 和 `DROP TABLE IF EXISTS stock_daily`
265
+
266
+ ---
267
+
268
+ ### 4.3 数据同步网络超时
269
+
270
+ **问题描述**:大量 `Read timed out` 和 `ConnectionResetError`
271
+
272
+ **原因分析**:并发过高,触发数据源频率限制
273
+
274
+ **解决方案**:
275
+ 1. 降低并发数:`MAX_WORKERS` 从 10 降为 5
276
+ 2. 增加重试次数:`max_retries` 从 2 增为 3
277
+
278
+ ---
279
+
280
+ ### 4.4 可转债数据解析失败
281
+
282
+ **问题描述**:`Failed to fetch sh110805 (可转债): 'date'`
283
+
284
+ **原因分析**:部分可转债代码格式或接口返回异常,缺少 `date` 列
285
+
286
+ **解决方案**:
287
+ 1. 增加代码格式处理:`cov_symbol = code[-6:] if len(code) > 6 else code`
288
+ 2. 增强字段兼容性:尝试将索引转为列 `df.reset_index()`
289
+ 3. 静默跳过无效数据,避免日志噪音
290
+
291
+ ---
292
+
293
+ ### 4.5 重开按钮逻辑优化
294
+
295
+ **需求**:点击"重开"或"再来一局"后,回到首页,不自动开始
296
+
297
+ **解决方案**:修改 `TradePanel.tsx` 中的按钮点击事件,移除 `handleStartGame()` 调用,仅调用 `reset()`
298
+
299
+ **修改前**:
300
+ ```typescript
301
+ onClick={() => { reset(); handleStartGame(); }}
302
+ ```
303
+
304
+ **修改后**:
305
+ ```typescript
306
+ onClick={() => { setAutoPlay(false); reset(); }}
307
+ ```
308
+
309
+ ---
310
+
311
+ ## 5. 部署与运维
312
+
313
+ ### 5.1 环境变量
314
+
315
+ | 变量名 | 说明 | 示例 |
316
+ |--------|------|------|
317
+ | `HF_TOKEN` | Hugging Face Token | `hf_xxxxx` |
318
+ | `DATASET_REPO_ID` | HF Dataset 仓库 ID | `superxu520/Paper_Trading_Data` |
319
+ | `DUCKDB_PATH` | 本地 DuckDB 路径(可选) | `/app/data/stock_data.duckdb` |
320
+
321
+ ### 5.2 数据同步流程
322
+
323
+ 1. 本地配置 `HF_TOKEN` 和 `DATASET_REPO_ID`
324
+ 2. 运行 `python backend/scripts/sync_data.py`
325
+ 3. 脚本会自动抓取数据、保存为 Parquet、上传至 HF Dataset
326
+
327
+ ### 5.3 Hugging Face Spaces 部署
328
+
329
+ - 项目根目录需包含 `Dockerfile`
330
+ - `Dockerfile` 中需设置环境变量、安装依赖、启动命令
331
+ - HF Space 启动后,会自动从 Dataset 下载最新数据
332
+
333
+ ---
334
+
335
+ ## 6. 未来优化方向
336
+
337
+ 1. **数据源稳定性**:寻找更稳定的可转债数据源,或增加多源切换机制
338
+ 2. **自动化同步**:使用 GitHub Actions 定时触发 `sync_data.py`,实现每日自动更新
339
+ 3. **更多指标**:支持 BOLL, KDJ, RSI 等更多技术指标
340
+ 4. **策略回测**:支持用户编写策略脚本进行自动化回测
341
+ 5. **用户系统**:增加用户注册、登录,保存历史战绩
342
+ 6. **社交分享**:优化分享卡片,支持更多社交平台
343
+
344
+ ---
345
+
346
+ ## 7. 项目目录结构
347
+
348
+ ```
349
+ Paper_Trading/
350
+ ├── backend/
351
+ │ ├── app/
352
+ │ │ ├── __init__.py
353
+ │ │ ├── core.py # 游戏核心逻辑
354
+ │ │ ├── database.py # DuckDB 数据库管理
355
+ │ │ └── main.py # FastAPI 主入口
356
+ │ ├── scripts/
357
+ │ │ └── sync_data.py # 数据同步脚本
358
+ │ └── data/ # 本地数据目录
359
+ ├── frontend/
360
+ │ ├── src/
361
+ │ │ ├── app/
362
+ │ │ │ ├── page.tsx # 主页面
363
+ │ │ │ └── layout.tsx # 布局组件
364
+ │ │ ├── components/
365
+ │ │ │ ├── TradePanel.tsx # 交易面板
366
+ │ │ │ ├── Chart.tsx # K线图表
367
+ │ │ │ └── StockHeader.tsx # 股票信息栏
368
+ │ │ ├── store/
369
+ │ │ │ └── gameStore.ts # Zustand 状态管理
370
+ │ │ └── lib/
371
+ │ │ ├── api.ts # API 封装
372
+ │ │ └── resample.ts # K线重采样
373
+ │ └── public/ # 静态资源
374
+ ├── Dockerfile # HF Spaces 部署配置
375
+ ├── README.md # 项目说明
376
+ └── TECHNICAL_DOCUMENTATION.md # 本文档
377
+ ```
378
+
379
+ ---
380
+
381
+ ## 8. 参��资料
382
+
383
+ - [FastAPI 官方文档](https://fastapi.tiangolo.com/)
384
+ - [DuckDB 官方文档](https://duckdb.org/docs/)
385
+ - [Akshare 官方文档](https://akshare.akfamily.xyz/)
386
+ - [Next.js 官方文档](https://nextjs.org/docs)
387
+ - [Zustand 官方文档](https://docs.pmnd.rs/zustand)
388
+ - [Hugging Face Spaces 文档](https://huggingface.co/docs/hub/spaces)
389
+
390
+ ---
391
+
392
+ *文档版本:2025.02.18*
393
+ *最后更新:优化重开按钮逻辑、修复 DuckDB 冲突、增强 ETF/LOF/REITs 数据同步*