Initial commit: 可转债轮动分析系统

- 实时行情:东财push2主源 + 腾讯批量报价备用源(自动切换)
- 历史K线:东财push2his主源 + 腾讯web子域备用源(含熔断机制)
- 增量同步:支持 -incr/-since 参数按存档日期增量入库
- 策略回测:净值曲线/10指标/基准对比,含基本面过滤
- 用户认证:用户名密码 + 邮箱验证码双登录方式
- 前端:三Tab SPA(实时行情/策略回测/数据管理)
This commit is contained in:
2026-08-17 20:23:22 +08:00
commit e28bfec957
45 changed files with 7889 additions and 0 deletions

102
cmd/fundamentals/main.go Normal file
View File

@@ -0,0 +1,102 @@
// fundamentals 是正股基本面数据入库命令:
//
// go run ./cmd/fundamentals [-db 库路径]
//
// 从 convert_bond 表读取所有正股代码,通过东财 clist 接口批量拉取
// 基本面数据(ROE/资产负债率/净利润/ST状态),写入 stock_fundamental 表。
package main
import (
"context"
"flag"
"fmt"
"os"
"path/filepath"
"time"
"stock-analysis/internal/datasource/eastmoney"
"stock-analysis/internal/storage/sqlite"
)
func main() {
dbPath := flag.String("db", defaultDBPath(), "SQLite 数据库文件路径")
flag.Parse()
store, err := sqlite.Open(*dbPath)
if err != nil {
fatal("打开数据库失败: ", err)
}
defer store.Close()
ctx := context.Background()
// 1. 获取所有正股代码。
stock2bond, err := store.GetStockBondMap(ctx)
if err != nil {
fatal("获取正股映射失败: ", err)
}
if len(stock2bond) == 0 {
fmt.Println("convert_bond 表为空,请先执行 go run ./cmd/ingest -all 入库转债数据")
return
}
stockCodes := make([]string, 0, len(stock2bond))
for code := range stock2bond {
stockCodes = append(stockCodes, code)
}
fmt.Printf("正股数量:%d\n", len(stockCodes))
// 2. 分批拉取基本面数据(每批最多 200 只)。
client := eastmoney.NewClient()
const batchSize = 200
var totalSaved, totalFail int
for i := 0; i < len(stockCodes); i += batchSize {
end := i + batchSize
if end > len(stockCodes) {
end = len(stockCodes)
}
batch := stockCodes[i:end]
funds, err := client.FetchStockFundamentals(ctx, batch)
if err != nil {
fmt.Printf("批次 %d-%d 拉取失败: %v\n", i, end-1, err)
totalFail += len(batch)
continue
}
for _, f := range funds {
f.UpdateDate = time.Now().Format("2006-01-02")
if err := store.SaveStockFundamental(ctx, f); err != nil {
fmt.Printf(" %s %s: 保存失败 %v\n", f.Code, f.Name, err)
totalFail++
continue
}
totalSaved++
}
fmt.Printf("批次 %d-%d: 拉取 %d 只, 已保存 %d\n", i+1, end, len(batch), totalSaved)
// 控制请求频率。
if end < len(stockCodes) {
time.Sleep(2 * time.Second)
}
}
fmt.Printf("\n基本面入库完成:成功 %d 只,失败 %d 只(库: %s)\n", totalSaved, totalFail, *dbPath)
}
// defaultDBPath 返回默认数据库路径。
func defaultDBPath() string {
local := os.Getenv("LOCALAPPDATA")
if local == "" {
local = os.TempDir()
}
return filepath.Join(local, "stock-analysis", "stock.db")
}
// fatal 打印错误并退出。
func fatal(msg string, err error) {
fmt.Fprintln(os.Stderr, msg, err)
os.Exit(1)
}