feat: add Citations and WatchlistChat components, integrate with API
- Implemented Citations component to display citation data. - Created WatchlistDrawer and ChatSidebar components for managing watchlists and AI chat functionality. - Integrated API calls for watchlist management and chat interactions. - Updated index.tsx to include new components in the main application layout. - Added API client in lib/api.ts for structured API interactions. - Developed Alerts, Dashboard, Portfolio, Routines, Screener, and Report pages with relevant data fetching and UI components. - Introduced styles in tokens.css for consistent theming across the application. - Configured TypeScript and Vite for project setup and development.
This commit is contained in:
1 parent
0db2cf28b3
commit
8c184ccae1
111 files changed
+12349
-62
No files matched your search
@@ -0,0 +1,206 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"flowsight/internal/model"
|
||||
)
|
||||
|
||||
// RiskProfile shifts signal weights: conservative is fundamental-heavy,
|
||||
// aggressive leans into technical + broker flows.
|
||||
type RiskProfile string
|
||||
|
||||
const (
|
||||
Conservative RiskProfile = "conservative"
|
||||
Moderate RiskProfile = "moderate"
|
||||
Aggressive RiskProfile = "aggressive"
|
||||
)
|
||||
|
||||
// Synthesis is the Master Synthesizer (A7) output.
|
||||
type Synthesis struct {
|
||||
Recommendation string `json:"recommendation"` // BUY | HOLD | AVOID
|
||||
Conviction int `json:"conviction"` // 1..5
|
||||
Thesis string `json:"thesis"`
|
||||
PositionPct float64 `json:"position_pct"`
|
||||
Conflict bool `json:"conflict"`
|
||||
ConflictNote string `json:"conflict_note,omitempty"`
|
||||
Scores []string `json:"scores"`
|
||||
Citations []model.Citation `json:"citations"`
|
||||
}
|
||||
|
||||
// Synthesize (A7) weights A1..A6 by risk profile x accuracy-ledger weights,
|
||||
// adds agreement bonus / conflict flag, and sizes via capped Kelly (max 10%).
|
||||
func Synthesize(ctx context.Context, d Deps, ticker string, profile RiskProfile, results []model.AgentResult) Synthesis {
|
||||
_ = ctx
|
||||
ticker = strings.ToUpper(ticker)
|
||||
byAgent := map[string]model.AgentResult{}
|
||||
for _, r := range results {
|
||||
byAgent[r.Agent] = r
|
||||
}
|
||||
|
||||
base := map[string]float64{
|
||||
"smart-money": 0.22, "broker-intel": 0.13, "sentiment": 0.12,
|
||||
"fundamental": 0.25, "technical": 0.15, "catalyst": 0.13,
|
||||
}
|
||||
switch profile {
|
||||
case Conservative:
|
||||
base = map[string]float64{
|
||||
"smart-money": 0.15, "broker-intel": 0.10, "sentiment": 0.10,
|
||||
"fundamental": 0.40, "technical": 0.10, "catalyst": 0.15,
|
||||
}
|
||||
case Aggressive:
|
||||
base = map[string]float64{
|
||||
"smart-money": 0.27, "broker-intel": 0.15, "sentiment": 0.10,
|
||||
"fundamental": 0.13, "technical": 0.25, "catalyst": 0.10,
|
||||
}
|
||||
}
|
||||
|
||||
// Ledger weights: 0.5 until an agent has >=10 resolved calls.
|
||||
weights := map[string]float64{}
|
||||
norm := func(score float64) float64 { return (score + 100) / 200 } // -100..100 -> 0..1
|
||||
get := func(name string, raw, lo, hi float64) float64 {
|
||||
v := raw
|
||||
if hi == 1 && lo == -1 { // sentiment -1..1
|
||||
v = raw * 100
|
||||
} else if hi == 100 && lo == 0 { // fundamental/catalyst 0..100
|
||||
v = raw*2 - 100
|
||||
}
|
||||
return clampScore(v, -100, 100)
|
||||
}
|
||||
_ = norm
|
||||
|
||||
total, wsum := 0.0, 0.0
|
||||
var lines []string
|
||||
var cites []model.Citation
|
||||
for _, a := range []string{"smart-money", "broker-intel", "sentiment", "fundamental", "technical", "catalyst"} {
|
||||
r := byAgent[a]
|
||||
lw := d.DB.AccuracyWeight(a)
|
||||
w := base[a] * (0.5 + lw) // ledger blends in without zeroing anyone
|
||||
weights[a] = w
|
||||
var v float64
|
||||
switch a {
|
||||
case "sentiment":
|
||||
v = get(a, r.Score, -1, 1)
|
||||
case "fundamental", "catalyst":
|
||||
v = get(a, r.Score, 0, 100)
|
||||
default:
|
||||
v = get(a, r.Score, -100, 100)
|
||||
}
|
||||
total += w * v
|
||||
wsum += w
|
||||
lines = append(lines, fmt.Sprintf("%s %+.0f", a, v))
|
||||
cites = append(cites, r.Citations...)
|
||||
}
|
||||
score := 0.0
|
||||
if wsum > 0 {
|
||||
score = total / wsum
|
||||
}
|
||||
|
||||
// Agreement bonus (>=3 aligned) / conflict flag (fundamental vs flows).
|
||||
align := 0
|
||||
for _, a := range []string{"smart-money", "fundamental", "technical", "sentiment"} {
|
||||
r := byAgent[a]
|
||||
v := r.Score
|
||||
if a == "sentiment" {
|
||||
v *= 100
|
||||
} else if a == "fundamental" {
|
||||
v = v*2 - 100
|
||||
}
|
||||
if (score > 0 && v > 0) || (score < 0 && v < 0) {
|
||||
align++
|
||||
}
|
||||
}
|
||||
if align >= 3 {
|
||||
if score > 0 {
|
||||
score += 5
|
||||
} else {
|
||||
score -= 5
|
||||
}
|
||||
}
|
||||
fund := byAgent["fundamental"].Score*2 - 100
|
||||
flow := byAgent["smart-money"].Score
|
||||
conflict := (fund > 20 && flow < -20) || (fund < -20 && flow > 20)
|
||||
conflictNote := ""
|
||||
if conflict {
|
||||
conflictNote = fmt.Sprintf("fundamental %+.0f opposes smart-money %+.0f", fund, flow)
|
||||
}
|
||||
|
||||
rec := "HOLD"
|
||||
switch {
|
||||
case score >= 25 && !conflict:
|
||||
rec = "BUY"
|
||||
case score <= -25:
|
||||
rec = "AVOID"
|
||||
case conflict && score >= 25:
|
||||
rec = "HOLD" // good fundamental + broker selling => HOLD-or-lower, cited
|
||||
}
|
||||
conviction := 3
|
||||
switch {
|
||||
case score >= 50 || score <= -50:
|
||||
conviction = 5
|
||||
case score >= 35 || score <= -35:
|
||||
conviction = 4
|
||||
case score >= -15 && score <= 15:
|
||||
conviction = 2
|
||||
}
|
||||
if conflict && conviction > 3 {
|
||||
conviction = 3
|
||||
}
|
||||
|
||||
// Capped Kelly: edge from score magnitude, max 10% single name.
|
||||
edge := (score / 100) * 0.5
|
||||
size := edge * 0.25 * 100
|
||||
if size < 0 {
|
||||
size = 0
|
||||
}
|
||||
if size > 10 {
|
||||
size = 10
|
||||
}
|
||||
if rec != "BUY" {
|
||||
size = 0
|
||||
}
|
||||
|
||||
thesis := fmt.Sprintf("%s %s (conviction %d/5): weighted score %+.0f. %s.",
|
||||
ticker, rec, conviction, score, strings.Join(citedLines(byAgent), ", "))
|
||||
if conflict {
|
||||
thesis += " Conflict: " + conflictNote + citeStr(byAgent["smart-money"]) + citeStr(byAgent["fundamental"]) + "."
|
||||
}
|
||||
|
||||
// Record predictions for the +30d accuracy ledger.
|
||||
_ = d.DB.RecordPrediction("synthesizer", ticker, rec)
|
||||
for _, a := range []string{"smart-money", "fundamental", "technical"} {
|
||||
_ = d.DB.RecordPrediction(a, ticker, rec)
|
||||
}
|
||||
|
||||
return Synthesis{
|
||||
Recommendation: rec, Conviction: conviction, Thesis: thesis,
|
||||
PositionPct: size, Conflict: conflict, ConflictNote: conflictNote,
|
||||
Scores: lines, Citations: cites,
|
||||
}
|
||||
}
|
||||
|
||||
// citedLines appends each agent's first citation marker to its score line so
|
||||
// every thesis claim is individually traceable.
|
||||
func citedLines(byAgent map[string]model.AgentResult) []string {
|
||||
var out []string
|
||||
for _, a := range []string{"smart-money", "broker-intel", "sentiment", "fundamental", "technical", "catalyst"} {
|
||||
r := byAgent[a]
|
||||
line := strings.TrimSpace(strings.Split(r.Summary, ";")[0])
|
||||
if line == "" {
|
||||
line = a
|
||||
}
|
||||
out = append(out, line+citeStr(r))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// citeStr renders "[endpoint @ date]" for an agent's first citation.
|
||||
func citeStr(r model.AgentResult) string {
|
||||
if len(r.Citations) == 0 {
|
||||
return " [no snapshot]"
|
||||
}
|
||||
c := r.Citations[0]
|
||||
return " [" + c.Endpoint + " @ " + c.SnapshotAt + "]"
|
||||
}
|
||||
Reference in new issue
Block a user