135 lines
3.6 KiB
Go
135 lines
3.6 KiB
Go
|
|
package alp
|
||
|
|
|
||
|
|
import (
|
||
|
|
"net/http"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/gin-gonic/gin"
|
||
|
|
db "github.com/gothinkster/golang-gin-realworld-example-app/common"
|
||
|
|
"github.com/gothinkster/golang-gin-realworld-example-app/models"
|
||
|
|
)
|
||
|
|
|
||
|
|
// GetALPHistory returns historical APR and price data from alp_snapshots.
|
||
|
|
// Each point is computed from consecutive snapshot pairs.
|
||
|
|
// Query param: days=30 (default 30, max 90)
|
||
|
|
func GetALPHistory(c *gin.Context) {
|
||
|
|
database := db.GetDB()
|
||
|
|
|
||
|
|
days := 30
|
||
|
|
if d := c.Query("days"); d != "" {
|
||
|
|
if v := parseInt(d, 30); v > 0 && v <= 90 {
|
||
|
|
days = v
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
since := time.Now().UTC().Add(-time.Duration(days) * 24 * time.Hour)
|
||
|
|
|
||
|
|
var snaps []models.ALPSnapshot
|
||
|
|
if err := database.
|
||
|
|
Where("snapshot_time >= ?", since).
|
||
|
|
Order("snapshot_time ASC").
|
||
|
|
Find(&snaps).Error; err != nil {
|
||
|
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
type Point struct {
|
||
|
|
Time string `json:"time"`
|
||
|
|
TS int64 `json:"ts"`
|
||
|
|
PoolAPR float64 `json:"poolAPR"`
|
||
|
|
ALPPrice float64 `json:"alpPrice"`
|
||
|
|
FeeSurplus float64 `json:"feeSurplus"`
|
||
|
|
PoolValue float64 `json:"poolValue"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// Deduplicate: keep the latest snapshot per calendar day (UTC)
|
||
|
|
dayMap := make(map[string]models.ALPSnapshot)
|
||
|
|
dayOrder := make([]string, 0)
|
||
|
|
for _, snap := range snaps {
|
||
|
|
key := snap.SnapshotTime.UTC().Format("2006-01-02")
|
||
|
|
if _, exists := dayMap[key]; !exists {
|
||
|
|
dayOrder = append(dayOrder, key)
|
||
|
|
}
|
||
|
|
dayMap[key] = snap // overwrite keeps the latest of the day
|
||
|
|
}
|
||
|
|
daily := make([]models.ALPSnapshot, 0, len(dayOrder))
|
||
|
|
for _, key := range dayOrder {
|
||
|
|
daily = append(daily, dayMap[key])
|
||
|
|
}
|
||
|
|
|
||
|
|
points := make([]Point, 0, len(daily))
|
||
|
|
for i, snap := range daily {
|
||
|
|
apr := 0.0
|
||
|
|
if i > 0 {
|
||
|
|
prev := daily[i-1]
|
||
|
|
d := snap.SnapshotTime.Sub(prev.SnapshotTime).Hours() / 24
|
||
|
|
if d > 0 && snap.PoolValue > 0 && snap.FeeSurplus > prev.FeeSurplus {
|
||
|
|
apr = (snap.FeeSurplus-prev.FeeSurplus) / snap.PoolValue / d * 365 * 100
|
||
|
|
}
|
||
|
|
}
|
||
|
|
points = append(points, Point{
|
||
|
|
Time: snap.SnapshotTime.Format("01/02"),
|
||
|
|
TS: snap.SnapshotTime.Unix(),
|
||
|
|
PoolAPR: apr,
|
||
|
|
ALPPrice: snap.ALPPrice,
|
||
|
|
FeeSurplus: snap.FeeSurplus,
|
||
|
|
PoolValue: snap.PoolValue,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": points})
|
||
|
|
}
|
||
|
|
|
||
|
|
func parseInt(s string, def int) int {
|
||
|
|
if s == "" {
|
||
|
|
return def
|
||
|
|
}
|
||
|
|
v := 0
|
||
|
|
for _, c := range s {
|
||
|
|
if c < '0' || c > '9' {
|
||
|
|
return def
|
||
|
|
}
|
||
|
|
v = v*10 + int(c-'0')
|
||
|
|
}
|
||
|
|
return v
|
||
|
|
}
|
||
|
|
|
||
|
|
// GetALPStats returns current ALP pool APR calculated from snapshots.
|
||
|
|
// TVL and ALP price are read directly from chain on the frontend;
|
||
|
|
// this endpoint only provides the APR which requires historical data.
|
||
|
|
func GetALPStats(c *gin.Context) {
|
||
|
|
database := db.GetDB()
|
||
|
|
|
||
|
|
// Latest snapshot
|
||
|
|
var latest models.ALPSnapshot
|
||
|
|
if err := database.Order("snapshot_time DESC").First(&latest).Error; err != nil {
|
||
|
|
c.JSON(http.StatusOK, gin.H{
|
||
|
|
"success": true,
|
||
|
|
"data": gin.H{"poolAPR": 0.0, "rewardAPR": 0.0},
|
||
|
|
})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
// Use the oldest available snapshot as reference (maximises data coverage)
|
||
|
|
var past models.ALPSnapshot
|
||
|
|
database.Order("snapshot_time ASC").First(&past)
|
||
|
|
found := past.ID != 0 && past.ID != latest.ID
|
||
|
|
|
||
|
|
poolAPR := 0.0
|
||
|
|
if found && latest.PoolValue > 0 && past.FeeSurplus < latest.FeeSurplus {
|
||
|
|
days := latest.SnapshotTime.Sub(past.SnapshotTime).Hours() / 24
|
||
|
|
if days > 0 {
|
||
|
|
surplusDelta := latest.FeeSurplus - past.FeeSurplus
|
||
|
|
poolAPR = surplusDelta / latest.PoolValue / days * 365 * 100 // annualized %
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
c.JSON(http.StatusOK, gin.H{
|
||
|
|
"success": true,
|
||
|
|
"data": gin.H{
|
||
|
|
"poolAPR": poolAPR,
|
||
|
|
"rewardAPR": 0.0, // placeholder until reward contract is connected
|
||
|
|
},
|
||
|
|
})
|
||
|
|
}
|