├── 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 |
--------------------------------------------------------------------------------