• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

achannarasappa / ticker / 15099751812

18 May 2025 08:28PM UTC coverage: 88.649% (-4.5%) from 93.13%
15099751812

push

github

achannarasappa
fix: setting initial price on row

7 of 7 new or added lines in 1 file covered. (100.0%)

40 existing lines in 6 files now uncovered.

2835 of 3198 relevant lines covered (88.65%)

8.45 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.86
/internal/cli/cli.go
1
package cli
2

3
import (
4
        "errors"
5
        "fmt"
6
        "log"
7
        "os"
8
        "strings"
9
        "time"
10

11
        "github.com/achannarasappa/ticker/v4/internal/cli/symbol"
12
        c "github.com/achannarasappa/ticker/v4/internal/common"
13
        "github.com/achannarasappa/ticker/v4/internal/ui/util"
14

15
        "github.com/adrg/xdg"
16
        "github.com/mitchellh/go-homedir"
17
        "github.com/spf13/afero"
18
        "github.com/spf13/cobra"
19
        "github.com/spf13/viper"
20
        "gopkg.in/yaml.v2"
21
)
22

23
// Options to configured ticker behavior
24
type Options struct {
25
        RefreshInterval       int
26
        Watchlist             string
27
        Separate              bool
28
        ExtraInfoExchange     bool
29
        ExtraInfoFundamentals bool
30
        ShowSummary           bool
31
        ShowHoldings          bool
32
        Sort                  string
33
}
34

35
type symbolSource struct {
36
        symbol string
37
        source c.QuoteSource
38
}
39

40
// Run starts the ticker UI
41
func Run(uiStartFn func() error) func(*cobra.Command, []string) {
2✔
42
        return func(_ *cobra.Command, _ []string) {
4✔
43
                err := uiStartFn()
2✔
44

2✔
45
                if err != nil {
3✔
46
                        fmt.Println(fmt.Errorf("unable to start UI: %w", err).Error())
1✔
47
                }
1✔
48
        }
49
}
50

51
// Validate checks whether config is valid and returns an error if invalid or if an error was generated earlier
52
func Validate(config *c.Config, options *Options, prevErr *error) func(*cobra.Command, []string) error {
4✔
53
        return func(_ *cobra.Command, _ []string) error {
8✔
54

4✔
55
                if prevErr != nil && *prevErr != nil {
5✔
56
                        return *prevErr
1✔
57
                }
1✔
58

59
                if len(config.Watchlist) == 0 && len(options.Watchlist) == 0 && len(config.Lots) == 0 && len(config.AssetGroup) == 0 {
4✔
60
                        return errors.New("invalid config: No watchlist provided") //nolint:goerr113
1✔
61
                }
1✔
62

63
                return nil
2✔
64
        }
65
}
66

67
func GetDependencies() c.Dependencies {
1✔
68
        return c.Dependencies{
1✔
69
                Fs:                               afero.NewOsFs(),
1✔
70
                SymbolsURL:                       "https://raw.githubusercontent.com/achannarasappa/ticker-static/master/symbols.csv",
1✔
71
                MonitorYahooBaseURL:              "https://query1.finance.yahoo.com",
1✔
72
                MonitorYahooSessionRootURL:       "https://finance.yahoo.com",
1✔
73
                MonitorYahooSessionCrumbURL:      "https://query2.finance.yahoo.com",
1✔
74
                MonitorYahooSessionConsentURL:    "https://consent.yahoo.com",
1✔
75
                MonitorPriceCoinbaseBaseURL:      "https://api.coinbase.com",
1✔
76
                MonitorPriceCoinbaseStreamingURL: "wss://ws-feed.exchange.coinbase.com",
1✔
77
        }
1✔
78
}
1✔
79

