-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathstock.go
210 lines (182 loc) · 5.46 KB
/
stock.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
package portfolio
import (
"fmt"
"strconv"
)
// NewStock returns pointer to a new Stock structure
// for a given stock ticker. Assumes data, both daily close
// and dividend files, are in the "data/" directory.
// This set of functions assumes the data is from Yahoo history.
// TODO: convert this to Alpha Advantage data.
func NewStock(ticker string) (*Stock, error) {
result := Stock{Ticker: ticker}
err := result.readHistory()
return &result, err
}
// ReadHistory reads CSV files to load history for a stock.
// The "Ticker" value must already be set.
//
// Current directory must contain three files:
// - data/{Ticker}.csv - daily history of stocks
// with minimum of "Date" and "Close" columns
// - data/{Ticker}_div.csv - history of stock dividends
// with minimum of "Date" and "Dividends" columns
// - data/{Ticker}_distr.csv - history of capital gains distriubtions
// with minimum of "Date" and "Distribution" columns
//
// Currently assumes that:
// 1. "Date" columns are the first (0 index) column in all three CSV files
// 2. "Close" is column index 4 in the daily close CSV
// 3. "Dividends" is column index 1 in the dividends CSV
// 4. "Distributions" is column index 1 in the distributions CSV
//
func (s *Stock) readHistory() error {
if err := s.readCloseData(); err != nil {
return err
}
if err := s.readDivData(); err != nil {
return err
}
return s.readDistrData()
}
// readCloseData reads the stock market date
// and close amount.
func (s *Stock) readCloseData() error {
// init date and close price for stock
csv, err := readCSVFile("data/" + s.Ticker + ".csv")
if err != nil {
return err
}
dayCount := len(csv) - 1
s.History = make([]StockHistory, dayCount)
dateIdx := 0
closeIdx := 4
for i, day := range csv {
if i == 0 {
if day[dateIdx] != "Date" {
return fmt.Errorf("daily close file for %s does not start with 'Date'", s.Ticker)
}
if day[closeIdx] != "Close" {
return fmt.Errorf("daily close file for %s does not have 'Close' in expected column", s.Ticker)
}
} else {
s.History[i-1].Date = day[dateIdx]
s.History[i-1].Close, err = strconv.ParseFloat(day[closeIdx], 64)
if err != nil {
return fmt.Errorf("invalid float in stock history file for %s, line %d, %v",
s.Ticker, i, err)
}
}
}
return nil
}
// readDivData reads dividend amounts and adds them
// to the existing close data.
func (s *Stock) readDivData() error {
dateIdx := 0
dividendsIdx := 1
csv, err := readCSVFile("data/" + s.Ticker + "_div.csv")
if err != nil {
return err
}
for i, day := range csv {
if i == 0 {
if day[dateIdx] != "Date" {
return fmt.Errorf("dividend file for %s does not start with 'Date'", s.Ticker)
}
if day[dividendsIdx] != "Dividends" {
return fmt.Errorf("dividend file for %s does not have 'Dividends' in expected column", s.Ticker)
}
} else {
for j, history := range s.History {
if history.Date >= day[dateIdx] {
dividend, err := strconv.ParseFloat(day[dividendsIdx], 64)
if err != nil {
return fmt.Errorf("invalid float in stock dividends file for %s, line %d, %v",
s.Ticker, i, err)
}
s.History[j].Dividend += dividend
break
}
}
}
}
return nil
}
// readDistrData reads capital gains distribution data
// and adds them to the existing close and dividends data.
// Note that if the daily history file does not contain the stock distribution file date
// the dividend will be shown on the following day (but shouldn't happen?)
func (s *Stock) readDistrData() error {
dateIdx := 0
distributionIdx := 1
csv, err := readCSVFile("data/" + s.Ticker + "_distr.csv")
if err != nil {
return err
}
for i, day := range csv {
if i == 0 {
if day[dateIdx] != "Date" {
return fmt.Errorf("distribution file for %s does not start with 'Date'", s.Ticker)
}
if day[distributionIdx] != "Distributions" {
return fmt.Errorf("distributions file for %s does not have 'Distributions' in expected column", s.Ticker)
}
} else {
for j, history := range s.History {
if history.Date >= day[dateIdx] {
distribution, err := strconv.ParseFloat(day[distributionIdx], 64)
if err != nil {
return fmt.Errorf("invalid float in stock distributions file for %s, line %d, %v",
s.Ticker, i, err)
}
s.History[j].Distribution += distribution
break
}
}
}
}
return nil
}
// getHistIdx gets the index of the stock history entry
// where the date is <= a given date.
func (s *Stock) getHistIdx(date string, startIdx int) int {
result := startIdx
for i := startIdx + 1; i < len(s.History); i++ {
if s.History[i].Date <= date {
result = i
} else {
break
}
}
return result
}
// getNextDate returns the next stock history date
// which is greater than lastDate.
// Starts searching stock history at beginIdx.
// If no next history date available, returns the constant MaxDate
func (s *Stock) getNextDate(lastDate string, beginIdx int) string {
for i := beginIdx; i < len(s.History); i++ {
if s.History[i].Date > lastDate {
return s.History[i].Date
}
}
return MaxDate
}
// getCloseDateIdx returns the index of the stock close date
// which is on or before the specified date.
// Starts search in stock history at beginIdx.
func (s *Stock) getCloseDateIdx(closeDate string, beginIdx int) int {
result := beginIdx
for i := beginIdx + 1; i < len(s.History); i++ {
if s.History[i].Date <= closeDate {
result = i
} else {
return result
}
}
if result != beginIdx {
return result
}
return -1
}