欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

基于go中fyne gui的通達(dá)信數(shù)據(jù)導(dǎo)出工具詳解

 更新時間:2024年12月12日 10:25:32   作者:布林模型  
這篇文章主要介紹了基于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ù)分析和處理,需要的朋友可以參考下

這是一個用 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使用MinIO的方案詳解

    Golang使用MinIO的方案詳解

    這篇文章主要介紹了Golang使用MinIO的過程,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2023-08-08
  • GO語言Context的作用及各種使用方法

    GO語言Context的作用及各種使用方法

    golang的Context包是專門用來處理多個goroutine之間與請求域的數(shù)據(jù)、取消信號、截止時間等相關(guān)操作,下面這篇文章主要給大家介紹了關(guān)于GO語言Context的作用及各種使用方法的相關(guān)資料,需要的朋友可以參考下
    2024-01-01
  • 基于Golang實(shí)現(xiàn)YOLO目標(biāo)檢測算法

    基于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
  • 淺析Golang中float64的精度問題

    淺析Golang中float64的精度問題

    這篇文章主要來和大家一起探討一下Golang中關(guān)于float64的精度問題,文中的示例代碼講解詳細(xì),具有一定的學(xué)習(xí)價(jià)值,感興趣的小伙伴可以了解下
    2023-08-08
  • 在Go中實(shí)現(xiàn)和使用堆棧以及先進(jìn)先出原則詳解

    在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
  • Go語言命令行操作命令詳細(xì)介紹

    Go語言命令行操作命令詳細(xì)介紹

    這篇文章主要介紹了Go語言命令行操作命令詳細(xì)介紹,本文重點(diǎn)介紹了go build、go clean、go fmt、go get等命令,需要的朋友可以參考下
    2014-10-10
  • go zero微服務(wù)框架logx日志組件剖析

    go zero微服務(wù)框架logx日志組件剖析

    這篇文章主要為大家介紹了go zero微服務(wù)框架logx日志組件剖析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-09-09
  • Go語言并發(fā)編程基礎(chǔ)上下文概念詳解

    Go語言并發(fā)編程基礎(chǔ)上下文概念詳解

    這篇文章主要為大家介紹了Go語言并發(fā)編程基礎(chǔ)上下文示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-08-08
  • 深入string理解Golang是怎樣實(shí)現(xiàn)的

    深入string理解Golang是怎樣實(shí)現(xiàn)的

    這篇文章主要為大家介紹了深入string理解Golang是怎樣實(shí)現(xiàn)的原理詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-04-04
  • golang?字符串拼接方法對比分析

    golang?字符串拼接方法對比分析

    這篇文章主要為大家介紹了golang?字符串拼接方法對比分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-09-09

最新評論