80
// GetContext builds the context from the config and reference data
81
func GetContext(d c.Dependencies, config c.Config) (c.Context, error) {
8✔
82
        var (
8✔
83
                reference c.Reference
8✔
84
                groups    []c.AssetGroup
8✔
85
                err       error
8✔
86
        )
8✔
87

8✔
88
        if err != nil {
8✔
UNCOV
89
                return c.Context{}, err
×
UNCOV
90
        }
×
91

92
        groups, err = getGroups(config, d)
8✔
93

8✔
94
        if err != nil {
9✔
95
                return c.Context{}, err
1✔
96
        }
1✔
97

98
        reference, err = getReference(config, groups)
7✔
99

7✔
100
        if err != nil {
7✔
UNCOV
101
                return c.Context{}, err
×
UNCOV
102
        }
×
103

104
        var logger *log.Logger
7✔
105

7✔
106
        if config.Debug {
8✔
107
                logger, err = getLogger(d)
1✔
108

1✔
109
                if err != nil {
2✔
110
                        return c.Context{}, err
1✔
111
                }
1✔
112
        }
113

114
        context := c.Context{
6✔
115
                Reference: reference,
6✔
116
                Config:    config,
6✔
117
                Groups:    groups,
6✔
118
                Logger:    logger,
6✔
119
        }
6✔
120

6✔
121
        return context, err
6✔
122
}
123

124
func readConfig(fs afero.Fs, configPathOption string) (c.Config, error) {
20✔
125
        var config c.Config
20✔
126
        configPath, err := getConfigPath(fs, configPathOption)
20✔
127

20✔
128
        if err != nil {
25✔
129
                return config, nil //nolint:nilerr
5✔
130
        }
5✔
131
        handle, err := fs.Open(configPath)
15✔
132

15✔
133
        if err != nil {
16✔
134
                return config, fmt.Errorf("invalid config: %w", err)
1✔
135
        }
1✔
136

137
        defer handle.Close()
14✔
138
        err = yaml.NewDecoder(handle).Decode(&config)
14✔
139

14✔
140
        if err != nil {
15✔
141
                return config, fmt.Errorf("invalid config: %w", err)
1✔
142
        }
1✔
143

144
        return config, nil
13✔
145
}
146

147
func getReference(config c.Config, assetGroups []c.AssetGroup) (c.Reference, error) {
7✔
148

7✔
149
        var err error
7✔
150

7✔
151
        styles := util.GetColorScheme(config.ColorScheme)
7✔
152

7✔
153
        if err != nil {
7✔
UNCOV
154
                return c.Reference{}, err
×
UNCOV
155
        }
×
156

157
        return c.Reference{
7✔
158
                Styles: styles,
7✔
159
        }, err
7✔
160

161
}
162

163
func GetConfig(dep c.Dependencies, configPath string, options Options) (c.Config, error) {
20✔
164

20✔
165
        config, err := readConfig(dep.Fs, configPath)
20✔
166

20✔
167
        if err != nil {
22✔
168
                return c.Config{}, err
2✔
169
        }
2✔
170

171
        if len(options.Watchlist) != 0 {
20✔
172
                config.Watchlist = strings.Split(strings.ReplaceAll(options.Watchlist, " ", ""), ",")
2✔
173
        }
2✔
174

175
        config.RefreshInterval = getRefreshInterval(options.RefreshInterval, config.RefreshInterval)
18✔
176
        config.Separate = getBoolOption(options.Separate, config.Separate)
18✔
177
        config.ExtraInfoExchange = getBoolOption(options.ExtraInfoExchange, config.ExtraInfoExchange)
18✔
178
        config.ExtraInfoFundamentals = getBoolOption(options.ExtraInfoFundamentals, config.ExtraInfoFundamentals)
18✔
179
        config.ShowSummary = getBoolOption(options.ShowSummary, config.ShowSummary)
18✔
180
        config.ShowHoldings = getBoolOption(options.ShowHoldings, config.ShowHoldings)
18✔
181
        config.Sort = getStringOption(options.Sort, config.Sort)
18✔
182

18✔
183
        return config, nil
18✔
184
}
185

