基于go中fyne gui的通達信數(shù)據(jù)導出工具詳解
更新時間:2024年12月12日 10:25:32 作者:布林模型
這篇文章主要介紹了基于go中fyne gui的通達信數(shù)據(jù)導出工具,這是一個用 Go 語言開發(fā)的通達信數(shù)據(jù)導出工具,可以將通達信的本地數(shù)據(jù)導出為多種格式,方便用戶進行數(shù)據(jù)分析和處理,需要的朋友可以參考下

這是一個用 Go 語言開發(fā)的通達信數(shù)據(jù)導出工具,可以將通達信的本地數(shù)據(jù)導出為多種格式,方便用戶進行數(shù)據(jù)分析和處理。
主要功能
- 支持多種數(shù)據(jù)類型導出:
- 日線數(shù)據(jù)
- 5分鐘線數(shù)據(jù)
- 1分鐘線數(shù)據(jù)
- 支持多種導出格式:
- CSV 格式
- SQLite 數(shù)據(jù)庫
- Excel 文件
- Postgres數(shù)據(jù)庫連接導出
特點
- 圖形化界面,操作簡單直觀
- 支持增量導出,避免重復處理
- 可配置數(shù)據(jù)源和導出路徑
- 實時顯示處理進度
- 支持批量處理大量數(shù)據(jù)
使用說明
- 設置通達信數(shù)據(jù)路徑
- 選擇要導出的數(shù)據(jù)類型
- 選擇導出格式
- 點擊導出按鈕開始處理
技術棧
- Go 語言開發(fā)
- Fyne GUI 框架
- SQLite/CSV/Excel/postgres 數(shù)據(jù)處理
配置要求
- 需要本地安裝通達信軟件
- 需要有通達信的歷史數(shù)據(jù)
注意事項
- 首次使用需要配置通達信數(shù)據(jù)路徑
- 建議定期備份導出的數(shù)據(jù)
- 大量數(shù)據(jù)導出可能需要較長時間

所有測試工作都在在mac中進行,源碼可以在多平臺運行.程序目錄結構

