121 lines
3.3 KiB
Go
121 lines
3.3 KiB
Go
package server
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"net/http"
|
|
|
|
"git.rouggy.com/rouggy/stockradar/internal/scanner"
|
|
)
|
|
|
|
func (s *Server) handleGetSignals(w http.ResponseWriter, r *http.Request) {
|
|
onlyEtoro := r.URL.Query().Get("etoro") == "1"
|
|
|
|
query := `
|
|
SELECT sig.ticker, COALESCE(inst.name, sig.ticker),
|
|
sig.price, sig.change_pct, sig.rsi14,
|
|
sig.macd, sig.macd_signal, sig.macd_hist,
|
|
sig.sma20, sig.sma50, sig.volume, sig.avg_volume20,
|
|
COALESCE(sig.market_cap, 0), COALESCE(sig.short_ratio, 0),
|
|
COALESCE(sig.week52_high, 0), COALESCE(sig.week52_low, 0),
|
|
COALESCE(sig.pct_from_high, 0), COALESCE(sig.insider_value_30d, 0),
|
|
COALESCE(sig.insider_sell_value_30d, 0), COALESCE(sig.earnings_date, ''),
|
|
COALESCE(sig.ceo_change, 0),
|
|
COALESCE(sig.score, 0), COALESCE(sig.on_etoro, 0),
|
|
COALESCE(sig.alert,''), sig.computed_at
|
|
FROM signals sig
|
|
LEFT JOIN instruments inst ON inst.ticker = sig.ticker`
|
|
|
|
query += ` WHERE sig.source = 'watchlist'`
|
|
if onlyEtoro {
|
|
query += ` AND sig.on_etoro = 1`
|
|
}
|
|
query += ` ORDER BY sig.score DESC, CASE WHEN sig.alert != '' THEN 0 ELSE 1 END`
|
|
|
|
rows, err := s.db.Query(query)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
signals := []scanner.Signal{}
|
|
for rows.Next() {
|
|
var sig scanner.Signal
|
|
var onEtoro, ceoChange int
|
|
if err := rows.Scan(
|
|
&sig.Ticker, &sig.Name,
|
|
&sig.Price, &sig.ChangePct,
|
|
&sig.RSI14, &sig.MACD, &sig.MACDSignal, &sig.MACDHist,
|
|
&sig.SMA20, &sig.SMA50, &sig.Volume, &sig.AvgVolume20,
|
|
&sig.MarketCap, &sig.ShortRatio,
|
|
&sig.Week52High, &sig.Week52Low,
|
|
&sig.PctFromHigh, &sig.InsiderValue30d,
|
|
&sig.InsiderSell30d, &sig.EarningsDate,
|
|
&ceoChange,
|
|
&sig.Score, &onEtoro,
|
|
&sig.Alert, &sig.ComputedAt,
|
|
); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
sig.OnEtoro = onEtoro == 1
|
|
sig.CEOChange = ceoChange == 1
|
|
signals = append(signals, sig)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(signals)
|
|
}
|
|
|
|
func (s *Server) handleTriggerScan(w http.ResponseWriter, r *http.Request) {
|
|
go func() { s.scanner.Scan() }()
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write([]byte(`{"status":"scanning"}`))
|
|
}
|
|
|
|
func (s *Server) handleGetPrices(w http.ResponseWriter, r *http.Request) {
|
|
ticker := r.URL.Query().Get("ticker")
|
|
if ticker == "" {
|
|
http.Error(w, "ticker required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
rows, err := s.db.Query(`
|
|
SELECT date, open, high, low, close, volume
|
|
FROM prices WHERE ticker = ?
|
|
ORDER BY date ASC LIMIT 90
|
|
`, ticker)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
type bar struct {
|
|
Date string `json:"date"`
|
|
Open float64 `json:"open"`
|
|
High float64 `json:"high"`
|
|
Low float64 `json:"low"`
|
|
Close float64 `json:"close"`
|
|
Volume int64 `json:"volume"`
|
|
}
|
|
|
|
bars := []bar{}
|
|
for rows.Next() {
|
|
var b bar
|
|
var vol sql.NullInt64
|
|
if err := rows.Scan(&b.Date, &b.Open, &b.High, &b.Low, &b.Close, &vol); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if vol.Valid {
|
|
b.Volume = vol.Int64
|
|
}
|
|
bars = append(bars, b)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(bars)
|
|
}
|