186
func getConfigPath(fs afero.Fs, configPathOption string) (string, error) {
20✔
187
        var err error
20✔
188
        if configPathOption != "" {
23✔
189
                return configPathOption, nil
3✔
190
        }
3✔
191

192
        home, _ := homedir.Dir()
17✔
193

17✔
194
        v := viper.New()
17✔
195
        v.SetFs(fs)
17✔
196
        v.SetConfigType("yaml")
17✔
197
        v.AddConfigPath(home)
17✔
198
        v.AddConfigPath(".")
17✔
199
        v.AddConfigPath(xdg.ConfigHome)
17✔
200
        v.AddConfigPath(xdg.ConfigHome + "/ticker")
17✔
201
        v.SetConfigName(".ticker")
17✔
202
        err = v.ReadInConfig()
17✔
203

17✔
204
        if err != nil {
22✔
205
                return "", fmt.Errorf("invalid config: %w", err)
5✔
206
        }
5✔
207

208
        return v.ConfigFileUsed(), nil
12✔
209
}
210

211
func getRefreshInterval(optionsRefreshInterval int, configRefreshInterval int) int {
18✔
212

18✔
213
        if optionsRefreshInterval > 0 {
20✔
214
                return optionsRefreshInterval
2✔
215
        }
2✔
216

217
        if configRefreshInterval > 0 {
17✔
218
                return configRefreshInterval
1✔
219
        }
1✔
220

221
        return 5
15✔
222
}
223

224
func getBoolOption(cliValue bool, configValue bool) bool {
90✔
225

90✔
226
        if cliValue {
91✔
227
                return cliValue
1✔
228
        }
1✔
229

230
        if configValue {
91✔
231
                return configValue
2✔
232
        }
2✔
233

234
        return false
87✔
235
}
236

237
func getStringOption(cliValue string, configValue string) string {
18✔
238

18✔
239
        if cliValue != "" {
18✔
UNCOV
240
                return cliValue
×
UNCOV
241
        }
×
242

243
        if configValue != "" {
18✔
UNCOV
244
                return configValue
×
UNCOV
245
        }
×
246

247
        return ""
18✔
248
}
249

250
func getGroups(config c.Config, d c.Dependencies) ([]c.AssetGroup, error) {
8✔
251

8✔
252
        groups := make([]c.AssetGroup, 0)
8✔
253
        var configAssetGroups []c.ConfigAssetGroup
8✔
254

8✔
255
        tickerSymbolToSourceSymbol, err := symbol.GetTickerSymbols(d.SymbolsURL)
8✔
256

8✔
257
        if err != nil {
9✔
258
                return []c.AssetGroup{}, err
1✔
259
        }
1✔
260

261
        if len(config.Watchlist) > 0 || len(config.Lots) > 0 {
12✔
262
                configAssetGroups = append(configAssetGroups, c.ConfigAssetGroup{
5✔
263
                        Name:      "default",
5✔
264
                        Watchlist: config.Watchlist,
5✔
265
                        Holdings:  config.Lots,
5✔
266
                })
5✔
267
        }
5✔
268

269
        configAssetGroups = append(configAssetGroups, config.AssetGroup...)
7✔
270

7✔
271
        for _, configAssetGroup := range configAssetGroups {
14✔
272

7✔
273
                symbols := make(map[string]bool)
7✔
274
                symbolsUnique := make(map[c.QuoteSource]c.AssetGroupSymbolsBySource)
7✔
275
                var assetGroupSymbolsBySource []c.AssetGroupSymbolsBySource
7✔
276

7✔
277
                for _, symbol := range configAssetGroup.Watchlist {
21✔
278
                        if !symbols[symbol] {
28✔
279
                                symbols[symbol] = true
14✔
280
                                symbolAndSource := getSymbolAndSource(symbol, tickerSymbolToSourceSymbol)
14✔
281
                                symbolsUnique = appendSymbol(symbolsUnique, symbolAndSource)
14✔
282
                        }
14✔
283
                }
284

285
                for _, lot := range configAssetGroup.Holdings {
8✔
286
                        if !symbols[lot.Symbol] {
2✔
287
                                symbols[lot.Symbol] = true
1✔
288
                                symbolAndSource := getSymbolAndSource(lot.Symbol, tickerSymbolToSourceSymbol)
1✔
289
                                symbolsUnique = appendSymbol(symbolsUnique, symbolAndSource)
1✔
290
                        }
1✔
291
                }
292

293
                for _, symbolsBySource := range symbolsUnique {
15✔
294
                        assetGroupSymbolsBySource = append(assetGroupSymbolsBySource, symbolsBySource)
8✔
295
                }
8✔
296

297
                groups = append(groups, c.AssetGroup{
7✔
298
                        ConfigAssetGroup: configAssetGroup,
7✔
299
                        SymbolsBySource:  assetGroupSymbolsBySource,
7✔
300
                })
7✔
301

302
        }
303

304
        return groups, nil
7✔
305

306
}
307

