Files
assetx/webapp-back/fundmarket/routers.go

435 lines
12 KiB
Go
Raw Permalink Normal View History

package fundmarket
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/gothinkster/golang-gin-realworld-example-app/common"
"github.com/gothinkster/golang-gin-realworld-example-app/models"
)
// GetProducts returns all fund market products
func GetProducts(c *gin.Context) {
db := common.GetDB()
// Query assets with their performance data
// token_list=1: return all active assets (for dropdowns)
// default: exclude stablecoins (Fund Market product listing)
var assets []models.Asset
query := db.Where("is_active = ?", true)
if c.Query("token_list") != "1" {
query = query.Where("token_role != ?", "stablecoin")
}
err := query.Find(&assets).Error
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"error": "Failed to fetch products",
})
return
}
// Get performance data for each asset
products := make([]models.ProductResponse, 0)
for _, asset := range assets {
var perf models.AssetPerformance
db.Where("asset_id = ?", asset.ID).
Order("snapshot_date DESC").
First(&perf)
riskLabel := asset.RiskLabel
if riskLabel == "" {
riskLabel = "--"
}
circulatingSupplyStr := "--"
if perf.CirculatingSupply > 0 {
circulatingSupplyStr = formatUSD(perf.CirculatingSupply)
}
poolCapStr := "--"
if asset.PoolCapUSD > 0 {
poolCapStr = formatUSD(asset.PoolCapUSD)
}
product := models.ProductResponse{
ID: int(asset.ID),
Name: asset.Name,
TokenSymbol: asset.TokenSymbol,
Decimals: asset.Decimals,
ContractAddress: asset.ContractAddress,
ChainID: asset.ChainID,
TokenRole: asset.TokenRole,
Category: asset.Category,
CategoryColor: asset.CategoryColor,
IconURL: asset.IconURL,
YieldAPY: fmt.Sprintf("%.1f%%", asset.TargetAPY),
PoolCap: poolCapStr,
Risk: riskLabel,
RiskLevel: asset.RiskLevel,
CirculatingSupply: circulatingSupplyStr,
PoolCapacityPercent: perf.PoolCapacityPercent,
}
products = append(products, product)
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": products,
})
}
// GetStats returns fund market statistics
func GetStats(c *gin.Context) {
db := common.GetDB()
// Total Value Locked = sum of latest tvlusd across all active assets
var totalTVL float64
db.Raw(`
SELECT COALESCE(SUM(ap.tvlusd), 0)
FROM asset_performance ap
INNER JOIN (
SELECT asset_id, MAX(snapshot_date) AS latest
FROM asset_performance
GROUP BY asset_id
) latest ON ap.asset_id = latest.asset_id AND ap.snapshot_date = latest.latest
INNER JOIN assets a ON a.id = ap.asset_id AND a.is_active = true
`).Scan(&totalTVL)
// Cumulative Yield = sum of cumulative_yield_usd from latest snapshots
var totalYield float64
db.Raw(`
SELECT COALESCE(SUM(ap.cumulative_yield_usd), 0)
FROM asset_performance ap
INNER JOIN (
SELECT asset_id, MAX(snapshot_date) AS latest
FROM asset_performance
GROUP BY asset_id
) latest ON ap.asset_id = latest.asset_id AND ap.snapshot_date = latest.latest
`).Scan(&totalYield)
stats := []models.StatsResponse{
{
Label: "Total Value Locked",
Value: formatUSD(totalTVL),
Change: "",
IsPositive: true,
},
{
Label: "Cumulative Yield",
Value: formatUSD(totalYield),
Change: "",
IsPositive: true,
},
{
Label: "Your Total Balance",
Value: "--",
Change: "",
IsPositive: true,
},
{
Label: "Your Total Earning",
Value: "--",
Change: "",
IsPositive: true,
},
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": stats,
})
}
// GetProductByID returns detailed product information by ID
func GetProductByID(c *gin.Context) {
db := common.GetDB()
id := c.Param("id")
var asset models.Asset
err := db.Where("id = ? AND is_active = ?", id, true).First(&asset).Error
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"success": false,
"error": "Product not found",
})
return
}
// Get performance data
var perf models.AssetPerformance
db.Where("asset_id = ?", asset.ID).
Order("snapshot_date DESC").
First(&perf)
// Calculate volume change vs 7-day average (excluding today)
var avg7dVolume float64
db.Raw(`
SELECT AVG(volume_24h_usd) FROM (
SELECT volume_24h_usd FROM asset_performance
WHERE asset_id = ? AND snapshot_date < CURDATE()
ORDER BY snapshot_date DESC
LIMIT 7
) t
`, asset.ID).Scan(&avg7dVolume)
volumeChangeVsAvg := 0.0
if avg7dVolume > 0 {
volumeChangeVsAvg = (perf.Volume24hUSD - avg7dVolume) / avg7dVolume * 100
}
// Get custody info
var custody models.AssetCustody
hasCustody := db.Where("asset_id = ?", asset.ID).First(&custody).Error == nil
// Get audit reports
var auditReports []models.AssetAuditReport
db.Where("asset_id = ? AND is_active = ?", asset.ID, true).
Order("report_date DESC").
Limit(10).
Find(&auditReports)
// Get product links
var productLinks []models.ProductLink
db.Where("asset_id = ? AND is_active = ?", asset.ID, true).
Order("display_order ASC, id ASC").
Find(&productLinks)
// Build response
detail := models.ProductDetailResponse{
// Basic Info
ID: int(asset.ID),
AssetCode: asset.AssetCode,
Name: asset.Name,
Subtitle: asset.Subtitle,
Description: asset.Description,
TokenSymbol: asset.TokenSymbol,
Decimals: asset.Decimals,
// Investment Parameters
UnderlyingAssets: asset.UnderlyingAssets,
PoolCapUSD: asset.PoolCapUSD,
RiskLevel: asset.RiskLevel,
RiskLabel: asset.RiskLabel,
TargetAPY: asset.TargetAPY,
// Contract Info
ContractAddress: asset.ContractAddress,
ChainID: asset.ChainID,
// Display Info
Category: asset.Category,
CategoryColor: asset.CategoryColor,
IconURL: asset.IconURL,
// Performance Data
CurrentAPY: perf.CurrentAPY,
TVLUSD: perf.TVLUSD,
Volume24hUSD: perf.Volume24hUSD,
VolumeChangeVsAvg: volumeChangeVsAvg,
CirculatingSupply: perf.CirculatingSupply,
PoolCapacityPercent: perf.PoolCapacityPercent,
CurrentPrice: perf.YTPrice,
}
// Add custody info if exists
if hasCustody {
lastAuditDate := ""
if custody.LastAuditDate.Time != nil {
lastAuditDate = custody.LastAuditDate.Time.Format("02 Jan 2006")
}
var additionalInfo map[string]interface{}
if custody.AdditionalInfo != "" {
_ = json.Unmarshal([]byte(custody.AdditionalInfo), &additionalInfo)
}
if additionalInfo == nil {
additionalInfo = map[string]interface{}{}
}
// 从 maturity_date 动态计算 days_remaining不再依赖存储值
if maturityStr, ok := additionalInfo["maturity_date"].(string); ok && maturityStr != "" {
if maturityDate, err := time.Parse("2006-01-02", maturityStr); err == nil {
days := int(time.Until(maturityDate).Hours() / 24)
additionalInfo["days_remaining"] = days
}
} else {
delete(additionalInfo, "days_remaining")
}
detail.Custody = &models.CustodyInfo{
CustodianName: custody.CustodianName,
CustodyType: custody.CustodyType,
CustodyLocation: custody.CustodyLocation,
AuditorName: custody.AuditorName,
LastAuditDate: lastAuditDate,
AuditReportURL: custody.AuditReportURL,
AdditionalInfo: additionalInfo,
}
}
// Add audit reports
detail.AuditReports = make([]models.AuditReportInfo, 0, len(auditReports))
for _, report := range auditReports {
detail.AuditReports = append(detail.AuditReports, models.AuditReportInfo{
ReportType: report.ReportType,
ReportTitle: report.ReportTitle,
ReportDate: report.ReportDate.Format("02 Jan 2006"),
AuditorName: report.AuditorName,
Summary: report.Summary,
ReportURL: report.ReportURL,
})
}
// Add product links
detail.ProductLinks = make([]models.ProductLinkInfo, 0, len(productLinks))
for _, link := range productLinks {
detail.ProductLinks = append(detail.ProductLinks, models.ProductLinkInfo{
LinkText: link.LinkText,
LinkURL: link.LinkURL,
Description: link.Description,
DisplayArea: link.DisplayArea,
DisplayOrder: link.DisplayOrder,
})
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": detail,
})
}
// GetProductHistory returns daily APY/price history for a product (one point per day)
func GetProductHistory(c *gin.Context) {
db := common.GetDB()
id := c.Param("id")
// Pick the last snapshot of each calendar day (UTC), up to 30 days
var snapshots []models.APYSnapshot
if err := db.Raw(`
SELECT ap.* FROM apy_snapshots ap
INNER JOIN (
SELECT DATE(snapshot_time) AS snap_date, MAX(snapshot_time) AS latest
FROM apy_snapshots
WHERE asset_id = ?
GROUP BY DATE(snapshot_time)
ORDER BY snap_date DESC
LIMIT 30
) d ON DATE(ap.snapshot_time) = d.snap_date AND ap.snapshot_time = d.latest
WHERE ap.asset_id = ?
ORDER BY ap.snapshot_time ASC
`, id, id).Scan(&snapshots).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"error": "Failed to fetch history",
})
return
}
type HistoryPoint struct {
Time string `json:"time"`
APY float64 `json:"apy"`
Price float64 `json:"price"`
}
points := make([]HistoryPoint, len(snapshots))
for i, s := range snapshots {
points[i] = HistoryPoint{
Time: s.SnapshotTime.UTC().Format(time.RFC3339),
APY: s.APYValue,
Price: s.Price,
}
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": points,
})
}
// Helper function to format USD values
func formatUSD(value float64) string {
if value >= 1000000 {
return fmt.Sprintf("$%.1fM", value/1000000)
} else if value >= 1000 {
return fmt.Sprintf("$%.1fK", value/1000)
}
return fmt.Sprintf("$%.2f", value)
}
// GetDailyReturns returns daily net return data for a product by month.
// Query params: year (int), month (int). Defaults to current month.
func GetDailyReturns(c *gin.Context) {
db := common.GetDB()
id := c.Param("id")
now := time.Now().UTC()
year := now.Year()
month := int(now.Month())
if y, err := strconv.Atoi(c.Query("year")); err == nil {
year = y
}
if m, err := strconv.Atoi(c.Query("month")); err == nil && m >= 1 && m <= 12 {
month = m
}
startDate := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC)
endDate := startDate.AddDate(0, 1, 0)
// Include one day before month start for delta calculation
queryStart := startDate.AddDate(0, 0, -1)
var perfs []models.AssetPerformance
db.Where("asset_id = ? AND snapshot_date >= ? AND snapshot_date < ?", id, queryStart, endDate).
Order("snapshot_date ASC").
Find(&perfs)
// Map date string -> performance
type perfEntry struct {
ytPrice float64
hasData bool
}
perfMap := make(map[string]perfEntry)
for _, p := range perfs {
perfMap[p.SnapshotDate.Format("2006-01-02")] = perfEntry{ytPrice: p.YTPrice, hasData: true}
}
type DayReturn struct {
Date string `json:"date"`
YTPrice float64 `json:"ytPrice"`
DailyReturn float64 `json:"dailyReturn"`
HasData bool `json:"hasData"`
}
today := now.Format("2006-01-02")
var results []DayReturn
for d := startDate; d.Before(endDate); d = d.AddDate(0, 0, 1) {
dateStr := d.Format("2006-01-02")
// Skip future days
if dateStr > today {
break
}
cur := perfMap[dateStr]
prev := perfMap[d.AddDate(0, 0, -1).Format("2006-01-02")]
dailyReturn := 0.0
if cur.hasData && prev.hasData && prev.ytPrice > 0 {
dailyReturn = (cur.ytPrice - prev.ytPrice) / prev.ytPrice * 100
}
results = append(results, DayReturn{
Date: dateStr,
YTPrice: cur.ytPrice,
DailyReturn: dailyReturn,
HasData: cur.hasData,
})
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": results,
})
}