package analyze import ( "fmt" "sort" "strings" "time" "gitea.konchin.com/service/stonks/internal/config" "gitea.konchin.com/service/stonks/internal/fetch" ) // SystemPrompt instructs Claude how to analyze the fetched data and format // its response for posting to Discord. const SystemPrompt = `You are a financial analyst producing a concise daily briefing for a personal Discord channel. You will be given today's prices (with percent change) and recent news headlines for a fixed watchlist of cryptocurrencies, NASDAQ equities, futures (metals, crude oil), and forex pairs. Structure your response in Markdown, formatted for Discord (short lines, ` + "`**bold**`" + ` section headers, no HTML, no huge tables): 1. A one-paragraph market-wide summary. 2. A "Crypto" section: brief commentary per asset grounded in the given price/news data. 3. A "NASDAQ Equities" section: same, grouped sensibly (e.g. by sector) if useful. 4. A "Futures" section (gold, silver, crude oil). 5. A "Forex" section covering the given currency pairs. 6. A short "Watch" section flagging anything time-sensitive (earnings, macro events implied by the news) for the next day or two. Keep the whole message under roughly 1500 words. Ground every claim in the provided data — do not fabricate news or prices. If data for an asset is missing, say so briefly rather than inventing a view. End with a one-line disclaimer that this is not financial advice. If the user turn includes a section titled "Other AI Analysts' Independent Takes", those are additional perspectives from other AI models given the same data — not ground truth and not your own reasoning. Weigh them alongside the price/news data, note explicitly where you agree or disagree and why, but reach your own independent conclusion. Do not simply restate their conclusions. After the full briefing above, on its own line, output exactly: ===RECOMMENDATIONS_JSON=== followed immediately by a single JSON object (no markdown code fence, no commentary before or after it) with this exact shape: {"recommendations": [{"symbol": "AAPL", "action": "buy", "reason": "one short sentence"}]} "action" must be exactly "buy" or "sell". Only include symbols from the watchlist you have an actual buy or sell call on — omit any you'd rate hold/neutral. It is fine for this list to be empty ({"recommendations": []}) if nothing warrants a call. This JSON must be the very last thing in your response.` // BuildUserPrompt renders the fetched result into the user-turn text sent // alongside SystemPrompt. func BuildUserPrompt(targets config.Targets, result fetch.Result) string { var b strings.Builder fmt.Fprintf(&b, "Date: %s\n\n", time.Now().Format("2006-01-02")) b.WriteString("## Prices\n\n") writeQuoteSection(&b, "Crypto", targets.Crypto, result.Quotes) writeQuoteSection(&b, "NASDAQ Equities", targets.Equities, result.Quotes) futureSymbols := make([]string, 0, len(targets.Futures)) for _, symbol := range targets.Futures { futureSymbols = append(futureSymbols, symbol) } sort.Strings(futureSymbols) writeQuoteSection(&b, "Futures", futureSymbols, result.Quotes) writeQuoteSection(&b, "Forex", targets.Forex, result.Quotes) b.WriteString("\n## Recent News\n\n") if len(result.News) == 0 { b.WriteString("(no news items fetched)\n") } for _, n := range result.News { fmt.Fprintf(&b, "- [%s] %s", n.Source, n.Title) if !n.Published.IsZero() { fmt.Fprintf(&b, " (%s)", n.Published.Format("2006-01-02")) } b.WriteString("\n") if n.Summary != "" { fmt.Fprintf(&b, " %s\n", truncate(n.Summary, 300)) } } return b.String() } // AppendAdvisorSection appends successful advisor outputs (see // RunAdvisors) to prompt as an "Other AI Analysts' Independent Takes" // section, one subsection per advisor. Advisors that errored are skipped — // callers should log those separately. Returns prompt unchanged if there // are no successful results, so it's safe to call unconditionally. func AppendAdvisorSection(prompt string, results []AdvisorResult) string { var b strings.Builder for _, r := range results { if r.Err != nil || r.Output == "" { continue } fmt.Fprintf(&b, "### %s\n\n%s\n\n", r.Name, r.Output) } if b.Len() == 0 { return prompt } return prompt + "\n## Other AI Analysts' Independent Takes\n\n" + b.String() } func writeQuoteSection(b *strings.Builder, heading string, symbols []string, quotes []fetch.Quote) { fmt.Fprintf(b, "### %s\n\n", heading) bySymbol := map[string][]fetch.Quote{} for _, q := range quotes { bySymbol[q.Symbol] = append(bySymbol[q.Symbol], q) } for _, sym := range symbols { qs, ok := bySymbol[sym] if !ok || len(qs) == 0 { fmt.Fprintf(b, "- %s: no data available\n", sym) continue } for _, q := range qs { fmt.Fprintf(b, "- %s (%s) [%s]: %.2f %s (%+.2f%%)\n", sym, q.Label, q.Source, q.Price, q.Currency, q.ChangePercent) } } b.WriteString("\n") } func truncate(s string, n int) string { if len(s) <= n { return s } return s[:n] + "..." }