基于go中fyne gui的通達(dá)信數(shù)據(jù)導(dǎo)出工具詳解
這是一個用 Go 語言開發(fā)的通達(dá)信數(shù)據(jù)導(dǎo)出工具,可以將通達(dá)信的本地?cái)?shù)據(jù)導(dǎo)出為多種格式,方便用戶進(jìn)行數(shù)據(jù)分析和處理。
主要功能
- 支持多種數(shù)據(jù)類型導(dǎo)出:
- 日線數(shù)據(jù)
- 5分鐘線數(shù)據(jù)
- 1分鐘線數(shù)據(jù)
- 支持多種導(dǎo)出格式:
- CSV 格式
- SQLite 數(shù)據(jù)庫
- Excel 文件
- Postgres數(shù)據(jù)庫連接導(dǎo)出
特點(diǎn)
- 圖形化界面,操作簡單直觀
- 支持增量導(dǎo)出,避免重復(fù)處理
- 可配置數(shù)據(jù)源和導(dǎo)出路徑
- 實(shí)時顯示處理進(jìn)度
- 支持批量處理大量數(shù)據(jù)
使用說明
- 設(shè)置通達(dá)信數(shù)據(jù)路徑
- 選擇要導(dǎo)出的數(shù)據(jù)類型
- 選擇導(dǎo)出格式
- 點(diǎn)擊導(dǎo)出按鈕開始處理
技術(shù)棧
- Go 語言開發(fā)
- Fyne GUI 框架
- SQLite/CSV/Excel/postgres 數(shù)據(jù)處理
配置要求
- 需要本地安裝通達(dá)信軟件
- 需要有通達(dá)信的歷史數(shù)據(jù)
注意事項(xiàng)
- 首次使用需要配置通達(dá)信數(shù)據(jù)路徑
- 建議定期備份導(dǎo)出的數(shù)據(jù)
- 大量數(shù)據(jù)導(dǎo)出可能需要較長時間
所有測試工作都在在mac中進(jìn)行,源碼可以在多平臺運(yùn)行.程序目錄結(jié)構(gòu)
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() { // 設(shè)置中文編碼 os.Setenv("LANG", "zh_CN.UTF-8") myApp := app.New() // 設(shè)置中文字體 myApp.Settings().SetTheme(&myTheme{theme.DefaultTheme()}) myWindow := myApp.NewWindow("通達(dá)信數(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" { // 默認(rèn)選中所有選項(xiàng),但保持可選狀態(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)建結(jié)果顯示區(qū)域 - 修改為更大的顯示區(qū)域 resultArea := widget.NewMultiLineEntry() resultArea.Disable() resultArea.SetPlaceHolder("導(dǎo)出結(jié)果將在這里顯示") resultArea.Resize(fyne.NewSize(580, 300)) // 設(shè)置更大的尺寸 resultArea.SetMinRowsVisible(15) // 顯示更多行 // 創(chuàng)建按鈕 settingsBtn := widget.NewButtonWithIcon("設(shè)置", theme.SettingsIcon(), func() { showSettingsDialog(myWindow) }) var exportBtn, updateBtn *widget.Button // 創(chuàng)建左側(cè)布局 leftPanel := container.NewVBox( widget.NewLabel("導(dǎo)出格式:"), formatSelect, widget.NewSeparator(), // 添加隔線 widget.NewLabel("數(shù)據(jù)類型選擇:"), dayGroup, minGroup, ) exportBtn = widget.NewButtonWithIcon("導(dǎo)出", 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() // 設(shè)置為只讀 memo.SetPlaceHolder("提示信息將在這里顯示") memo.SetMinRowsVisible(5) // 設(shè)置最小顯示行數(shù) memo.SetText("歡迎使用通達(dá)信數(shù)據(jù)工具\(yùn)n請選擇要導(dǎo)出的數(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 } // 基本設(shè)置頁面 tdxPath := widget.NewEntry() tdxPath.SetText(settings.TdxPath) tdxPath.SetPlaceHolder("請輸入通達(dá)信數(shù)據(jù)目路徑") exportPath := widget.NewEntry() exportPath.SetText(settings.ExportPath) exportPath.SetPlaceHolder("請輸入導(dǎo)出數(shù)據(jù)保存路徑") // 數(shù)據(jù)庫設(shè)置頁面 dbHost := widget.NewEntry() dbHost.SetText(settings.DBConfig.Host) dbHost.SetPlaceHolder("數(shù)據(jù)庫主機(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ù)庫設(shè)置頁面布局 dbSettings := container.NewVBox( // 添加測試連接按鈕到頂部 container.NewHBox( layout.NewSpacer(), testConnBtn, layout.NewSpacer(), ), widget.NewSeparator(), // 分隔線 container.NewGridWithColumns(2, container.NewVBox( widget.NewLabel("主機(jī)地址:"), dbHost, widget.NewLabel("用戶名:"), dbUser, widget.NewLabel("數(shù)據(jù)庫名:"), dbName, ), container.NewVBox( widget.NewLabel("端口號:"), dbPort, widget.NewLabel("密碼:"), dbPassword, ), ), ) // 修改基本設(shè)置頁面布局 basicSettings := container.NewVBox( widget.NewLabel("通達(dá)信數(shù)據(jù)路徑:"), container.NewPadded(tdxPath), widget.NewSeparator(), widget.NewLabel("導(dǎo)出數(shù)據(jù)保存路徑:"), container.NewPadded(exportPath), ) // 創(chuàng)建標(biāo)簽頁 tabs := container.NewAppTabs( container.NewTabItem("基本設(shè)置", container.NewPadded(basicSettings)), container.NewTabItem("數(shù)據(jù)庫設(shè)置", container.NewPadded(dbSettings)), ) tabs.SetTabLocation(container.TabLocationTop) dialog := dialog.NewCustomConfirm( "參數(shù)設(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("成功", "設(shè)置已保存", window) }, window, ) // 設(shè)置對話框大小 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è)置中設(shè)置通達(dá)信數(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ù)導(dǎo)出完成", 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è)置中設(shè)置通達(dá)信數(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{}) // 添加進(jìn)度回調(diào)函數(shù)類型 type ProgressCallback func(stockCode string, current, total int) // 添加數(shù)據(jù)類型結(jié)構(gòu) type DataTypes struct { Day bool Min5 bool Min1 bool } // 添加數(shù)據(jù)結(jié)構(gòu)定義 type tdxMinRecord struct { Date uint16 // 日期,2字節(jié) Minute uint16 // 分鐘,2字節(jié) Open float32 // 開盤價(jià),4字節(jié) High float32 // 最高價(jià),4字節(jié) Low float32 // 最低價(jià),4字節(jié) Close float32 // 收盤價(jià),4字節(jié) Amount float32 // 20-23字節(jié):成交額(元),single float Volume uint32 // 24-27字節(jié):成交量(股),ulong Reserved uint32 // 28-31字節(jié):保留 } // 修改記錄結(jié)構(gòu)定義,分開日線和分鐘線 type tdxDayRecord struct { Date uint32 // 日期,4字節(jié),格式: YYYYMMDD Open uint32 // 開盤價(jià),4字節(jié) High uint32 // 最高價(jià),4字節(jié) Low uint32 // 最低價(jià),4字節(jié) Close uint32 // 收盤價(jià),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 { // 使用傳入的輸出路徑作為基礎(chǔ)目錄 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("正在導(dǎo)出日線數(shù)據(jù)到SQLite...") if err := dp.exportDayDataToSQLite(db, log); err != nil { return fmt.Errorf("導(dǎo)出日線數(shù)據(jù)失敗: %v", err) } } if opts.DataTypes.Min5 { log("正在導(dǎo)出5分鐘數(shù)據(jù)到SQLite...") if err := dp.exportMinDataToSQLite(db, "5min", "fivemin", log); err != nil { return fmt.Errorf("導(dǎo)出5分鐘數(shù)據(jù)失敗: %v", err) } } if opts.DataTypes.Min1 { log("正在導(dǎo)出1分鐘數(shù)據(jù)到SQLite...") if err := dp.exportMinDataToSQLite(db, "1min", "onemin", log); err != nil { return fmt.Errorf("導(dǎo)出1分鐘數(shù)據(jù)失敗: %v", err) } } log("數(shù)據(jù)導(dǎo)出完成") 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, 開盤價(jià) REAL, 最高價(jià) REAL, 最低價(jià) REAL, 收盤價(jià) 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, 開盤價(jià) REAL, 最高價(jià) REAL, 最低價(jià) REAL, 收盤價(jià) 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, 開盤價(jià) REAL, 最高價(jià) REAL, 最低價(jià) REAL, 收盤價(jià) 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) } // 開始事務(wù) tx, err := db.Begin() if err != nil { return fmt.Errorf("開始事務(wù)失敗: %v", err) } // 準(zhǔn)備插入語句 tableName := fmt.Sprintf("stock_%s_data", period) stmt, err := tx.Prepare(fmt.Sprintf(` INSERT OR REPLACE INTO %s ( 代碼, 日期, 時間, 開盤價(jià), 最高價(jià), 最低價(jià), 收盤價(jià), 成交額, 成交量 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `, tableName)) if err != nil { tx.Rollback() return fmt.Errorf("準(zhǔn)備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) // 跳過標(biāo)題行 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) } // 轉(zhuǎn)換數(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ǎo)入 %d 條記錄", stockCode, recordCount) csvFile.Close() } } // 提交事務(wù) if err := tx.Commit(); err != nil { return fmt.Errorf("提交事務(wù)失敗: %v", err) } log("完成導(dǎo)出%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("開始導(dǎo)出日線數(shù)據(jù)到SQLite...") tx, err := db.Begin() if err != nil { return fmt.Errorf("開始事務(wù)失敗: %v", err) } stmt, err := tx.Prepare(` INSERT OR REPLACE INTO stock_day_data ( 代碼, 日期, 開盤價(jià), 最高價(jià), 最低價(jià), 收盤價(jià), 成交額, 成交量 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `) if err != nil { tx.Rollback() return fmt.Errorf("準(zhǔn)備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() // 跳過標(biāo)題行 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ù)導(dǎo)出完成") return tx.Commit() } func (dp *DataProcessor) ReadTdxData() ([]TdxData, error) { if dp.DataPath == "" { return nil, errors.New("通達(dá)信數(shù)據(jù)路徑未設(shè)置") } // TODO: 實(shí)現(xiàn)通達(dá)信數(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"}, ) } // 確保目標(biāo)目錄存在 for _, dir := range targetDirs { if err := os.MkdirAll(dir, 0755); err != nil { return fmt.Errorf("創(chuàng)建目標(biāo)目錄失敗: %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)建目標(biāo)文件 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 = "日期,開盤價(jià),最高價(jià),最低價(jià),收盤價(jià),成交額,成交量\n" case "min1", "min5": header = "日期,時間,開盤價(jià),最高價(jià),最低價(jià),收盤價(jià),成交額,成交量\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 { // 跳過標(biāo)題行 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 } // 轉(zhuǎn)換日期 - 使用 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, // 開盤價(jià) record.High, // 最高價(jià) record.Low, // 最低價(jià) record.Close, // 收盤價(jià) 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"), } // 確保目標(biāo)目錄存在 for _, dir := range excelDirs { if err := os.MkdirAll(dir, 0755); err != nil { return fmt.Errorf("創(chuàng)建Excel目錄失敗: %v", err) } } // 先導(dǎo)出到CSV if err := dp.TransformData(opts); err != nil { return fmt.Errorf("轉(zhuǎn)換數(shù)據(jù)失敗: %v", err) } // 處理不同周期的數(shù)據(jù) if opts.DataTypes.Day { if err := dp.exportDayDataToExcel(excelDirs["day"]); err != nil { return fmt.Errorf("導(dǎo)出日線數(shù)據(jù)失敗: %v", err) } } if opts.DataTypes.Min5 { if err := dp.exportMinDataToExcel(excelDirs["min5"], "fivemin", "5分鐘"); err != nil { return fmt.Errorf("導(dǎo)出5分鐘數(shù)據(jù)失敗: %v", err) } } if opts.DataTypes.Min1 { if err := dp.exportMinDataToExcel(excelDirs["min1"], "onemin", "1分鐘"); err != nil { return fmt.Errorf("導(dǎo)出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() // 設(shè)置默認(rèn)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{"日期", "時間", "開盤價(jià)", "最高價(jià)", "最低價(jià)", "收盤價(jià)", "成交額", "成交量"} 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() // 跳過標(biāo)題行 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("完成導(dǎo)出%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() // 設(shè)置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{"日期", "開盤價(jià)", "最高價(jià)", "最低價(jià)", "收盤價(jià)", "成交額", "成交量"} 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() // 跳過標(biāo)題行 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("完成導(dǎo)出日線數(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)建對應(yīng)的表 if err := dp.createSelectedTables(db, opts.DataTypes); err != nil { return fmt.Errorf("創(chuàng)建表失敗: %v", err) } // 先導(dǎo)出到CSV if err := dp.TransformData(opts); err != nil { return fmt.Errorf("轉(zhuǎn)換數(shù)據(jù)失敗: %v", err) } // 根據(jù)選擇導(dǎo)入數(shù)據(jù) if opts.DataTypes.Day { fmt.Println("開始導(dǎo)入日線數(shù)據(jù)...") if err := dp.exportDayDataToPostgres(db); err != nil { return fmt.Errorf("導(dǎo)出日線數(shù)據(jù)失敗: %v", err) } } if opts.DataTypes.Min5 { fmt.Println("開始導(dǎo)入5分鐘線數(shù)據(jù)...") if err := dp.exportMinDataToPostgres(db, "5min", "fivemin"); err != nil { return fmt.Errorf("導(dǎo)出5分鐘數(shù)據(jù)失敗: %v", err) } } if opts.DataTypes.Min1 { fmt.Println("開始導(dǎo)入1分鐘線數(shù)據(jù)...") if err := dp.exportMinDataToPostgres(db, "1min", "onemin"); err != nil { return fmt.Errorf("導(dǎo)出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, 開盤價(jià) NUMERIC(10,2), 最高價(jià) NUMERIC(10,2), 最低價(jià) NUMERIC(10,2), 收盤價(jià) 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, 開盤價(jià) NUMERIC(10,2), 最高價(jià) NUMERIC(10,2), 最低價(jià) NUMERIC(10,2), 收盤價(jià) 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 (代碼, 日期, 開盤價(jià), 最高價(jià), 最低價(jià), 收盤價(jià), 成交額, 成交量) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (代碼, 日期) DO UPDATE SET 開盤價(jià) = EXCLUDED.開盤價(jià), 最高價(jià) = EXCLUDED.最高價(jià), 最低價(jià) = EXCLUDED.最低價(jià), 收盤價(jià) = EXCLUDED.收盤價(jià), 成交額 = EXCLUDED.成交額, 成交量 = EXCLUDED.成交量 `) if err != nil { return err } defer stmt.Close() // 讀取CSV文件并導(dǎo)入數(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 (代碼, 日期, 時間, 開盤價(jià), 最高價(jià), 最低價(jià), 收盤價(jià), 成交額, 成交量) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) ON CONFLICT (代碼, 日期, 時間) DO UPDATE SET 開盤價(jià) = EXCLUDED.開盤價(jià), 最高價(jià) = EXCLUDED.最高價(jià), 最低價(jià) = EXCLUDED.最低價(jià), 收???價(jià) = EXCLUDED.收盤價(jià), 成交額 = EXCLUDED.成交額, 成交量 = EXCLUDED.成交量 `, period)) if err != nil { return err } defer stmt.Close() // 讀取CSV文件并導(dǎo)入數(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 } // 計(jì)算總文件數(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("開始導(dǎo)入數(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() // 跳過標(biāo)題行 recordCount := 0 for { record, err := reader.Read() if err == io.EOF { break } if err != nil { csvFile.Close() return err } // 轉(zhuǎn)換數(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ǎo)入 %d 條記錄\n", stockCode, recordCount) } } fmt.Printf("數(shù)據(jù)導(dǎo)入完成,共處理 %d 個文件\n", processedFiles) return nil }
settings.go
package config import ( "encoding/json" "os" "path/filepath" ) type ExportInfo struct { LastPath string `json:"last_path"` // 上次導(dǎo)出路徑 LastTime string `json:"last_time"` // 上次導(dǎo)出時間 } // 添加數(shù)據(jù)庫連接配置結(jié)構(gòu) 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"` // 通達(dá)信數(shù)據(jù)路徑 ExportPath string `json:"export_path"` // 導(dǎo)出數(shù)據(jù)保存路徑 ExportPaths map[string]ExportInfo `json:"export_paths"` // 不同格式的導(dǎo)出信息 DBConfig DBConfig `json:"db_config"` // 數(shù)據(jù)庫連接配置 } func NewSettings() *Settings { // 默認(rèn)導(dǎo)出到用戶目錄下的 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 更新導(dǎo)出信息 func (s *Settings) UpdateExportInfo(format, path string, exportTime string) { s.ExportPaths[format] = ExportInfo{ LastPath: path, LastTime: exportTime, } } // GetLastExportInfo 獲取上次導(dǎo)出信息 func (s *Settings) GetLastExportInfo(format string) (ExportInfo, bool) { info, exists := s.ExportPaths[format] return info, exists } func getConfigPath() string { // 獲取當(dāng)前工作目錄 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)建默認(rèn)配置 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" } }
到此這篇關(guān)于基于go中fyne gui的通達(dá)信數(shù)據(jù)導(dǎo)出工具的文章就介紹到這了,更多相關(guān)go 通達(dá)信數(shù)據(jù)導(dǎo)出工具內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
基于Golang實(shí)現(xiàn)YOLO目標(biāo)檢測算法
目標(biāo)檢測是計(jì)算機(jī)視覺領(lǐng)域的重要任務(wù),它不僅可以識別圖像中的物體,還可以標(biāo)記出物體的位置和邊界框,YOLO是一種先進(jìn)的目標(biāo)檢測算法,以其高精度和實(shí)時性而聞名,本文將介紹如何使用Golang實(shí)現(xiàn)YOLO目標(biāo)檢測算法,文中有相關(guān)的代碼示例供大家參考,需要的朋友可以參考下2023-11-11在Go中實(shí)現(xiàn)和使用堆棧以及先進(jìn)先出原則詳解
Go是一種功能強(qiáng)大的編程語言,提供了豐富的數(shù)據(jù)結(jié)構(gòu)和算法,堆棧是計(jì)算機(jī)科學(xué)中的基本數(shù)據(jù)結(jié)構(gòu)之一,在本博文中,我們將探討如何在?Go?中實(shí)現(xiàn)和使用堆棧,以及堆棧如何遵循先進(jìn)先出?(FIFO)?原則2023-10-10深入string理解Golang是怎樣實(shí)現(xiàn)的
這篇文章主要為大家介紹了深入string理解Golang是怎樣實(shí)現(xiàn)的原理詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04