├── config.ini ├── README.md ├── .gitignore ├── LICENSE ├── config └── config.go ├── main.go ├── stock └── stock.go ├── turtle └── index.go ├── peroidexterma └── index.go ├── trading └── turtle.go └── history └── daily.go /config.ini: -------------------------------------------------------------------------------- 1 | [path] 2 | datadir = e:\data 3 | logpath = e:\data\main.log 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### 股票趋势技术分析 2 | 一个偶然的机会,接触到了期货交易中的海龟交易法则,因此想验证类似海龟的交易法则在股票交易中的效果。现在只计算美股,因为市场成熟,数据也更完整。 3 | ##### 按计划项目大致会分如下几步走: 4 | 1. 收集纳斯达克100指数的成份股在过去10年的每日复权股价 5 | 2. 计算不同参数设定下这些股票的区间极值和海龟指标 6 | 3. 测试不同的交易系统在不同股票上的表现 7 | 4. 收集股票的分时数据,完善模拟交易的过程 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 nzai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/Unknwon/goconfig" 7 | ) 8 | 9 | const ( 10 | configSection = "path" 11 | configKey = "datadir" 12 | configDefaultValue = "data" 13 | ) 14 | 15 | type Config struct { 16 | filename string 17 | configFile *goconfig.ConfigFile 18 | } 19 | 20 | var configInstance = New() 21 | 22 | // 默认 23 | func New() *goconfig.ConfigFile { 24 | configFile, err := goconfig.LoadConfigFile("config.ini") 25 | if err != nil { 26 | return nil 27 | } 28 | 29 | return configFile 30 | } 31 | 32 | // 设置配置文件 33 | func SetConfigFile(filePath string) error { 34 | 35 | configFile, err := goconfig.LoadConfigFile(filePath) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | configInstance = configFile 41 | 42 | return nil 43 | } 44 | 45 | // 获取配置 46 | func GetString(section, key, defaultValue string) string { 47 | return configInstance.MustValue(section, key, defaultValue) 48 | } 49 | 50 | // 获取数据保存目录 51 | func GetDataDir() (string, error) { 52 | 53 | // 数据保存目录 54 | dataDir := GetString(configSection, configKey, configDefaultValue) 55 | 56 | // 检查目录是否存在 57 | _, err := os.Stat(dataDir) 58 | if os.IsNotExist(err) { 59 | err = os.Mkdir(dataDir, 0x777) 60 | if err != nil { 61 | return "", err 62 | } 63 | } 64 | 65 | return dataDir, nil 66 | } 67 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/nzai/Tast/config" 9 | "github.com/nzai/Tast/history" 10 | "github.com/nzai/Tast/peroidexterma" 11 | "github.com/nzai/Tast/stock" 12 | "github.com/nzai/Tast/trading" 13 | "github.com/nzai/Tast/turtle" 14 | ) 15 | 16 | const ( 17 | configFileName = "config.ini" 18 | configLogSection = "path" 19 | configLogKey = "logpath" 20 | configLogDefaultFileName = "main.log" 21 | ) 22 | 23 | func main() { 24 | 25 | // 当前目录 26 | root := filepath.Dir(os.Args[0]) 27 | filename := filepath.Join(root, configFileName) 28 | 29 | // 使用所有cpu 30 | // runtime.GOMAXPROCS(runtime.NumCPU() - 1) 31 | 32 | // 读取配置文件 33 | err := config.SetConfigFile(filename) 34 | if err != nil { 35 | log.Fatal(err) 36 | return 37 | } 38 | 39 | // 日志文件路径 40 | logPath := config.GetString(configLogSection, configLogKey, configLogDefaultFileName) 41 | logDir := filepath.Dir(logPath) 42 | _, err = os.Stat(logDir) 43 | if os.IsNotExist(err) { 44 | err = os.Mkdir(logDir, 0x777) 45 | if err != nil { 46 | log.Fatal(err) 47 | return 48 | } 49 | } 50 | 51 | file, err := os.OpenFile(logPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0x777) 52 | if err != nil { 53 | log.Fatal(err) 54 | return 55 | } 56 | defer file.Close() 57 | 58 | // 设置日志输出文件 59 | log.SetOutput(file) 60 | 61 | // 更新股票信息 62 | err = stock.UpdateAll() 63 | if err != nil { 64 | log.Fatalf("更新股票列表发生错误:%v", err) 65 | return 66 | } 67 | 68 | // 更新所有股票的历史 69 | err = history.UpdateAll() 70 | if err != nil { 71 | log.Fatalf("更新股票历史发生错误:%v", err) 72 | return 73 | } 74 | 75 | // 更新所有股票的海龟指标 76 | err = turtle.UpdateAll() 77 | if err != nil { 78 | log.Fatalf("更新海龟指标发生错误:%v", err) 79 | return 80 | } 81 | 82 | // 更新所有股票的区间极值指标 83 | err = peroidexterma.UpdateAll() 84 | if err != nil { 85 | log.Fatalf("更新区间极值指标发生错误:%v", err) 86 | return 87 | } 88 | 89 | // 测试海龟交易系统 90 | err = trading.TestAll() 91 | if err != nil { 92 | log.Fatalf("测试海龟交易系统发生错误:%v", err) 93 | return 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /stock/stock.go: -------------------------------------------------------------------------------- 1 | package stock 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "net/http" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | 14 | "github.com/nzai/Tast/config" 15 | ) 16 | 17 | const ( 18 | stocksFileName = "stocks.txt" 19 | // 从智库百科-标准普尔500指数页面下载成份股 20 | //sp100url = "http://wiki.mbalib.com/wiki/%E6%A0%87%E5%87%86%E6%99%AE%E5%B0%94100%E6%8C%87%E6%95%B0" 21 | // 从纳斯达克100指数页面下载成份股 22 | nasdaq100Url = "http://www.nasdaq.com/quotes/nasdaq-100-stocks.aspx?render=download" 23 | ) 24 | 25 | // 股票 26 | type Stock struct { 27 | Code string 28 | EnglishName string 29 | } 30 | 31 | // 更新股票列表 32 | func UpdateAll() error { 33 | 34 | log.Println("开始更新股票列表") 35 | // 更新股票 36 | _, err := GetAll() 37 | 38 | log.Println("股票列表更新结束") 39 | 40 | return err 41 | } 42 | 43 | // 获取股票列表 44 | func GetAll() ([]Stock, error) { 45 | 46 | // 数据保存目录 47 | dataDir, err := config.GetDataDir() 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | // 股票列表文件路径 53 | filePath := filepath.Join(dataDir, stocksFileName) 54 | _, err = os.Stat(filePath) 55 | if os.IsNotExist(err) { 56 | // 如果股票列表文件不存在,则从纳斯达克下载 57 | stocks, err := downloadFromNasdaq100() 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | // 保存下载的股票 63 | return stocks, save(stocks, filePath) 64 | } 65 | 66 | return load(filePath) 67 | } 68 | 69 | //// 从智库百科下载标普100成份股 70 | //func downloadFromMbalib() ([]Stock, error) { 71 | // response, err := http.Get(sp100url) 72 | // if err != nil { 73 | // return nil, err 74 | // } 75 | // defer response.Body.Close() 76 | 77 | // buffer, err := ioutil.ReadAll(response.Body) 78 | // if err != nil { 79 | // return nil, err 80 | // } 81 | 82 | // regex := regexp.MustCompile(`(]*?>)?(\w+)()?]*?>([^<]+)([^<]*)?]*?>([^<]+?)`) 83 | // matches := regex.FindAllStringSubmatch(string(buffer), -1) 84 | 85 | // stocks := make([]Stock, 0) 86 | // for _, match := range matches { 87 | 88 | // if len(match) != 7 { 89 | // return nil, errors.New("股票列表格式不正确") 90 | // } 91 | 92 | // stocks = append(stocks, Stock{ 93 | // Code: match[2], 94 | // EnglishName: match[4], 95 | // ChineseName: match[6], 96 | // }) 97 | // } 98 | 99 | // return stocks, nil 100 | //} 101 | 102 | func downloadFromNasdaq100() ([]Stock, error) { 103 | 104 | response, err := http.Get(nasdaq100Url) 105 | if err != nil { 106 | return nil, err 107 | } 108 | defer response.Body.Close() 109 | 110 | buffer, err := ioutil.ReadAll(response.Body) 111 | if err != nil { 112 | return nil, err 113 | } 114 | 115 | lines := strings.Split(string(buffer), "\n") 116 | stocks := make([]Stock, 0) 117 | 118 | // 略过第一行的标题和最后一行空白 119 | for index := 1; index < len(lines)-1; index++ { 120 | 121 | parts := strings.Split(lines[index], ",") 122 | if len(parts) < 2 { 123 | return nil, errors.New("纳斯达克股票列表文件格式不正确") 124 | } 125 | 126 | stocks = append(stocks, Stock{ 127 | Code: strings.ToUpper(parts[0]), 128 | EnglishName: strings.Trim(parts[1], " "), 129 | }) 130 | } 131 | 132 | return stocks, nil 133 | } 134 | 135 | // 保存 136 | func save(stocks []Stock, filePath string) error { 137 | 138 | // 打开文件 139 | file, err := os.OpenFile(filePath, os.O_CREATE, 0x777) 140 | if err != nil { 141 | return err 142 | } 143 | defer file.Close() 144 | 145 | for _, stock := range stocks { 146 | 147 | line := fmt.Sprintf("%s\t%s\n", stock.Code, stock.EnglishName) 148 | 149 | // 将股票写入文件 150 | _, err = file.WriteString(line) 151 | if err != nil { 152 | return err 153 | } 154 | } 155 | return nil 156 | } 157 | 158 | // 读取 159 | func load(filePath string) ([]Stock, error) { 160 | 161 | // 打开股票列表文件 162 | file, err := os.Open(filePath) 163 | if err != nil { 164 | return nil, err 165 | } 166 | defer file.Close() 167 | 168 | scanner := bufio.NewScanner(file) 169 | stocks := make([]Stock, 0) 170 | for scanner.Scan() { 171 | parts := strings.Split(scanner.Text(), "\t") 172 | if len(parts) != 2 { 173 | return nil, errors.New("股票列表文件格式不正确") 174 | } 175 | 176 | stocks = append(stocks, Stock{ 177 | Code: strings.ToUpper(parts[0]), 178 | EnglishName: parts[1], 179 | }) 180 | } 181 | 182 | return stocks, nil 183 | } 184 | -------------------------------------------------------------------------------- /turtle/index.go: -------------------------------------------------------------------------------- 1 | package turtle 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "math" 9 | "os" 10 | "path/filepath" 11 | 12 | "strconv" 13 | "strings" 14 | 15 | "github.com/nzai/Tast/config" 16 | "github.com/nzai/Tast/history" 17 | "github.com/nzai/Tast/stock" 18 | ) 19 | 20 | type TurtleIndex struct { 21 | Code string 22 | Peroid int 23 | Date string 24 | N float64 // 波动性均值 25 | TR float64 // 真实波动性 26 | } 27 | 28 | const ( 29 | peroidMin = 2 30 | peroidMax = 50 31 | dataFileName = "Turtle.txt" 32 | ) 33 | 34 | // 更新海龟指数 35 | func UpdateAll() error { 36 | 37 | log.Println("开始更新海龟指标") 38 | 39 | // 数据保存目录 40 | dataDir, err := config.GetDataDir() 41 | if err != nil { 42 | return err 43 | } 44 | 45 | // 获取所有股票 46 | stocks, err := stock.GetAll() 47 | if err != nil { 48 | return err 49 | } 50 | 51 | //log.Printf("共有股票%d只", len(stocks)) 52 | 53 | for _, stock := range stocks { 54 | // 更新每只股票的指标 55 | err = updateStock(stock.Code, dataDir) 56 | if err != nil { 57 | log.Fatal(err) 58 | } 59 | } 60 | 61 | log.Println("海龟指标更新完毕") 62 | 63 | return err 64 | } 65 | 66 | func updateStock(code string, dataDir string) error { 67 | // 获取股票每日历史 68 | histories, err := history.GetStockDailyHistory(code, dataDir) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | filePath := filepath.Join(dataDir, code, dataFileName) 74 | _, err = os.Stat(filePath) 75 | if !os.IsNotExist(err) { 76 | // 如果文件存在就跳过不重新计算 77 | return nil 78 | } 79 | //log.Printf("股票%s历史记录有%d天", code, len(histories)) 80 | 81 | allIndex := make(map[int][]TurtleIndex) 82 | chanReceive := make(chan int) 83 | 84 | // 并发计算指标 85 | go func() { 86 | for peroid := peroidMin; peroid <= peroidMax; peroid++ { 87 | go func(p int) { 88 | // 更新股票在周期为peroid时的指数 89 | indexes, err := calculate(histories, p) 90 | if err != nil { 91 | log.Fatal(err) 92 | } 93 | 94 | allIndex[p] = indexes 95 | chanReceive <- 1 96 | }(peroid) 97 | } 98 | }() 99 | 100 | // 阻塞,直到所有股票更新完历史 101 | for peroid := peroidMin; peroid <= peroidMax; peroid++ { 102 | <-chanReceive 103 | } 104 | 105 | // 保存 106 | return save(code, allIndex, filePath) 107 | } 108 | 109 | // 根据股价历史计算指标 110 | func calculate(histories []history.DailyHistory, peroid int) ([]TurtleIndex, error) { 111 | 112 | peroid64 := float64(peroid) 113 | var n, prevn, pdc, tr float64 114 | list := make([]TurtleIndex, 0) 115 | for index, history := range histories { 116 | if index == 0 { 117 | pdc = 0 118 | } else { 119 | pdc = histories[index-1].Close 120 | } 121 | 122 | tr = math.Max(history.High-history.Low, math.Max(history.High-pdc, pdc-history.Low)) 123 | 124 | if index == 0 { 125 | n = tr / peroid64 126 | } else { 127 | n = ((peroid64-1)*prevn + tr) / peroid64 128 | } 129 | 130 | list = append(list, TurtleIndex{ 131 | Code: history.Code, 132 | Peroid: peroid, 133 | Date: history.Date, 134 | N: n, 135 | TR: tr, 136 | }) 137 | } 138 | 139 | return list, nil 140 | } 141 | 142 | // 将指标保存到文件 143 | func save(code string, allIndex map[int][]TurtleIndex, filePath string) error { 144 | // 打开文件 145 | file, err := os.OpenFile(filePath, os.O_CREATE, 0x777) 146 | if err != nil { 147 | return err 148 | } 149 | defer file.Close() 150 | 151 | for peroid := peroidMin; peroid <= peroidMax; peroid++ { 152 | 153 | indexes, found := allIndex[peroid] 154 | if !found { 155 | return errors.New(fmt.Sprintf("保存海龟指标时发现缺失code=%s peroid=%d的指标", code, peroid)) 156 | } 157 | 158 | for _, index := range indexes { 159 | line := fmt.Sprintf("%d\t%s\t%.6f\t%.6f\n", 160 | index.Peroid, 161 | index.Date, 162 | index.N, 163 | index.TR) 164 | 165 | // 将股价写入文件 166 | _, err = file.WriteString(line) 167 | if err != nil { 168 | return err 169 | } 170 | } 171 | } 172 | 173 | return nil 174 | } 175 | 176 | // 从文件中读入指标 177 | func load(code, filePath string) (map[int][]TurtleIndex, error) { 178 | file, err := os.Open(filePath) 179 | if err != nil { 180 | return nil, err 181 | } 182 | defer file.Close() 183 | 184 | scanner := bufio.NewScanner(file) 185 | allIndex := make(map[int][]TurtleIndex) 186 | for scanner.Scan() { 187 | parts := strings.Split(scanner.Text(), "\t") 188 | if len(parts) != 4 { 189 | return nil, errors.New("股票列表文件格式不正确") 190 | } 191 | 192 | peroid64, err := strconv.ParseInt(parts[0], 10, 64) 193 | if err != nil { 194 | return nil, err 195 | } 196 | peroid := int(peroid64) 197 | 198 | n, err := strconv.ParseFloat(parts[2], 64) 199 | if err != nil { 200 | return nil, err 201 | } 202 | 203 | tr, err := strconv.ParseFloat(parts[3], 64) 204 | if err != nil { 205 | return nil, err 206 | } 207 | 208 | indexes, found := allIndex[peroid] 209 | if !found { 210 | allIndex[peroid] = make([]TurtleIndex, 0) 211 | indexes = allIndex[peroid] 212 | } 213 | 214 | indexes = append(indexes, TurtleIndex{ 215 | Code: code, 216 | Peroid: peroid, 217 | Date: parts[1], 218 | N: n, 219 | TR: tr, 220 | }) 221 | } 222 | 223 | return allIndex, nil 224 | } 225 | -------------------------------------------------------------------------------- /peroidexterma/index.go: -------------------------------------------------------------------------------- 1 | package peroidexterma 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "math" 9 | "os" 10 | "path/filepath" 11 | 12 | "strconv" 13 | "strings" 14 | 15 | "github.com/nzai/Tast/config" 16 | "github.com/nzai/Tast/history" 17 | "github.com/nzai/Tast/stock" 18 | ) 19 | 20 | type PeroidExtermaIndex struct { 21 | Code string 22 | Peroid int 23 | Date string 24 | Min float64 // 最小值 25 | Max float64 // 最大值 26 | } 27 | 28 | const ( 29 | peroidMin = 2 30 | peroidMax = 50 31 | dataFileName = "PeroidExterma.txt" 32 | ) 33 | 34 | // 更新区间极值指数 35 | func UpdateAll() error { 36 | 37 | log.Println("开始更新区间极值指标") 38 | 39 | // 数据保存目录 40 | dataDir, err := config.GetDataDir() 41 | if err != nil { 42 | return err 43 | } 44 | 45 | // 获取所有股票 46 | stocks, err := stock.GetAll() 47 | if err != nil { 48 | return err 49 | } 50 | 51 | //log.Printf("共有股票%d只", len(stocks)) 52 | 53 | for _, stock := range stocks { 54 | // 更新每只股票的指标 55 | err = updateStock(stock.Code, dataDir) 56 | if err != nil { 57 | log.Fatal(err) 58 | } 59 | } 60 | 61 | log.Println("区间极值指标更新完毕") 62 | 63 | return err 64 | } 65 | 66 | func updateStock(code string, dataDir string) error { 67 | // 获取股票每日历史 68 | histories, err := history.GetStockDailyHistory(code, dataDir) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | filePath := filepath.Join(dataDir, code, dataFileName) 74 | _, err = os.Stat(filePath) 75 | if !os.IsNotExist(err) { 76 | // 如果文件存在就跳过不重新计算 77 | return nil 78 | } 79 | //log.Printf("股票%s历史记录有%d天", code, len(histories)) 80 | 81 | allIndex := make(map[int][]PeroidExtermaIndex) 82 | chanReceive := make(chan int) 83 | 84 | // 并发计算指标 85 | go func() { 86 | for peroid := peroidMin; peroid <= peroidMax; peroid++ { 87 | go func(p int) { 88 | // 更新股票在周期为peroid时的指数 89 | indexes, err := calculate(histories, p) 90 | if err != nil { 91 | log.Fatal(err) 92 | } 93 | 94 | allIndex[p] = indexes 95 | chanReceive <- 1 96 | }(peroid) 97 | } 98 | }() 99 | 100 | // 阻塞,直到所有股票更新完历史 101 | for peroid := peroidMin; peroid <= peroidMax; peroid++ { 102 | <-chanReceive 103 | } 104 | 105 | // 保存 106 | return save(code, allIndex, filePath) 107 | } 108 | 109 | // 获取股票历史的最大最小值 110 | func peroidExterma(histories []history.DailyHistory) (float64, float64) { 111 | min, max := math.MaxFloat64, -math.MaxFloat64 112 | for _, history := range histories { 113 | if history.Low < min { 114 | min = history.Low 115 | } 116 | 117 | if history.High > max { 118 | max = history.High 119 | } 120 | } 121 | 122 | return min, max 123 | } 124 | 125 | // 根据股价历史计算指标 126 | func calculate(histories []history.DailyHistory, peroid int) ([]PeroidExtermaIndex, error) { 127 | 128 | var min, max float64 129 | list := make([]PeroidExtermaIndex, 0) 130 | queue := make([]history.DailyHistory, 0) 131 | for index, history := range histories { 132 | 133 | if index >= peroid { 134 | queue = append(queue[1:], history) 135 | } else { 136 | queue = append(queue, history) 137 | } 138 | 139 | if index == 0 { 140 | min, max = history.Low, history.High 141 | } else { 142 | min, max = peroidExterma(queue) 143 | } 144 | 145 | list = append(list, PeroidExtermaIndex{ 146 | Code: history.Code, 147 | Peroid: peroid, 148 | Date: history.Date, 149 | Min: min, 150 | Max: max, 151 | }) 152 | } 153 | 154 | return list, nil 155 | } 156 | 157 | // 将指标保存到文件 158 | func save(code string, allIndex map[int][]PeroidExtermaIndex, filePath string) error { 159 | // 打开文件 160 | file, err := os.OpenFile(filePath, os.O_CREATE, 0x777) 161 | if err != nil { 162 | return err 163 | } 164 | defer file.Close() 165 | 166 | for peroid := peroidMin; peroid <= peroidMax; peroid++ { 167 | 168 | indexes, found := allIndex[peroid] 169 | if !found { 170 | return errors.New(fmt.Sprintf("保存区间极值指标时发现缺失code=%s peroid=%d的指标", code, peroid)) 171 | } 172 | 173 | for _, index := range indexes { 174 | line := fmt.Sprintf("%d\t%s\t%.6f\t%.6f\n", 175 | index.Peroid, 176 | index.Date, 177 | index.Max, 178 | index.Min) 179 | 180 | // 将股价写入文件 181 | _, err = file.WriteString(line) 182 | if err != nil { 183 | return err 184 | } 185 | } 186 | } 187 | 188 | return nil 189 | } 190 | 191 | // 从文件中读入指标 192 | func load(code, filePath string) (map[int][]PeroidExtermaIndex, error) { 193 | file, err := os.Open(filePath) 194 | if err != nil { 195 | return nil, err 196 | } 197 | defer file.Close() 198 | 199 | scanner := bufio.NewScanner(file) 200 | allIndex := make(map[int][]PeroidExtermaIndex) 201 | for scanner.Scan() { 202 | parts := strings.Split(scanner.Text(), "\t") 203 | if len(parts) != 4 { 204 | return nil, errors.New("股票列表文件格式不正确") 205 | } 206 | 207 | peroid64, err := strconv.ParseInt(parts[0], 10, 64) 208 | if err != nil { 209 | return nil, err 210 | } 211 | peroid := int(peroid64) 212 | 213 | max, err := strconv.ParseFloat(parts[2], 64) 214 | if err != nil { 215 | return nil, err 216 | } 217 | 218 | min, err := strconv.ParseFloat(parts[3], 64) 219 | if err != nil { 220 | return nil, err 221 | } 222 | 223 | indexes, found := allIndex[peroid] 224 | if !found { 225 | allIndex[peroid] = make([]PeroidExtermaIndex, 0) 226 | indexes = allIndex[peroid] 227 | } 228 | 229 | indexes = append(indexes, PeroidExtermaIndex{ 230 | Code: code, 231 | Peroid: peroid, 232 | Date: parts[1], 233 | Min: min, 234 | Max: max, 235 | }) 236 | } 237 | 238 | return allIndex, nil 239 | } 240 | -------------------------------------------------------------------------------- /trading/turtle.go: -------------------------------------------------------------------------------- 1 | package trading 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/nzai/Tast/config" 10 | "github.com/nzai/Tast/stock" 11 | ) 12 | 13 | const ( 14 | dataFileName = "TradingSystem.txt" 15 | ) 16 | 17 | // 海龟交易系统参数 18 | type TurtleTradingSystemParameter struct { 19 | Holding int 20 | N int 21 | Enter int 22 | Exit int 23 | Stop int 24 | } 25 | 26 | // 海龟交易系统 27 | type TurtleTradingSystem struct { 28 | Codes []string 29 | StartAmount float64 30 | Commission float64 31 | StartDate string 32 | EndDate string 33 | Start TurtleTradingSystemParameter 34 | End TurtleTradingSystemParameter 35 | Current TurtleTradingSystemParameter 36 | CurrentProfit float64 37 | CurrentProfitPercent float64 38 | Best TurtleTradingSystemParameter 39 | BestProfit float64 40 | BestProfitPercent float64 41 | CalculatingAmount int64 42 | CalculatedAmount int64 43 | CalculatedSeconds int64 44 | RemainTips string 45 | } 46 | 47 | func Default() *TurtleTradingSystem { 48 | stocks, err := stock.GetAll() 49 | if err != nil { 50 | log.Fatal("获取股票列表时发生错误:", err) 51 | return nil 52 | } 53 | 54 | codes := make([]string, 0) 55 | for _, s := range stocks { 56 | codes = append(codes, s.Code) 57 | } 58 | 59 | system := &TurtleTradingSystem{ 60 | Codes: codes, 61 | StartAmount: 100000, 62 | Commission: 7, 63 | StartDate: "20060101", 64 | EndDate: "20141231", 65 | Start: TurtleTradingSystemParameter{ 66 | Holding: 2, 67 | N: 2, 68 | Enter: 2, 69 | Exit: 2, 70 | Stop: 2}, 71 | End: TurtleTradingSystemParameter{ 72 | Holding: 20, 73 | N: 50, 74 | Enter: 50, 75 | Exit: 50, 76 | Stop: 50}, 77 | Current: TurtleTradingSystemParameter{ 78 | Holding: 2, 79 | N: 2, 80 | Enter: 2, 81 | Exit: 2, 82 | Stop: 2}, 83 | CurrentProfit: 0, 84 | CurrentProfitPercent: 0, 85 | Best: TurtleTradingSystemParameter{ 86 | Holding: 2, 87 | N: 2, 88 | Enter: 2, 89 | Exit: 2, 90 | Stop: 2}, 91 | BestProfit: 0, 92 | BestProfitPercent: 0, 93 | CalculatedSeconds: 0, 94 | RemainTips: "计算尚未开始", 95 | } 96 | 97 | system.CalculatingAmount = int64(len(system.Codes) * 98 | (system.End.Holding - system.Start.Holding + 1) * 99 | (system.End.N - system.Start.N + 1) * 100 | (system.End.Enter - system.Start.Enter + 1) * 101 | (system.End.Exit - system.Start.Exit + 1) * 102 | (system.End.Stop - system.Start.Stop + 1)) 103 | 104 | return system 105 | } 106 | 107 | var currentTurtleTradingSystem *TurtleTradingSystem = Default() 108 | 109 | func saveSystem() error { 110 | dataDir, err := config.GetDataDir() 111 | if err != nil { 112 | return err 113 | } 114 | 115 | filePath := filepath.Join(dataDir, dataFileName) 116 | // 打开文件 117 | file, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0x777) 118 | if err != nil { 119 | return err 120 | } 121 | defer file.Close() 122 | 123 | file.WriteString(fmt.Sprintf("Codes = %d %v\n", len(currentTurtleTradingSystem.Codes), currentTurtleTradingSystem.Codes)) 124 | file.WriteString(fmt.Sprintf("StartAmount = %f\n", currentTurtleTradingSystem.StartAmount)) 125 | file.WriteString(fmt.Sprintf("Commission = %f\n", currentTurtleTradingSystem.Commission)) 126 | file.WriteString(fmt.Sprintf("StartDate = %s\n", currentTurtleTradingSystem.StartDate)) 127 | file.WriteString(fmt.Sprintf("EndDate = %s\n", currentTurtleTradingSystem.EndDate)) 128 | file.WriteString(fmt.Sprintf("Start\t[Holding = %d N = %d Enter = %d Exit = %d Stop = %d]\n", 129 | currentTurtleTradingSystem.Start.Holding, 130 | currentTurtleTradingSystem.Start.N, 131 | currentTurtleTradingSystem.Start.Enter, 132 | currentTurtleTradingSystem.Start.Exit, 133 | currentTurtleTradingSystem.Start.Stop)) 134 | file.WriteString(fmt.Sprintf("End\t[Holding = %d N = %d Enter = %d Exit = %d Stop = %d]\n", 135 | currentTurtleTradingSystem.End.Holding, 136 | currentTurtleTradingSystem.End.N, 137 | currentTurtleTradingSystem.End.Enter, 138 | currentTurtleTradingSystem.End.Exit, 139 | currentTurtleTradingSystem.End.Stop)) 140 | file.WriteString(fmt.Sprintf("Current\t[Holding = %d N = %d Enter = %d Exit = %d Stop = %d]\n", 141 | currentTurtleTradingSystem.Current.Holding, 142 | currentTurtleTradingSystem.Current.N, 143 | currentTurtleTradingSystem.Current.Enter, 144 | currentTurtleTradingSystem.Current.Exit, 145 | currentTurtleTradingSystem.Current.Stop)) 146 | file.WriteString(fmt.Sprintf("CurrentProfit = %.3f\n", currentTurtleTradingSystem.CurrentProfit)) 147 | file.WriteString(fmt.Sprintf("CurrentProfit = %.3f%%\n", currentTurtleTradingSystem.CurrentProfitPercent*100)) 148 | file.WriteString(fmt.Sprintf("Best\t[Holding = %d N = %d Enter = %d Exit = %d Stop = %d]\n", 149 | currentTurtleTradingSystem.Best.Holding, 150 | currentTurtleTradingSystem.Best.N, 151 | currentTurtleTradingSystem.Best.Enter, 152 | currentTurtleTradingSystem.Best.Exit, 153 | currentTurtleTradingSystem.Best.Stop)) 154 | file.WriteString(fmt.Sprintf("BestProfit = %.3f\n", currentTurtleTradingSystem.BestProfit)) 155 | file.WriteString(fmt.Sprintf("BestProfit = %.3f%%\n", currentTurtleTradingSystem.BestProfitPercent*100)) 156 | file.WriteString(fmt.Sprintf("CalculatingAmount = %d\n", currentTurtleTradingSystem.CalculatingAmount)) 157 | file.WriteString(fmt.Sprintf("CalculatedAmount = %d\n", currentTurtleTradingSystem.CalculatedAmount)) 158 | file.WriteString(fmt.Sprintf("CalculatedSeconds = %d\n", currentTurtleTradingSystem.CalculatedSeconds)) 159 | file.WriteString(fmt.Sprintf("RemainTips = %s\n", currentTurtleTradingSystem.RemainTips)) 160 | 161 | return nil 162 | } 163 | 164 | func TestAll() error { 165 | log.Print("开始测试海龟交易系统") 166 | 167 | // 保存系统 168 | err := saveSystem() 169 | if err != nil { 170 | return err 171 | } 172 | 173 | log.Print("海龟交易系统测试结束") 174 | 175 | return nil 176 | } 177 | 178 | func TestStock(code string) error { 179 | return nil 180 | } 181 | -------------------------------------------------------------------------------- /history/daily.go: -------------------------------------------------------------------------------- 1 | package history 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "log" 10 | "net/http" 11 | "os" 12 | "path/filepath" 13 | "regexp" 14 | "sort" 15 | "strconv" 16 | "strings" 17 | "time" 18 | 19 | "github.com/nzai/Tast/config" 20 | "github.com/nzai/Tast/stock" 21 | ) 22 | 23 | const ( 24 | historyDirName = "History" 25 | dailyDataFileName = "Daily.txt" 26 | updateGoroutinesCount = 8 27 | ) 28 | 29 | // 股票历史 30 | type DailyHistory struct { 31 | Code string 32 | Date string 33 | PrevDate string 34 | Open float64 35 | Close float64 36 | High float64 37 | Low float64 38 | Volume int64 39 | } 40 | 41 | type StockDailyHistories []DailyHistory 42 | 43 | func (slice StockDailyHistories) Len() int { 44 | return len(slice) 45 | } 46 | 47 | func (slice StockDailyHistories) Less(i, j int) bool { 48 | return slice[i].Date < slice[j].Date 49 | } 50 | 51 | func (slice StockDailyHistories) Swap(i, j int) { 52 | slice[i], slice[j] = slice[j], slice[i] 53 | } 54 | 55 | // 更新股票历史 56 | func UpdateAll() error { 57 | 58 | log.Print("开始更新股票历史") 59 | 60 | // 数据保存目录 61 | dataDir, err := config.GetDataDir() 62 | if err != nil { 63 | return err 64 | } 65 | 66 | // 获取所有的股票 67 | stocks, err := stock.GetAll() 68 | if err != nil { 69 | return err 70 | } 71 | 72 | chanSend := make(chan int, updateGoroutinesCount) 73 | chanReceive := make(chan int) 74 | 75 | // 并发获取股票历史 76 | go func() { 77 | for _, stock := range stocks { 78 | go func(code string) { 79 | // 更新每只股票的历史 80 | err = updateStock(code, dataDir) 81 | if err != nil { 82 | log.Fatal(err) 83 | } 84 | <-chanSend 85 | chanReceive <- 1 86 | }(stock.Code) 87 | 88 | chanSend <- 1 89 | } 90 | }() 91 | 92 | // 阻塞,直到所有股票更新完历史 93 | for _, _ = range stocks { 94 | <-chanReceive 95 | } 96 | 97 | log.Print("股票历史更新成功") 98 | 99 | return err 100 | } 101 | 102 | // 更新股票历史 103 | func updateStock(code string, dataDir string) error { 104 | //log.Print(code) 105 | dir := filepath.Join(dataDir, code) 106 | _, err := os.Stat(dir) 107 | if os.IsNotExist(err) { 108 | err = os.Mkdir(dir, 0x777) 109 | if err != nil { 110 | return err 111 | } 112 | } 113 | 114 | return updateStockDaily(code, dir) 115 | } 116 | 117 | // 更新股票每日历史 118 | func updateStockDaily(code string, codeDataDir string) error { 119 | 120 | // 每日历史文件 121 | filePath := filepath.Join(codeDataDir, dailyDataFileName) 122 | _, err := os.Stat(filePath) 123 | if os.IsNotExist(err) { 124 | // 如果文件不存在就从纳斯达克更新股票复权每日历史 125 | _, err := getFromNasdaq(code, filePath) 126 | if err != nil { 127 | return err 128 | } 129 | } 130 | 131 | return nil 132 | } 133 | 134 | // 从纳斯达克更新股票复权每日历史 135 | func getFromNasdaq(code string, filePath string) ([]DailyHistory, error) { 136 | 137 | // 获取记录股票历史股价的纳斯达克页面 138 | html, err := downloadHtmlFromNasdaq(code) 139 | if err != nil { 140 | return nil, err 141 | } 142 | 143 | // 从html中抓取股票历史股价 144 | histories, err := parseHtml(code, html) 145 | if err != nil { 146 | return nil, err 147 | } 148 | 149 | // 保存 150 | err = saveToFile(code, histories, filePath) 151 | if err != nil { 152 | return nil, err 153 | } 154 | 155 | return histories, nil 156 | } 157 | 158 | // 获取记录股票历史股价的纳斯达克页面 159 | func downloadHtmlFromNasdaq(code string) (string, error) { 160 | queryPattern := `http://www.nasdaq.com/symbol/%s/historical` 161 | 162 | // 查询最近10年的除权股价及交易量 163 | url := fmt.Sprintf(queryPattern, strings.ToLower(code)) 164 | payload := []byte(fmt.Sprintf("10y|false|%s", code)) 165 | // log.Printf("url:%s payload:%s", url, payload) 166 | 167 | request, err := http.NewRequest("POST", url, bytes.NewBuffer(payload)) 168 | if err != nil { 169 | return "", err 170 | } 171 | request.Header.Set("Content-Type", "application/json") 172 | 173 | client := &http.Client{} 174 | // client.Timeout = time.Second * 60 175 | response, err := client.Do(request) 176 | if err != nil { 177 | return "", err 178 | } 179 | defer response.Body.Close() 180 | 181 | buffer, err := ioutil.ReadAll(response.Body) 182 | if err != nil { 183 | return "", err 184 | } 185 | 186 | return string(buffer), nil 187 | } 188 | 189 | // 从html中抓取股票历史股价 190 | func parseHtml(code string, html string) ([]DailyHistory, error) { 191 | 192 | matchPattern := `\s+\s+([\d\/]+)\s+\s+\s+([\d\.]+)\s+\s+\s+([\d\.]+)\s+\s+\s+([\d\.]+)\s+\s+\s+([\d\.]+)\s+\s+\s+([\d\.,]+)\s+\s+` 193 | 194 | regex := regexp.MustCompile(matchPattern) 195 | matches := regex.FindAllStringSubmatch(html, -1) 196 | readLayout := "01/02/2006" 197 | writeLayout := "20060102" 198 | 199 | // log.Print(len(matches)) 200 | histories := make([]DailyHistory, 0) 201 | for _, match := range matches { 202 | if len(match) != 7 { 203 | return nil, errors.New("纳斯达克股票历史格式不正确" + fmt.Sprint(match)) 204 | } 205 | 206 | date, err := time.Parse(readLayout, match[1]) 207 | if err != nil { 208 | return nil, err 209 | } 210 | 211 | // log.Print(match) 212 | open, err := strconv.ParseFloat(match[2], 64) 213 | if err != nil { 214 | return nil, err 215 | } 216 | 217 | high, err := strconv.ParseFloat(match[3], 64) 218 | if err != nil { 219 | return nil, err 220 | } 221 | 222 | low, err := strconv.ParseFloat(match[4], 64) 223 | if err != nil { 224 | return nil, err 225 | } 226 | 227 | _close, err := strconv.ParseFloat(match[5], 64) 228 | if err != nil { 229 | return nil, err 230 | } 231 | 232 | volume, err := strconv.ParseInt(strings.Replace(match[6], ",", "", -1), 10, 64) 233 | if err != nil { 234 | return nil, err 235 | } 236 | 237 | histories = append(histories, DailyHistory{ 238 | Code: code, 239 | Date: date.Format(writeLayout), 240 | Open: open, 241 | Close: _close, 242 | High: high, 243 | Low: low, 244 | Volume: volume, 245 | }) 246 | } 247 | 248 | // 下载的数据是日期倒序排序的,需要重新排序一下 249 | for index, _ := range histories { 250 | if index == len(histories)-1 { 251 | histories[index].PrevDate = "" 252 | } else { 253 | histories[index].PrevDate = histories[index+1].Date 254 | } 255 | } 256 | 257 | // 将股票历史按照日期正序排序 258 | sort.Sort(StockDailyHistories(histories)) 259 | 260 | return histories, nil 261 | } 262 | 263 | // 保存股票历史 264 | func saveToFile(code string, histories []DailyHistory, filePath string) error { 265 | // 打开文件 266 | file, err := os.OpenFile(filePath, os.O_CREATE, 0x777) 267 | if err != nil { 268 | return err 269 | } 270 | defer file.Close() 271 | 272 | for _, history := range histories { 273 | 274 | line := fmt.Sprintf("%s\t%.6f\t%.6f\t%.6f\t%.6f\t%d\t%s\n", 275 | history.Date, 276 | history.Open, 277 | history.Close, 278 | history.High, 279 | history.Low, 280 | history.Volume, 281 | history.PrevDate) 282 | 283 | // 将股价写入文件 284 | _, err = file.WriteString(line) 285 | if err != nil { 286 | return err 287 | } 288 | } 289 | 290 | return nil 291 | } 292 | 293 | // 获取文件每日历史 294 | func GetStockDailyHistory(code, dataDir string) ([]DailyHistory, error) { 295 | 296 | codeDailyFileName := filepath.Join(dataDir, code, dailyDataFileName) 297 | 298 | _, err := os.Stat(codeDailyFileName) 299 | if os.IsNotExist(err) { 300 | // 如果文件不存在就从纳斯达克获取股票复权每日历史 301 | return getFromNasdaq(code, codeDailyFileName) 302 | } 303 | 304 | return loadFromFile(code, codeDailyFileName) 305 | } 306 | 307 | // 从文件读取股票每日历史 308 | func loadFromFile(code, filePath string) ([]DailyHistory, error) { 309 | 310 | file, err := os.Open(filePath) 311 | if err != nil { 312 | return nil, err 313 | } 314 | defer file.Close() 315 | 316 | scanner := bufio.NewScanner(file) 317 | histories := make([]DailyHistory, 0) 318 | for scanner.Scan() { 319 | parts := strings.Split(scanner.Text(), "\t") 320 | if len(parts) != 7 { 321 | return nil, errors.New("股票列表文件格式不正确") 322 | } 323 | 324 | open, err := strconv.ParseFloat(parts[1], 64) 325 | if err != nil { 326 | return nil, err 327 | } 328 | 329 | _close, err := strconv.ParseFloat(parts[2], 64) 330 | if err != nil { 331 | return nil, err 332 | } 333 | 334 | high, err := strconv.ParseFloat(parts[3], 64) 335 | if err != nil { 336 | return nil, err 337 | } 338 | 339 | low, err := strconv.ParseFloat(parts[4], 64) 340 | if err != nil { 341 | return nil, err 342 | } 343 | 344 | volume, err := strconv.ParseInt(parts[5], 10, 64) 345 | if err != nil { 346 | return nil, err 347 | } 348 | 349 | histories = append(histories, DailyHistory{ 350 | Code: code, 351 | Date: parts[0], 352 | PrevDate: parts[6], 353 | Open: open, 354 | Close: _close, 355 | High: high, 356 | Low: low, 357 | Volume: volume, 358 | }) 359 | } 360 | 361 | return histories, nil 362 | } 363 | --------------------------------------------------------------------------------