308
func getLogger(d c.Dependencies) (*log.Logger, error) {
1✔
309
        // Create log file with current date
1✔
310
        currentTime := time.Now()
1✔
311
        logFileName := fmt.Sprintf("ticker-log-%s.log", currentTime.Format("2006-01-02"))
1✔
312
        logFile, err := d.Fs.OpenFile(logFileName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
1✔
313
        if err != nil {
2✔
314
                return nil, fmt.Errorf("failed to create log file: %w", err)
1✔
315
        }
1✔
316

UNCOV
317
        return log.New(logFile, "", log.LstdFlags), nil
×
318
}
319

320
func getSymbolAndSource(symbol string, tickerSymbolToSourceSymbol symbol.TickerSymbolToSourceSymbol) symbolSource {
15✔
321

15✔
322
        symbolUppercase := strings.ToUpper(symbol)
15✔
323

15✔
324
        if strings.HasSuffix(symbolUppercase, ".CB") {
17✔
325

2✔
326
                symbol = strings.ToUpper(symbol)[:len(symbol)-3]
2✔
327

2✔
328
                // Futures contracts on Coinbase Derivatives Exchange are implicitly USD-denominated
2✔
329
                if strings.HasSuffix(symbol, "-CDE") {
3✔
330
                        return symbolSource{
1✔
331
                                source: c.QuoteSourceCoinbase,
1✔
332
                                symbol: symbol,
1✔
333
                        }
1✔
334
                }
1✔
335

336
                return symbolSource{
1✔
337
                        source: c.QuoteSourceCoinbase,
1✔
338
                        symbol: symbol + "-USD",
1✔
339
                }
1✔
340
        }
341

342
        if strings.HasSuffix(symbolUppercase, ".X") {
14✔
343

1✔
344
                if tickerSymbolToSource, exists := tickerSymbolToSourceSymbol[symbolUppercase]; exists {
2✔
345

1✔
346
                        return symbolSource{
1✔
347
                                source: tickerSymbolToSource.Source,
1✔
348
                                symbol: tickerSymbolToSource.SourceSymbol,
1✔
349
                        }
1✔
350

1✔
351
                }
1✔
352

353
        }
354

355
        return symbolSource{
12✔
356
                source: c.QuoteSourceYahoo,
12✔
357
                symbol: symbolUppercase,
12✔
358
        }
12✔
359

360
}
361

362
func appendSymbol(symbolsUnique map[c.QuoteSource]c.AssetGroupSymbolsBySource, symbolAndSource symbolSource) map[c.QuoteSource]c.AssetGroupSymbolsBySource {
15✔
363

15✔
364
        if symbolsBySource, ok := symbolsUnique[symbolAndSource.source]; ok {
22✔
365

7✔
366
                symbolsBySource.Symbols = append(symbolsBySource.Symbols, symbolAndSource.symbol)
7✔
367

7✔
368
                symbolsUnique[symbolAndSource.source] = symbolsBySource
7✔
369

7✔
370
                return symbolsUnique
7✔
371
        }
7✔
372

373
        symbolsUnique[symbolAndSource.source] = c.AssetGroupSymbolsBySource{
8✔
374
                Source:  symbolAndSource.source,
8✔
375
                Symbols: []string{symbolAndSource.symbol},
8✔
376
        }
8✔
377

8✔
378
        return symbolsUnique
8✔
379

380
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc