Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions internal/adapters/market/crypto_price_provider_coingecko.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func (p *CoinGeckoProvider) GetQuote(ctx context.Context, ticker string) (domain
q := u.Query()
q.Set("ids", coinID)
q.Set("vs_currencies", "usd")
q.Set("include_24hr_change", "true")
u.RawQuery = q.Encode()

req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
Expand Down Expand Up @@ -72,10 +73,20 @@ func (p *CoinGeckoProvider) GetQuote(ctx context.Context, ticker string) (domain
return domain.Quote{}, fmt.Errorf("coingecko returned no usd price for %q", coinID)
}

changePctPercent := coinData["usd_24h_change"]
changePctRatio := changePctPercent / 100

previous := price / (1 + changePctRatio)
change := price - previous

return domain.Quote{
Ticker: strings.ToUpper(strings.TrimSpace(ticker)),
Price: price,
PriceCurrency: "USD",
PreviousClose: previous,
Change: change,
ChangePercent: changePctPercent,
Source: "coingecko",
}, nil
}

Expand Down
8 changes: 4 additions & 4 deletions internal/adapters/news/newsapi_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func (p *NewsAPIProvider) GetNews(
ctx context.Context,
ticker string,
limit int,
) (domain.TickerNewsReport, error) {
) (domain.NewsSummary, error) {

query := buildQuery(ticker)

Expand All @@ -49,7 +49,7 @@ func (p *NewsAPIProvider) GetNews(

resp, err := p.httpClient.Do(req)
if err != nil {
return domain.TickerNewsReport{}, err
return domain.NewsSummary{}, err
}
defer func() {
_ = resp.Body.Close()
Expand All @@ -62,10 +62,10 @@ func (p *NewsAPIProvider) GetNews(
}

if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil {
return domain.TickerNewsReport{}, err
return domain.NewsSummary{}, err
}

out := domain.TickerNewsReport{
out := domain.NewsSummary{
Ticker: ticker,
}

Expand Down
4 changes: 2 additions & 2 deletions internal/adapters/news/static_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ func (p *StaticProvider) GetNews(
_ context.Context,
symbol string,
limit int,
) (domain.TickerNewsReport, error) {
report := domain.TickerNewsReport{
) (domain.NewsSummary, error) {
report := domain.NewsSummary{
Ticker: symbol,
}

Expand Down
4 changes: 3 additions & 1 deletion internal/app/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,9 @@ func (c Config) String() string {
}

rows := []kv{
{"PRICE_PROVIDER", c.EquityPriceProvider},
{"EQUITY_PRICE_PROVIDER", c.EquityPriceProvider},
{"FX_PROVIDER", c.FXProvider},
{"CRYPTO_PRICE_PROVIDER", c.CryptoPriceProvider},
{"CACHE_ENABLED", fmt.Sprintf("%t", c.CacheEnabled)},
{"CACHE_PRICE_TTL", c.PriceCacheTTL.String()},
{"CACHE_FX_TTL", c.FXCacheTTL.String()},
Expand All @@ -158,6 +159,7 @@ func (c Config) String() string {
{"", ""}, // spacer

{"FINNHUB_API_KEY", mask(c.FinnhubAPIKey)},
{"NEWSAPIORG_API_KEY", mask(c.NewsAPIOrgAPIKey)},
}

// find max key length
Expand Down
2 changes: 1 addition & 1 deletion internal/app/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ type (
Upsert(ctx context.Context, cached repository.CachedFXRate) error
}
NewsProvider interface {
GetNews(ctx context.Context, symbol string, limit int) (domain.TickerNewsReport, error)
GetNews(ctx context.Context, symbol string, limit int) (domain.NewsSummary, error)
}
LLMProvider interface {
Complete(ctx context.Context, req llm.CompletionRequest) (llm.CompletionResponse, error)
Expand Down
9 changes: 6 additions & 3 deletions internal/app/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type Runtime struct {
GetDailyReport *usecase.GetDailyReportUseCase
ImportPortfolio *usecase.ImportPortfolioUseCase
GetTickerNews *usecase.GetTickerNewsUseCase
GetMorningBrief *usecase.GetMorningBriefUsecase
}

func BuildRuntime(dbPath string) (*Runtime, error) {
Expand All @@ -38,7 +39,7 @@ func BuildRuntime(dbPath string) (*Runtime, error) {
portfolioRepo := repository.NewPortfolioRepository(database)
positionRepo := repository.NewPositionRepository(database)
instrumentRepo := repository.NewInstrumentRepository(database)
portfolioSnapshotRepo := repository.NewPortfolioSnapshotRepository(database)
snapshotRepo := repository.NewSnapshotRepository(database)

// Caching
priceCacher := repository.NewPriceCacheRepository(database)
Expand Down Expand Up @@ -79,13 +80,14 @@ func BuildRuntime(dbPath string) (*Runtime, error) {
portfolioSvc := service.NewPortfolioService(portfolioRepo, positionRepo, portfolioAnalyser, riskAnalyser)
portfolioInsights := service.NewPortfolioInsights()
newsSvc := service.NewNewsService(newsProvider)
snapshotSvc := service.NewSnapshotService(snapshotRepo)

instrumentResolver, err := instruments.NewStaticResolver()
if err != nil {
return nil, err
}

reportingBuilder := report.NewReportBuilder(portfolioSvc, newsSvc, portfolioInsights)
reportingBuilder := report.NewReportBuilder(portfolioSvc, pricingSvc, newsSvc, portfolioInsights, snapshotSvc)

var summarizer usecase.DailyReportSummarizer = service.NoopSummarizer{}

Expand All @@ -102,8 +104,9 @@ func BuildRuntime(dbPath string) (*Runtime, error) {
GetPortfolioRisk: usecase.NewGetPortfolioRiskUseCase(
portfolioSvc,
),
GetDailyReport: usecase.NewGetDailyReportUseCase(reportingBuilder, summarizer, portfolioSnapshotRepo),
GetDailyReport: usecase.NewGetDailyReportUseCase(reportingBuilder, summarizer, snapshotRepo),
ImportPortfolio: usecase.NewImportPortfolioUseCase(positionRepo, portfolioRepo, instrumentRepo),
GetTickerNews: usecase.NewGetTickerNewsUseCase(newsSvc),
GetMorningBrief: usecase.NewGetMorningBriefUsecase(reportingBuilder),
}, nil
}
31 changes: 31 additions & 0 deletions internal/cli/brief.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package cli

import (
"github.com/spf13/cobra"
"github.com/squeakycheese75/tick/internal/domain"
"github.com/squeakycheese75/tick/internal/render"
)

func newBriefCmd(runtimeBuilder RuntimeBuilder) *cobra.Command {
return &cobra.Command{
Use: "brief",
Short: "Show your morning market and portfolio brief",
RunE: func(cmd *cobra.Command, args []string) error {
rt, err := runtimeBuilder()
if err != nil {
return err
}

out, err := rt.GetMorningBrief.Execute(cmd.Context(), domain.GetMorningBriefUsecaseInput{
PortfolioName: "main",
})
if err != nil {
return err
}

opts := render.DefaultBriefReportOptions()

return render.RenderBriefReport(cmd.OutOrStdout(), out.Report, opts)
},
}
}
1 change: 1 addition & 0 deletions internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ func NewRootCmd(runtimeBuilder RuntimeBuilder) *cobra.Command {

rootCmd.AddCommand(newVersionCmd())
rootCmd.AddCommand(newDailyCmd(runtimeBuilder))
rootCmd.AddCommand(newBriefCmd(runtimeBuilder))
rootCmd.AddCommand(newPortfolioCmd(runtimeBuilder))
rootCmd.AddCommand(newAddPositionCmd(runtimeBuilder))
rootCmd.AddCommand(newInfoCmd())
Expand Down
62 changes: 51 additions & 11 deletions internal/domain/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,24 @@ type DailyHolding struct {
ChangePercent float64
}

type Holding struct {
Symbol string
Quantity float64
Weight float64
MarketValueBase float64
QuotedPrice float64
PriceCurrency string
ChangeAbsolute float64
ChangePercent float64
SinceLastSnapshot *ValueChangeSummary
}

type HoldingSummary struct {
Holdings []Holding
TotalValue float64
Change *ValueChangeSummary
}

type DailyRisk struct {
LargestPosition string
LargestWeight float64
Expand All @@ -50,31 +68,53 @@ type DailyNews struct {
}

type DailyReport struct {
PortfolioName string
BaseCurrency string
TotalValue float64
Portfolio PortfolioSummary
TopHoldings HoldingSummary
Risk RiskSummary
News []NewsSummary
Attention []string
}

ChangeSinceLastSnapshot *ValueChangeReport
type BriefReport struct {
Greeting string
Portfolio PortfolioSummary
Movers HoldingSummary
Markets []MarketSummary
News []NewsSummary
}

TopHoldings []TopHoldingReport
Risk RiskReport
News []TickerNewsReport
Attention []string
type PortfolioSummary struct {
Name string
BaseCurrency string
TotalValue float64
Change *ValueChangeSummary
}

type TickerNewsReport struct {
type MarketSummary struct {
Symbol string
Price float64
Change float64
ChangePercent float64
Currency string
}

type NewsSummary struct {
Ticker string
Headlines []NewsHeadline
Summary string
}

type RiskReport struct {
type RiskSummary struct {
LargestPosition string
LargestWeight float64
Top3Concentration float64
Observations []string
}

type AttentionSummary struct {
Signal string
}

type TopHoldingReport struct {
Symbol string
Weight float64
Expand All @@ -86,7 +126,7 @@ type TopHoldingReport struct {
SinceLastSnapshot *HoldingSnapshotChangeReport
}

type ValueChangeReport struct {
type ValueChangeSummary struct {
Absolute float64
Percent float64
}
Expand Down
25 changes: 25 additions & 0 deletions internal/domain/usecase.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,32 @@ type GetDailyReportInput struct {
WithAI bool
}

func (i *GetDailyReportInput) ApplyDefaults() {
if i.PortfolioName == "" {
i.PortfolioName = "main"
}

if i.NewsLimit <= 0 {
i.NewsLimit = 2
}
}

type GetDailyReportOutput struct {
DailyReport DailyReport
AISummary string
}

type GetMorningBriefUsecaseInput struct {
PortfolioName string
NewsLimit int
}

func (i *GetMorningBriefUsecaseInput) ApplyDefaults() {
if i.PortfolioName == "" {
i.PortfolioName = "main"
}
}

type GetMorningBriefUsecaseOutput struct {
Report BriefReport
}
21 changes: 21 additions & 0 deletions internal/render/brief.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package render

import (
"io"

"github.com/squeakycheese75/tick/internal/domain"
)

func RenderBriefReport(w io.Writer, r domain.BriefReport, opts BriefReportOptions) error {
out := &writer{w: w}

renderPortfolioSummary(out, r.Portfolio, opts.Summary)
out.println("")

renderMoversSummary(out, r.Movers, r.Portfolio.BaseCurrency, opts.Holdings)
out.println("")

renderNewsSummary(out, r.News, opts.News)

return out.err
}
Loading
Loading