main.go
package main
import (
"fmt"
"os"
"path/filepath"
"strconv"
"tdx_exporter/config"
"tdx_exporter/tdx"
"time"
_ "github.com/lib/pq"
"image/color"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
var (
dayGroup *widget.CheckGroup
minGroup *widget.CheckGroup
)
// 創(chuàng)建自定義主題
type myTheme struct {
fyne.Theme
}
func (m myTheme) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color {
if name == theme.ColorNameForeground {
return color.NRGBA{R: 0, G: 0, B: 0, A: 255} // 黑色
}
return theme.DefaultTheme().Color(name, variant)
}
func main() {
// 設置中文編碼
os.Setenv("LANG", "zh_CN.UTF-8")
myApp := app.New()
// 設置中文字體
myApp.Settings().SetTheme(&myTheme{theme.DefaultTheme()})
myWindow := myApp.NewWindow("通達信數(shù)據(jù)工具")
// 創(chuàng)建數(shù)據(jù)類型選擇容器,分組顯示
dayGroup = widget.NewCheckGroup([]string{"日據(jù)"}, nil)
dayGroup.SetSelected([]string{"日線數(shù)據(jù)"})
minGroup = widget.NewCheckGroup([]string{"1分鐘線", "5分鐘線"}, nil)
// 然后創(chuàng)建格式選擇
formatSelect := widget.NewSelect([]string{"Postgres", "CSV", "SQLite", "Excel"}, func(value string) {
if value == "SQLite" || value == "Postgres" {
// 默認選中所有選項,但保持可選狀態(tài)
if len(dayGroup.Selected) == 0 {
dayGroup.SetSelected([]string{"日線數(shù)據(jù)"})
}
if len(minGroup.Selected) == 0 {
minGroup.SetSelected([]string{"1分鐘線", "5分鐘線"})
}
// 移除禁用代碼,保持控件可選
dayGroup.Enable()
minGroup.Enable()
} else {
// CSV或Excel格式時保持原有邏輯
dayGroup.Enable()
minGroup.Enable()
}
})
formatSelect.SetSelected("Postgres")
// 創(chuàng)建結果顯示區(qū)域 - 修改為更大的顯示區(qū)域
resultArea := widget.NewMultiLineEntry()
resultArea.Disable()
resultArea.SetPlaceHolder("導出結果將在這里顯示")
resultArea.Resize(fyne.NewSize(580, 300)) // 設置更大的尺寸
resultArea.SetMinRowsVisible(15) // 顯示更多行
// 創(chuàng)建按鈕
settingsBtn := widget.NewButtonWithIcon("設置", theme.SettingsIcon(), func() {
showSettingsDialog(myWindow)
})
var exportBtn, updateBtn *widget.Button
// 創(chuàng)建左側布局
leftPanel := container.NewVBox(
widget.NewLabel("導出格式:"),
formatSelect,
widget.NewSeparator(), // 添加隔線
widget.NewLabel("數(shù)據(jù)類型選擇:"),
dayGroup,
minGroup,
)
exportBtn = widget.NewButtonWithIcon("導出", theme.DocumentSaveIcon(), func() {
exportBtn.Disable()
settingsBtn.Disable()
formatSelect.Disable()
startExport(myWindow, formatSelect.Selected, func() {
exportBtn.Enable()
settingsBtn.Enable()
formatSelect.Enable()
})
})
updateBtn = widget.NewButtonWithIcon("更新", theme.ViewRefreshIcon(), func() {
updateBtn.Disable()
settingsBtn.Disable()
exportBtn.Disable()
formatSelect.Disable()
startUpdate(myWindow, func() {
updateBtn.Enable()
settingsBtn.Enable()
exportBtn.Enable()
formatSelect.Enable()
})
})
// 創(chuàng)建按鈕布局
buttons := container.NewHBox(
layout.NewSpacer(), // 添加彈性空間使按鈕居中
settingsBtn,
exportBtn,
updateBtn,
layout.NewSpacer(),
)
// 創(chuàng)建 memo 控件
memo := widget.NewMultiLineEntry()
memo.Disable() // 設置為只讀
memo.SetPlaceHolder("提示信息將在這里顯示")
memo.SetMinRowsVisible(5) // 設置最小顯示行數(shù)
memo.SetText("歡迎使用通達信數(shù)據(jù)工具\n請選擇要導出的數(shù)據(jù)類型和格式")
// 創(chuàng)建主布局
content := container.NewBorder(
buttons,
nil,
container.NewPadded(leftPanel),
nil,
memo,
)
myWindow.SetContent(content)
myWindow.Resize(fyne.NewSize(800, 400))
myWindow.ShowAndRun()
}
func showSettingsDialog(window fyne.Window) {
settings, err := config.LoadSettings()
if err != nil {
dialog.ShowError(err, window)
return
}
// 基本設置頁面
tdxPath := widget.NewEntry()
tdxPath.SetText(settings.TdxPath)
tdxPath.SetPlaceHolder("請輸入通達信數(shù)據(jù)目路徑")
exportPath := widget.NewEntry()
exportPath.SetText(settings.ExportPath)
exportPath.SetPlaceHolder("請輸入導出數(shù)據(jù)保存路徑")
// 數(shù)據(jù)庫設置頁面
dbHost := widget.NewEntry()
dbHost.SetText(settings.DBConfig.Host)
dbHost.SetPlaceHolder("數(shù)據(jù)庫主機地址")
dbPort := widget.NewEntry()
dbPort.SetText(fmt.Sprintf("%d", settings.DBConfig.Port))
dbPort.SetPlaceHolder("端口號")
dbUser := widget.NewEntry()
dbUser.SetText(settings.DBConfig.User)
dbUser.SetPlaceHolder("用戶名")
dbPassword := widget.NewPasswordEntry()
dbPassword.SetText(settings.DBConfig.Password)
dbPassword.SetPlaceHolder("密碼")
dbName := widget.NewEntry()
dbName.SetText(settings.DBConfig.DBName)
dbName.SetPlaceHolder("數(shù)據(jù)庫名")
testConnBtn := widget.NewButton("測試連接", func() {
// ... 測試連接代碼 ...
})
// 修改數(shù)據(jù)庫設置頁面布局
dbSettings := container.NewVBox(
// 添加測試連接按鈕到頂部
container.NewHBox(
layout.NewSpacer(),
testConnBtn,
layout.NewSpacer(),
),
widget.NewSeparator(), // 分隔線
container.NewGridWithColumns(2,
container.NewVBox(
widget.NewLabel("主機地址:"),
dbHost,
widget.NewLabel("用戶名:"),
dbUser,
widget.NewLabel("數(shù)據(jù)庫名:"),
dbName,
),
container.NewVBox(
widget.NewLabel("端口號:"),
dbPort,
widget.NewLabel("密碼:"),
dbPassword,
),
),
)
// 修改基本設置頁面布局
basicSettings := container.NewVBox(
widget.NewLabel("通達信數(shù)據(jù)路徑:"),
container.NewPadded(tdxPath),
widget.NewSeparator(),
widget.NewLabel("導出數(shù)據(jù)保存路徑:"),
container.NewPadded(exportPath),
)
// 創(chuàng)建標簽頁
tabs := container.NewAppTabs(
container.NewTabItem("基本設置", container.NewPadded(basicSettings)),
container.NewTabItem("數(shù)據(jù)庫設置", container.NewPadded(dbSettings)),
)
tabs.SetTabLocation(container.TabLocationTop)
dialog := dialog.NewCustomConfirm(
"參數(shù)設置",
"確定",
"取消",
tabs,
func(ok bool) {
if !ok {
return
}
port, _ := strconv.Atoi(dbPort.Text)
newSettings := &config.Settings{
TdxPath: tdxPath.Text,
ExportPath: exportPath.Text,
DBConfig: config.DBConfig{
Host: dbHost.Text,
Port: port,
User: dbUser.Text,
Password: dbPassword.Text,
DBName: dbName.Text,
},
ExportPaths: settings.ExportPaths,
}
if err := config.SaveSettings(newSettings); err != nil {
dialog.ShowError(err, window)
return
}
dialog.ShowInformation("成功", "設置已保存", window)
},
window,
)
// 設置對話框大小
dialog.Resize(fyne.NewSize(500, 400))
dialog.Show()
}
// 修改 startExport 函數(shù)
func startExport(window fyne.Window, format string, onComplete func()) {
settings, err := config.LoadSettings()
if err != nil {
dialog.ShowError(err, window)
onComplete()
return
}
if settings.TdxPath == "" {
dialog.ShowError(fmt.Errorf("請先在參數(shù)設置中設置通達信數(shù)據(jù)路徑"), window)
onComplete()
return
}
go func() {
processor := tdx.NewDataProcessor(settings.TdxPath)
lastExportInfo, hasLastExport := settings.GetLastExportInfo(format)
exportOpts := tdx.ExportOptions{
IsIncremental: hasLastExport,
LastExportTime: lastExportInfo.LastTime,
DataTypes: tdx.DataTypes{
Day: contains(dayGroup.Selected, "日線數(shù)據(jù)"),
Min1: contains(minGroup.Selected, "1分鐘線"),
Min5: contains(minGroup.Selected, "5分鐘線"),
},
TargetDir: settings.ExportPath,
}
var exportErr error
switch format {
case "Postgres":
dbConfig := tdx.DBConfig{
Host: settings.DBConfig.Host,
Port: settings.DBConfig.Port,
User: settings.DBConfig.User,
Password: settings.DBConfig.Password,
DBName: settings.DBConfig.DBName,
}
exportErr = processor.ExportToPostgres(dbConfig, exportOpts)
case "CSV":
exportErr = processor.ExportToCSV(settings.ExportPath, exportOpts)
case "SQLite":
outputPath := filepath.Join(settings.ExportPath, "export.db")
exportErr = processor.ExportToSQLite(outputPath, exportOpts)
case "Excel":
outputPath := filepath.Join(settings.ExportPath, "export.xlsx")
exportErr = processor.ExportToExcel(outputPath, exportOpts)
}
if exportErr != nil {
dialog.ShowError(exportErr, window)
} else {
dialog.ShowInformation("成功", "數(shù)據(jù)導出完成", window)
settings.UpdateExportInfo(format, settings.ExportPath, time.Now().Format("2006-01-02"))
config.SaveSettings(settings)
}
onComplete()
}()
}
// 添加更新處理函數(shù)
func startUpdate(window fyne.Window, onComplete func()) {
settings, err := config.LoadSettings()
if err != nil {
dialog.ShowError(err, window)
onComplete()
return
}
if settings.TdxPath == "" {
dialog.ShowError(fmt.Errorf("請先在參數(shù)設置中設置通達信數(shù)據(jù)路徑"), window)
onComplete()
return
}
go func() {
processor := tdx.NewDataProcessor(settings.TdxPath)
progress := dialog.NewProgress("更新數(shù)據(jù)", "正在更新...", window)
progress.Show()
progressCallback := func(stockCode string, current, total int) {
progress.SetValue(float64(current) / float64(total))
}
if err := processor.UpdateData(progressCallback); err != nil {
progress.Hide()
dialog.ShowError(err, window)
} else {
progress.Hide()
dialog.ShowInformation("成功", "數(shù)據(jù)更新完成", window)
}
onComplete()
}()
}
// 輔助函數(shù):檢查字符串是否在切片中
func contains(slice []string, str string) bool {
for _, v := range slice {
if v == str {
return true
}
}
return false
}go.mod
module tdx_exporter go 1.23.1 require ( fyne.io/fyne/v2 v2.5.2 github.com/lib/pq v1.10.9 github.com/mattn/go-sqlite3 v1.14.24 github.com/xuri/excelize/v2 v2.9.0 ) require ( fyne.io/systray v1.11.0 // indirect github.com/BurntSushi/toml v1.4.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fredbi/uri v1.1.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fyne-io/gl-js v0.0.0-20220119005834-d2da28d9ccfe // indirect github.com/fyne-io/glfw-js v0.0.0-20240101223322-6e1efdc71b7a // indirect github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2 // indirect github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 // indirect github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect github.com/go-text/render v0.2.0 // indirect github.com/go-text/typesetting v0.2.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gopherjs/gopherjs v1.17.2 // indirect github.com/jeandeaual/go-locale v0.0.0-20240223122105-ce5225dcaa49 // indirect github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/nicksnyder/go-i18n/v2 v2.4.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/richardlehane/mscfb v1.0.4 // indirect github.com/richardlehane/msoleps v1.0.4 // indirect github.com/rymdport/portal v0.2.6 // indirect github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect github.com/stretchr/testify v1.8.4 // indirect github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d // indirect github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 // indirect github.com/yuin/goldmark v1.7.1 // indirect golang.org/x/crypto v0.28.0 // indirect golang.org/x/image v0.18.0 // indirect golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a // indirect golang.org/x/net v0.30.0 // indirect golang.org/x/sys v0.26.0 // indirect golang.org/x/text v0.19.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect )
data_processor.go
package tdx
import (
"bytes"
"database/sql"
"encoding/binary"
"encoding/csv"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strconv"
"strings"
_ "github.com/mattn/go-sqlite3"
"github.com/xuri/excelize/v2"
)
type DBConfig struct {
Host string
Port int
User string
Password string
DBName string
}
type DayData struct {
Date string `json:"date"`
Open float64 `json:"open"`
High float64 `json:"high"`
Low float64 `json:"low"`
Close float64 `json:"close"`
Amount float64 `json:"amount"`
Volume int64 `json:"volume"`
}
type TdxData struct {
Date string
Open float64
High float64
Low float64
Close float64
Volume int64
Amount float64
}
type DataProcessor struct {
DataPath string
}
type ExportOptions struct {
LastExportTime string
IsIncremental bool
TargetDir string
DataTypes DataTypes
LogCallback LogCallback
}
type LogCallback func(format string, args ...interface{})
// 添加進度回調函數(shù)類型
type ProgressCallback func(stockCode string, current, total int)
// 添加數(shù)據(jù)類型結構
type DataTypes struct {
Day bool
Min5 bool
Min1 bool
}
// 添加數(shù)據(jù)結構定義
type tdxMinRecord struct {
Date uint16 // 日期,2字節(jié)
Minute uint16 // 分鐘,2字節(jié)
Open float32 // 開盤價,4字節(jié)
High float32 // 最高價,4字節(jié)
Low float32 // 最低價,4字節(jié)
Close float32 // 收盤價,4字節(jié)
Amount float32 // 20-23字節(jié):成交額(元),single float
Volume uint32 // 24-27字節(jié):成交量(股),ulong
Reserved uint32 // 28-31字節(jié):保留
}
// 修改記錄結構定義,分開日線和分鐘線
type tdxDayRecord struct {
Date uint32 // 日期,4字節(jié),格式: YYYYMMDD
Open uint32 // 開盤價,4字節(jié)
High uint32 // 最高價,4字節(jié)
Low uint32 // 最低價,4字節(jié)
Close uint32 // 收盤價,4字節(jié)
Amount float32 // 成交額,4字節(jié)
Volume uint32 // 成交量,4字節(jié)
Reserved uint32 // 保留,4字節(jié)
}
func NewDataProcessor(path string) *DataProcessor {
return &DataProcessor{
DataPath: path,
}
}
func (dp *DataProcessor) ExportToCSV(outputPath string, opts ExportOptions) error {
// 使用傳入的輸出路徑作為基礎目錄
opts.TargetDir = outputPath
return dp.TransformData(opts)
}
func (dp *DataProcessor) ExportToSQLite(dbPath string, opts ExportOptions) error {
log := opts.LogCallback
if log == nil {
log = func(format string, args ...interface{}) {
fmt.Printf(format+"\n", args...)
}
}
log("創(chuàng)建SQLite數(shù)據(jù)庫...")
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
return fmt.Errorf("創(chuàng)建SQLite數(shù)據(jù)庫失敗: %v", err)
}
defer db.Close()
log("創(chuàng)建數(shù)據(jù)表...")
if err := dp.createTables(db); err != nil {
return fmt.Errorf("創(chuàng)建表失敗: %v", err)
}
// 處理不同周期的數(shù)據(jù)
if opts.DataTypes.Day {
log("正在導出日線數(shù)據(jù)到SQLite...")
if err := dp.exportDayDataToSQLite(db, log); err != nil {
return fmt.Errorf("導出日線數(shù)據(jù)失敗: %v", err)
}
}
if opts.DataTypes.Min5 {
log("正在導出5分鐘數(shù)據(jù)到SQLite...")
if err := dp.exportMinDataToSQLite(db, "5min", "fivemin", log); err != nil {
return fmt.Errorf("導出5分鐘數(shù)據(jù)失敗: %v", err)
}
}
if opts.DataTypes.Min1 {
log("正在導出1分鐘數(shù)據(jù)到SQLite...")
if err := dp.exportMinDataToSQLite(db, "1min", "onemin", log); err != nil {
return fmt.Errorf("導出1分鐘數(shù)據(jù)失敗: %v", err)
}
}
log("數(shù)據(jù)導出完成")
return nil
}
func (dp *DataProcessor) createTables(db *sql.DB) error {
// 日線數(shù)據(jù)表
_, err := db.Exec(`
CREATE TABLE IF NOT EXISTS stock_day_data (
代碼 TEXT,
日期 TEXT,
開盤價 REAL,
最高價 REAL,
最低價 REAL,
收盤價 REAL,
成交額 REAL,
成交量 INTEGER,
PRIMARY KEY (代碼, 日期)
)
`)
if err != nil {
return err
}
// 5分鐘數(shù)據(jù)表
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS stock_5min_data (
代碼 TEXT,
日期 TEXT,
時間 TEXT,
開盤價 REAL,
最高價 REAL,
最低價 REAL,
收盤價 REAL,
成交額 REAL,
成交量 INTEGER,
PRIMARY KEY (代碼, 日期, 時間)
)
`)
if err != nil {
return err
}
// 1分鐘數(shù)據(jù)表
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS stock_1min_data (
代碼 TEXT,
日期 TEXT,
時間 TEXT,
開盤價 REAL,
最高價 REAL,
最低價 REAL,
收盤價 REAL,
成交額 REAL,
成交量 INTEGER,
PRIMARY KEY (代碼, 日期, 時間)
)
`)
return err
}
func (dp *DataProcessor) exportMinDataToSQLite(db *sql.DB, period string, dirName string, log LogCallback) error {
csvDir := filepath.Join(os.Getenv("HOME"), "tdx_export", dirName)
files, err := os.ReadDir(csvDir)
if err != nil {
return fmt.Errorf("讀取CSV目錄失敗: %v", err)
}
// 開始事務
tx, err := db.Begin()
if err != nil {
return fmt.Errorf("開始事務失敗: %v", err)
}
// 準備插入語句
tableName := fmt.Sprintf("stock_%s_data", period)
stmt, err := tx.Prepare(fmt.Sprintf(`
INSERT OR REPLACE INTO %s (
代碼, 日期, 時間, 開盤價, 最高價, 最低價, 收盤價, 成交額, 成交量
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`, tableName))
if err != nil {
tx.Rollback()
return fmt.Errorf("準備SQL語句失敗: %v", err)
}
defer stmt.Close()
// 處理每個CSV文件
fileCount := 0
for _, file := range files {
if !file.IsDir() && strings.HasSuffix(strings.ToLower(file.Name()), ".csv") {
stockCode := strings.TrimSuffix(file.Name(), ".csv")
if stockCode == "all_codes" {
continue
}
fileCount++
log("正在處理%s數(shù)據(jù),股票代碼:%s (%d/%d)", period, stockCode, fileCount, len(files)-1)
// 讀取CSV文件
csvPath := filepath.Join(csvDir, file.Name())
csvFile, err := os.Open(csvPath)
if err != nil {
tx.Rollback()
return fmt.Errorf("打開CSV文件失敗 %s: %v", file.Name(), err)
}
reader := csv.NewReader(csvFile)
// 跳過標題行
reader.Read()
// 讀取數(shù)據(jù)
recordCount := 0
for {
record, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
csvFile.Close()
tx.Rollback()
return fmt.Errorf("讀取CSV記錄失敗: %v", err)
}
// 轉換數(shù)據(jù)類型
open, _ := strconv.ParseFloat(record[2], 64)
high, _ := strconv.ParseFloat(record[3], 64)
low, _ := strconv.ParseFloat(record[4], 64)
close, _ := strconv.ParseFloat(record[5], 64)
amount, _ := strconv.ParseFloat(record[6], 64)
volume, _ := strconv.ParseInt(record[7], 10, 64)
recordCount++
// 插入數(shù)據(jù)
_, err = stmt.Exec(
stockCode,
record[0], // 日期
record[1], // 時間
open,
high,
low,
close,
amount,
volume,
)
if err != nil {
csvFile.Close()
tx.Rollback()
return fmt.Errorf("插入數(shù)據(jù)失敗: %v", err)
}
}
log("完成處理 %s,共導入 %d 條記錄", stockCode, recordCount)
csvFile.Close()
}
}
// 提交事務
if err := tx.Commit(); err != nil {
return fmt.Errorf("提交事務失敗: %v", err)
}
log("完成導出%s數(shù)據(jù),共處理 %d 個文件", period, fileCount)
return nil
}
func (dp *DataProcessor) exportDayDataToSQLite(db *sql.DB, log LogCallback) error {
csvDir := filepath.Join(os.Getenv("HOME"), "tdx_export", "day")
files, err := os.ReadDir(csvDir)
if err != nil {
log("讀取CSV目錄失敗: %v", err)
return fmt.Errorf("讀取CSV目錄失敗: %v", err)
}
log("開始導出日線數(shù)據(jù)到SQLite...")
tx, err := db.Begin()
if err != nil {
return fmt.Errorf("開始事務失敗: %v", err)
}
stmt, err := tx.Prepare(`
INSERT OR REPLACE INTO stock_day_data (
代碼, 日期, 開盤價, 最高價, 最低價, 收盤價, 成交額, 成交量
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`)
if err != nil {
tx.Rollback()
return fmt.Errorf("準備SQL語句失敗: %v", err)
}
defer stmt.Close()
for _, file := range files {
if !file.IsDir() && strings.HasSuffix(strings.ToLower(file.Name()), ".csv") {
stockCode := strings.TrimSuffix(file.Name(), ".csv")
if stockCode == "all_codes" {
continue
}
log("正在處理股票: %s", stockCode)
csvFile, err := os.Open(filepath.Join(csvDir, file.Name()))
if err != nil {
tx.Rollback()
return fmt.Errorf("打開CSV文件失敗 %s: %v", file.Name(), err)
}
reader := csv.NewReader(csvFile)
reader.Read() // 跳過標題行
for {
record, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
csvFile.Close()
tx.Rollback()
return fmt.Errorf("讀取CSV記錄失敗: %v", err)
}
open, _ := strconv.ParseFloat(record[1], 64)
high, _ := strconv.ParseFloat(record[2], 64)
low, _ := strconv.ParseFloat(record[3], 64)
close, _ := strconv.ParseFloat(record[4], 64)
amount, _ := strconv.ParseFloat(record[5], 64)
amount /= 100
volume, _ := strconv.ParseInt(record[6], 10, 64)
volume /= 100
_, err = stmt.Exec(
stockCode,
record[0], // 日期
open, high, low, close,
amount, volume,
)
if err != nil {
csvFile.Close()
tx.Rollback()
return fmt.Errorf("插入數(shù)據(jù)失敗: %v", err)
}
}
csvFile.Close()
}
}
log("日線數(shù)據(jù)導出完成")
return tx.Commit()
}
func (dp *DataProcessor) ReadTdxData() ([]TdxData, error) {
if dp.DataPath == "" {
return nil, errors.New("通達信數(shù)據(jù)路徑未設置")
}
// TODO: 實現(xiàn)通達信數(shù)據(jù)獲取
return nil, nil
}
func (dp *DataProcessor) TransformData(opts ExportOptions) error {
log := opts.LogCallback
if log == nil {
log = func(format string, args ...interface{}) {
fmt.Printf(format+"\n", args...)
}
}
// 使用傳入的輸出路徑
baseDir := opts.TargetDir
if baseDir == "" {
baseDir = filepath.Join(os.Getenv("HOME"), "tdx_export")
}
// 創(chuàng)建不同時間周期的目錄
targetDirs := map[string]string{
"day": filepath.Join(baseDir, "day"),
"min1": filepath.Join(baseDir, "min1"),
"min5": filepath.Join(baseDir, "min5"),
}
// 根據(jù)選擇的數(shù)據(jù)類型過濾源
var selectedSources []struct {
path string
interval string
}
// 根據(jù)用戶選擇添加數(shù)據(jù)源
if opts.DataTypes.Day {
selectedSources = append(selectedSources,
struct{ path, interval string }{filepath.Join(dp.DataPath, "vipdoc", "sz", "lday"), "day"},
struct{ path, interval string }{filepath.Join(dp.DataPath, "vipdoc", "sh", "lday"), "day"},
)
}
if opts.DataTypes.Min1 {
selectedSources = append(selectedSources,
struct{ path, interval string }{filepath.Join(dp.DataPath, "vipdoc", "sz", "minline"), "min1"},
struct{ path, interval string }{filepath.Join(dp.DataPath, "vipdoc", "sh", "minline"), "min1"},
)
}
if opts.DataTypes.Min5 {
selectedSources = append(selectedSources,
struct{ path, interval string }{filepath.Join(dp.DataPath, "vipdoc", "sz", "fzline"), "min5"},
struct{ path, interval string }{filepath.Join(dp.DataPath, "vipdoc", "sh", "fzline"), "min5"},
)
}
// 確保目標目錄存在
for _, dir := range targetDirs {
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("創(chuàng)建目標目錄失敗: %v", err)
}
}
// 處理選中的數(shù)據(jù)源
for _, source := range selectedSources {
files, err := os.ReadDir(source.path)
if err != nil {
continue
}
for _, file := range files {
if !file.IsDir() {
var fileExt string
switch source.interval {
case "day":
fileExt = ".day"
case "min1":
fileExt = ".lc1"
case "min5":
fileExt = ".lc5"
}
if strings.HasSuffix(strings.ToLower(file.Name()), fileExt) {
if err := dp.convertToCSV(source.path, file.Name(), targetDirs[source.interval], source.interval); err != nil {
continue
}
}
}
}
}
return nil
}
func (dp *DataProcessor) convertToCSV(sourcePath, fileName, targetDir string, dataType string) error {
// 讀取源文件
sourceFile, err := os.ReadFile(filepath.Join(sourcePath, fileName))
if err != nil {
return err
}
// 創(chuàng)建目標文件
targetFile, err := os.Create(filepath.Join(targetDir, strings.TrimSuffix(fileName, filepath.Ext(fileName))+".csv"))
if err != nil {
return err
}
defer targetFile.Close()
// 寫入CSV頭
var header string
switch dataType {
case "day":
header = "日期,開盤價,最高價,最低價,收盤價,成交額,成交量\n"
case "min1", "min5":
header = "日期,時間,開盤價,最高價,最低價,收盤價,成交額,成交量\n"
}
// 寫入 UTF-8 BOM,確保 Excel 正確識別中文
if _, err := targetFile.Write([]byte{0xEF, 0xBB, 0xBF}); err != nil {
return fmt.Errorf("寫入 BOM 失敗: %v", err)
}
if _, err := targetFile.WriteString(header); err != nil {
return fmt.Errorf("寫入CSV頭失敗: %v", err)
}
// 處理記錄
recordSize := 32
recordCount := len(sourceFile) / recordSize
for i := 0; i < recordCount; i++ {
offset := i * recordSize
var line string
switch dataType {
case "day":
var record tdxDayRecord
binary.Read(bytes.NewReader(sourceFile[offset:offset+recordSize]), binary.LittleEndian, &record)
line = dp.formatDayRecord(record)
case "min1", "min5":
var record tdxMinRecord
binary.Read(bytes.NewReader(sourceFile[offset:offset+recordSize]), binary.LittleEndian, &record)
line = dp.formatMinRecord(record)
}
targetFile.WriteString(line)
}
return nil
}
// UpdateData 增量更新數(shù)據(jù)
func (dp *DataProcessor) UpdateData(progress ProgressCallback) error {
// 取CSV文件目錄
csvDir := filepath.Join(os.Getenv("HOME"), "tdx_export", "day")
// 讀取所有股票列表
codes, err := dp.readAllCodes(filepath.Join(csvDir, "all_codes.csv"))
if err != nil {
return fmt.Errorf("讀取代碼列表失敗: %v", err)
}
for i, code := range codes {
if progress != nil {
progress(code, i+1, len(codes))
}
// 取現(xiàn)有CSV文件的最后一個日期
csvPath := filepath.Join(csvDir, code+".csv")
lastDate, err := dp.getLastDate(csvPath)
if err != nil {
return fmt.Errorf("讀取文件 %s 失敗: %v", code, err)
}
// 確定數(shù)據(jù)文件路徑
market := "sz"
if strings.HasPrefix(code, "6") || strings.HasPrefix(code, "5") {
market = "sh"
}
dayPath := filepath.Join(dp.DataPath, "vipdoc", market, "lday", code+".day")
// 增量更新數(shù)據(jù)
if err := dp.appendNewData(dayPath, csvPath, lastDate); err != nil {
return fmt.Errorf("更新文件 %s 失敗: %v", code, err)
}
}
return nil
}
// readAllCodes 讀取代碼列表文件
func (dp *DataProcessor) readAllCodes(filepath string) ([]string, error) {
file, err := os.Open(filepath)
if err != nil {
return nil, err
}
defer file.Close()
reader := csv.NewReader(file)
records, err := reader.ReadAll()
if err != nil {
return nil, err
}
var codes []string
for i, record := range records {
if i == 0 { // 跳過標題行
continue
}
codes = append(codes, record[0])
}
return codes, nil
}
// getLastDate 獲取CSV文件中最后一個日期
func (dp *DataProcessor) getLastDate(filepath string) (string, error) {
file, err := os.Open(filepath)
if err != nil {
return "", err
}
defer file.Close()
reader := csv.NewReader(file)
var lastDate string
for {
record, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
return "", err
}
if len(record) > 0 {
lastDate = record[0] // 第一是日期
}
}
return lastDate, nil
}
// appendNewData 追加新數(shù)據(jù)到CSV文件
func (dp *DataProcessor) appendNewData(dayPath, csvPath, lastDate string) error {
// 讀取day文件
dayFile, err := os.ReadFile(dayPath)
if err != nil {
return err
}
// 打開CSV文件用于追加
csvFile, err := os.OpenFile(csvPath, os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer csvFile.Close()
// 處理每條記錄
recordSize := 32
recordCount := len(dayFile) / recordSize
for i := 0; i < recordCount; i++ {
offset := i * recordSize
var record tdxMinRecord
err := binary.Read(strings.NewReader(string(dayFile[offset:offset+recordSize])), binary.LittleEndian, &record)
if err != nil {
return err
}
// 轉換日期 - 使用 YYYYMMDD 格式
year := record.Date / 10000
month := (record.Date % 10000) / 100
day := record.Date % 100
date := fmt.Sprintf("%d-%02d-%02d", year, month, day)
// 只追加新數(shù)據(jù)
if date <= lastDate {
continue
}
// 寫入新數(shù)據(jù)
line := fmt.Sprintf("%s,%.2f,%.2f,%.2f,%.2f,%.2f,%d\n",
date,
float64(record.Open)/100.0,
float64(record.High)/100.0,
float64(record.Low)/100.0,
float64(record.Close)/100.0,
float64(record.Amount)/100.0,
record.Volume)
if _, err := csvFile.WriteString(line); err != nil {
return err
}
}
return nil
}
func (dp *DataProcessor) formatDayRecord(record tdxDayRecord) string {
// Format date: YYYYMMDD
date := fmt.Sprintf("%d-%02d-%02d",
record.Date/10000,
(record.Date%10000)/100,
record.Date%100)
// Day prices need to be divided by 100 to get the actual value
return fmt.Sprintf("%s,%.2f,%.2f,%.2f,%.2f,%.2f,%d\n",
date,
float64(record.Open)/100.0,
float64(record.High)/100.0,
float64(record.Low)/100.0,
float64(record.Close)/100.0,
float64(record.Amount)/100.0, // Amount is already in correct format
int(record.Volume)/100)
}
func (dp *DataProcessor) formatMinRecord(record tdxMinRecord) string {
// 解析日期
year := 2004 + (record.Date / 2048)
month := (record.Date % 2048) / 100
day := record.Date % 2048 % 100
date := fmt.Sprintf("%d-%02d-%02d", year, month, day)
// 解析時間
hour := record.Minute / 60
minute := record.Minute % 60
time := fmt.Sprintf("%02d:%02d", hour, minute)
// 格式化輸出,將日期和時間分為兩個字段
return fmt.Sprintf("%s,%s,%.2f,%.2f,%.2f,%.2f,%.2f,%d\n",
date, // 日期字段
time, // 時間字段
record.Open, // 開盤價
record.High, // 最高價
record.Low, // 最低價
record.Close, // 收盤價
float64(record.Amount)/100, // 成交額
record.Volume/100)
}
func (dp *DataProcessor) ExportToExcel(outputPath string, opts ExportOptions) error {
// 創(chuàng)建 Excel 主目錄
excelDir := filepath.Join(outputPath, "excel")
// 創(chuàng)建不同時間周期的目錄
excelDirs := map[string]string{
"day": filepath.Join(excelDir, "day"),
"min1": filepath.Join(excelDir, "min1"),
"min5": filepath.Join(excelDir, "min5"),
}
// 確保目標目錄存在
for _, dir := range excelDirs {
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("創(chuàng)建Excel目錄失敗: %v", err)
}
}
// 先導出到CSV
if err := dp.TransformData(opts); err != nil {
return fmt.Errorf("轉換數(shù)據(jù)失敗: %v", err)
}
// 處理不同周期的數(shù)據(jù)
if opts.DataTypes.Day {
if err := dp.exportDayDataToExcel(excelDirs["day"]); err != nil {
return fmt.Errorf("導出日線數(shù)據(jù)失敗: %v", err)
}
}
if opts.DataTypes.Min5 {
if err := dp.exportMinDataToExcel(excelDirs["min5"], "fivemin", "5分鐘"); err != nil {
return fmt.Errorf("導出5分鐘數(shù)據(jù)失敗: %v", err)
}
}
if opts.DataTypes.Min1 {
if err := dp.exportMinDataToExcel(excelDirs["min1"], "onemin", "1分鐘"); err != nil {
return fmt.Errorf("導出1分鐘數(shù)據(jù)失敗: %v", err)
}
}
return nil
}
func (dp *DataProcessor) exportMinDataToExcel(outputPath, dirName, sheetPrefix string) error {
csvDir := filepath.Join(os.Getenv("HOME"), "tdx_export", dirName)
files, err := os.ReadDir(csvDir)
if err != nil {
return fmt.Errorf("讀取CSV目錄失敗: %v", err)
}
fileCount := 0
for _, file := range files {
if !file.IsDir() && strings.HasSuffix(strings.ToLower(file.Name()), ".csv") {
stockCode := strings.TrimSuffix(file.Name(), ".csv")
if stockCode == "all_codes" {
continue
}
fileCount++
fmt.Printf("正在處理%s數(shù)據(jù),股票代碼:%s (%d/%d)\n", sheetPrefix, stockCode, fileCount, len(files)-1)
// 創(chuàng)建新的Excel文件
f := excelize.NewFile()
defer f.Close()
// 設置默認sheet名稱
sheetName := fmt.Sprintf("%s_%s", stockCode, sheetPrefix)
index, err := f.NewSheet(sheetName)
if err != nil {
return fmt.Errorf("創(chuàng)建Sheet失敗: %v", err)
}
f.DeleteSheet("Sheet1")
f.SetActiveSheet(index)
// 寫入表頭
headers := []string{"日期", "時間", "開盤價", "最高價", "最低價", "收盤價", "成交額", "成交量"}
for i, header := range headers {
cell := fmt.Sprintf("%c1", 'A'+i)
f.SetCellValue(sheetName, cell, header)
}
// 讀取CSV數(shù)據(jù)
csvPath := filepath.Join(csvDir, file.Name())
csvFile, err := os.Open(csvPath)
if err != nil {
return fmt.Errorf("打開CSV文件失敗 %s: %v", file.Name(), err)
}
reader := csv.NewReader(csvFile)
reader.Read() // 跳過標題行
row := 2 // 從第2行開始寫入數(shù)據(jù)
for {
record, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
csvFile.Close()
return fmt.Errorf("讀取CSV記錄失敗: %v", err)
}
// 寫入數(shù)據(jù)行
for i, value := range record {
cell := fmt.Sprintf("%c%d", 'A'+i, row)
f.SetCellValue(sheetName, cell, value)
}
row++
}
csvFile.Close()
// 保存Excel文件
excelPath := filepath.Join(outputPath, fmt.Sprintf("%s.xlsx", stockCode))
if err := f.SaveAs(excelPath); err != nil {
return fmt.Errorf("保存Excel文件失敗: %v", err)
}
}
}
fmt.Printf("完成導出%s數(shù)據(jù),共處理 %d 個文件\n", sheetPrefix, fileCount)
return nil
}
func (dp *DataProcessor) exportDayDataToExcel(outputPath string) error {
csvDir := filepath.Join(os.Getenv("HOME"), "tdx_export", "day")
files, err := os.ReadDir(csvDir)
if err != nil {
return fmt.Errorf("讀取CSV目錄失敗: %v", err)
}
fileCount := 0
for _, file := range files {
if !file.IsDir() && strings.HasSuffix(strings.ToLower(file.Name()), ".csv") {
stockCode := strings.TrimSuffix(file.Name(), ".csv")
if stockCode == "all_codes" {
continue
}
fileCount++
fmt.Printf("正在處理日線數(shù)據(jù),股票代碼:%s (%d/%d)\n", stockCode, fileCount, len(files)-1)
// 創(chuàng)建新的Excel文件
f := excelize.NewFile()
defer f.Close()
// 設置sheet名稱
sheetName := fmt.Sprintf("%s_日線", stockCode)
index, err := f.NewSheet(sheetName)
if err != nil {
return fmt.Errorf("創(chuàng)建Sheet失敗: %v", err)
}
f.DeleteSheet("Sheet1")
f.SetActiveSheet(index)
// 寫入表頭
headers := []string{"日期", "開盤價", "最高價", "最低價", "收盤價", "成交額", "成交量"}
for i, header := range headers {
cell := fmt.Sprintf("%c1", 'A'+i)
f.SetCellValue(sheetName, cell, header)
}
// 讀取CSV數(shù)據(jù)
csvPath := filepath.Join(csvDir, file.Name())
csvFile, err := os.Open(csvPath)
if err != nil {
return fmt.Errorf("打開CSV文件失敗 %s: %v", file.Name(), err)
}
reader := csv.NewReader(csvFile)
reader.Read() // 跳過標題行
row := 2 // 從第2行開始寫入數(shù)據(jù)
for {
record, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
csvFile.Close()
return fmt.Errorf("讀取CSV記錄??敗: %v", err)
}
// 寫入數(shù)據(jù)行
for i, value := range record {
cell := fmt.Sprintf("%c%d", 'A'+i, row)
f.SetCellValue(sheetName, cell, value)
}
row++
}
csvFile.Close()
// 保存Excel文件
excelPath := filepath.Join(outputPath, fmt.Sprintf("%s.xlsx", stockCode))
if err := f.SaveAs(excelPath); err != nil {
return fmt.Errorf("保存Excel文件失敗: %v", err)
}
}
}
fmt.Printf("完成導出日線數(shù)據(jù),共處理 %d 個文件\n", fileCount)
return nil
}
func (dp *DataProcessor) ExportToPostgres(dbConfig DBConfig, opts ExportOptions) error {
connStr := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
dbConfig.Host, dbConfig.Port, dbConfig.User, dbConfig.Password, dbConfig.DBName)
db, err := sql.Open("postgres", connStr)
if err != nil {
return fmt.Errorf("連接數(shù)據(jù)庫失敗: %v", err)
}
defer db.Close()
// 測試連接
if err := db.Ping(); err != nil {
return fmt.Errorf("數(shù)據(jù)庫連接測試失敗: %v", err)
}
// 根據(jù)選擇創(chuàng)建對應的表
if err := dp.createSelectedTables(db, opts.DataTypes); err != nil {
return fmt.Errorf("創(chuàng)建表失敗: %v", err)
}
// 先導出到CSV
if err := dp.TransformData(opts); err != nil {
return fmt.Errorf("轉換數(shù)據(jù)失敗: %v", err)
}
// 根據(jù)選擇導入數(shù)據(jù)
if opts.DataTypes.Day {
fmt.Println("開始導入日線數(shù)據(jù)...")
if err := dp.exportDayDataToPostgres(db); err != nil {
return fmt.Errorf("導出日線數(shù)據(jù)失敗: %v", err)
}
}
if opts.DataTypes.Min5 {
fmt.Println("開始導入5分鐘線數(shù)據(jù)...")
if err := dp.exportMinDataToPostgres(db, "5min", "fivemin"); err != nil {
return fmt.Errorf("導出5分鐘數(shù)據(jù)失敗: %v", err)
}
}
if opts.DataTypes.Min1 {
fmt.Println("開始導入1分鐘線數(shù)據(jù)...")
if err := dp.exportMinDataToPostgres(db, "1min", "onemin"); err != nil {
return fmt.Errorf("導出1分鐘數(shù)據(jù)失敗: %v", err)
}
}
return nil
}
// 只創(chuàng)建選中的表
func (dp *DataProcessor) createSelectedTables(db *sql.DB, types DataTypes) error {
if types.Day {
if err := dp.createDayTable(db); err != nil {
return err
}
}
if types.Min1 {
if err := dp.createMinTable(db, "1min"); err != nil {
return err
}
}
if types.Min5 {
if err := dp.createMinTable(db, "5min"); err != nil {
return err
}
}
return nil
}
func (dp *DataProcessor) createDayTable(db *sql.DB) error {
// 日線數(shù)據(jù)表
_, err := db.Exec(`
CREATE TABLE IF NOT EXISTS stock_day_data (
代碼 TEXT,
日期 DATE,
開盤價 NUMERIC(10,2),
最高價 NUMERIC(10,2),
最低價 NUMERIC(10,2),
收盤價 NUMERIC(10,2),
成交額 NUMERIC(16,2),
成交量 BIGINT,
CONSTRAINT stock_day_data_key UNIQUE (代碼, 日期)
)
`)
if err != nil {
return err
}
return nil
}
func (dp *DataProcessor) createMinTable(db *sql.DB, period string) error {
// 分鐘線數(shù)據(jù)表
_, err := db.Exec(fmt.Sprintf(`
CREATE TABLE IF NOT EXISTS stock_%s_data (
代碼 TEXT,
日期 DATE,
時間 TIME,
開盤價 NUMERIC(10,2),
最高價 NUMERIC(10,2),
最低價 NUMERIC(10,2),
收盤價 NUMERIC(10,2),
成交額 NUMERIC(16,2),
成交量 BIGINT,
CONSTRAINT stock_%s_data_key UNIQUE (代碼, 日期, 時間)
)
`, period, period))
if err != nil {
return err
}
return nil
}
func (dp *DataProcessor) exportDayDataToPostgres(db *sql.DB) error {
stmt, err := db.Prepare(`
INSERT INTO stock_day_data (代碼, 日期, 開盤價, 最高價, 最低價, 收盤價, 成交額, 成交量)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
ON CONFLICT (代碼, 日期) DO UPDATE SET
開盤價 = EXCLUDED.開盤價,
最高價 = EXCLUDED.最高價,
最低價 = EXCLUDED.最低價,
收盤價 = EXCLUDED.收盤價,
成交額 = EXCLUDED.成交額,
成交量 = EXCLUDED.成交量
`)
if err != nil {
return err
}
defer stmt.Close()
// 讀取CSV文件并導入數(shù)據(jù)
csvDir := filepath.Join(os.Getenv("HOME"), "tdx_export", "day")
return dp.importCSVToPostgres(csvDir, stmt, false)
}
func (dp *DataProcessor) exportMinDataToPostgres(db *sql.DB, period string, dirName string) error {
stmt, err := db.Prepare(fmt.Sprintf(`
INSERT INTO stock_%s_data (代碼, 日期, 時間, 開盤價, 最高價, 最低價, 收盤價, 成交額, 成交量)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
ON CONFLICT (代碼, 日期, 時間) DO UPDATE SET
開盤價 = EXCLUDED.開盤價,
最高價 = EXCLUDED.最高價,
最低價 = EXCLUDED.最低價,
收???價 = EXCLUDED.收盤價,
成交額 = EXCLUDED.成交額,
成交量 = EXCLUDED.成交量
`, period))
if err != nil {
return err
}
defer stmt.Close()
// 讀取CSV文件并導入數(shù)據(jù)
csvDir := filepath.Join(os.Getenv("HOME"), "tdx_export", dirName)
return dp.importCSVToPostgres(csvDir, stmt, true)
}
func (dp *DataProcessor) importCSVToPostgres(csvDir string, stmt *sql.Stmt, hasTime bool) error {
files, err := os.ReadDir(csvDir)
if err != nil {
return err
}
// 計算總文件數(shù)(排除 all_codes.csv)
totalFiles := 0
for _, file := range files {
if !file.IsDir() && strings.HasSuffix(file.Name(), ".csv") && file.Name() != "all_codes.csv" {
totalFiles++
}
}
fmt.Printf("開始導入數(shù)據(jù),共 %d 個文件需要處理\n", totalFiles)
processedFiles := 0
for _, file := range files {
if !file.IsDir() && strings.HasSuffix(file.Name(), ".csv") {
stockCode := strings.TrimSuffix(file.Name(), ".csv")
if stockCode == "all_codes" {
continue
}
processedFiles++
fmt.Printf("正在處理 [%d/%d] %s\n", processedFiles, totalFiles, stockCode)
csvFile, err := os.Open(filepath.Join(csvDir, file.Name()))
if err != nil {
return err
}
reader := csv.NewReader(csvFile)
reader.Read() // 跳過標題行
recordCount := 0
for {
record, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
csvFile.Close()
return err
}
// 轉換數(shù)據(jù)類型
values := make([]interface{}, 0)
values = append(values, stockCode, record[0])
if hasTime {
values = append(values, record[1])
record = record[2:]
}
for _, v := range record[1:] {
val, _ := strconv.ParseFloat(v, 64)
values = append(values, val)
}
if _, err := stmt.Exec(values...); err != nil {
csvFile.Close()
return err
}
recordCount++
}
csvFile.Close()
fmt.Printf("完成處理 %s,導入 %d 條記錄\n", stockCode, recordCount)
}
}
fmt.Printf("數(shù)據(jù)導入完成,共處理 %d 個文件\n", processedFiles)
return nil
}settings.go
package config
import (
"encoding/json"
"os"
"path/filepath"
)
type ExportInfo struct {
LastPath string `json:"last_path"` // 上次導出路徑
LastTime string `json:"last_time"` // 上次導出時間
}
// 添加數(shù)據(jù)庫連接配置結構
type DBConfig struct {
Host string `json:"host"`
Port int `json:"port"`
User string `json:"user"`
Password string `json:"password"`
DBName string `json:"dbname"`
}
type Settings struct {
TdxPath string `json:"tdx_path"` // 通達信數(shù)據(jù)路徑
ExportPath string `json:"export_path"` // 導出數(shù)據(jù)保存路徑
ExportPaths map[string]ExportInfo `json:"export_paths"` // 不同格式的導出信息
DBConfig DBConfig `json:"db_config"` // 數(shù)據(jù)庫連接配置
}
func NewSettings() *Settings {
// 默認導出到用戶目錄下的 tdx_export
homeDir, _ := os.UserHomeDir()
return &Settings{
ExportPath: filepath.Join(homeDir, "tdx_export"),
ExportPaths: make(map[string]ExportInfo),
DBConfig: DBConfig{
Host: "localhost",
Port: 5432,
User: "postgres",
DBName: "tdx_data",
},
}
}
// UpdateExportInfo 更新導出信息
func (s *Settings) UpdateExportInfo(format, path string, exportTime string) {
s.ExportPaths[format] = ExportInfo{
LastPath: path,
LastTime: exportTime,
}
}
// GetLastExportInfo 獲取上次導出信息
func (s *Settings) GetLastExportInfo(format string) (ExportInfo, bool) {
info, exists := s.ExportPaths[format]
return info, exists
}
func getConfigPath() string {
// 獲取當前工作目錄
currentDir, err := os.Getwd()
if err != nil {
return ""
}
// 創(chuàng)建配置目錄
configDir := filepath.Join(currentDir, "config")
if err := os.MkdirAll(configDir, 0755); err != nil {
return ""
}
// 返回配置文件完整路徑
return filepath.Join(configDir, "settings.json")
}
func SaveSettings(settings *Settings) error {
configPath := getConfigPath()
// 格式化 JSON 以便于閱讀和編輯
data, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return err
}
return os.WriteFile(configPath, data, 0644)
}
func LoadSettings() (*Settings, error) {
configPath := getConfigPath()
// 如果配置文件不存在,創(chuàng)建默認配置
if _, err := os.Stat(configPath); os.IsNotExist(err) {
settings := NewSettings()
if err := SaveSettings(settings); err != nil {
return nil, err
}
return settings, nil
}
data, err := os.ReadFile(configPath)
if err != nil {
return NewSettings(), nil
}
var settings Settings
if err := json.Unmarshal(data, &settings); err != nil {
return NewSettings(), nil
}
// 確保 ExportPaths 已初始化
if settings.ExportPaths == nil {
settings.ExportPaths = make(map[string]ExportInfo)
}
return &settings, nil
} settings.json
{
"tdx_path": "/Users/Apple/Downloads/tdx",
"export_path": "/Users/Apple/Downloads/tdx/exportdata",
"export_paths": {
"CSV": {
"last_path": "/Users/Apple/Downloads/tdx/exportdata",
"last_time": "2024-10-08"
},
"SQLite": {
"last_path": "/Users/Apple/Downloads/tdx/exportdata",
"last_time": "2024-10-08"
}
},
"db_config": {
"host": "127.0.0.1",
"port": 5432,
"user": "postgres",
"password": "postgres",
"dbname": "stock"
}
}到此這篇關于基于go中fyne gui的通達信數(shù)據(jù)導出工具的文章就介紹到這了,更多相關go 通達信數(shù)據(jù)導出工具內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
您可能感興趣的文章:

