From ae07e12507c4ec432c063eb1382f3c10ca7654f0 Mon Sep 17 00:00:00 2001 From: Ross Date: Wed, 7 Jan 2026 19:42:27 -0500 Subject: [PATCH 1/3] updated curve and convex forward APY calcs and fix APR to APY. Added apr logging helper --- processes/apr/apy_trace.go | 178 ++++++++++++++++++++++++++++++++ processes/apr/forward.convex.go | 14 +-- processes/apr/forward.curve.go | 14 +-- processes/apr/helpers.go | 14 +-- 4 files changed, 197 insertions(+), 23 deletions(-) create mode 100644 processes/apr/apy_trace.go diff --git a/processes/apr/apy_trace.go b/processes/apr/apy_trace.go new file mode 100644 index 000000000..9dcd1c897 --- /dev/null +++ b/processes/apr/apy_trace.go @@ -0,0 +1,178 @@ +package apr + +import ( + "fmt" + "os" + "strconv" + "strings" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" +) + +// APY trace logging helper (opt-in via env vars): +// +// - Enable tracing: +// APY_TRACE=1 +// +// - Write to a dedicated file (optional): +// APY_TRACE_PATH=/tmp/apy_trace.log +// +// - If this is a relative path, it is relative to the process working directory. +// +// - If not set, output goes to stderr. +// +// - Filter by chain, vault, and/or strategy (comma-separated): +// APY_TRACE_CHAIN=1,10 +// APY_TRACE_VAULT=0xabc...,0xdef... +// APY_TRACE_STRATEGY=0x123...,0x456... +// +// Notes: +// - Filters are case-insensitive and address strings should be hex with 0x prefix. +// - If a filter is set, only matching entries are emitted. +type apyTraceConfig struct { + enabled bool + path string + chains map[string]struct{} + vaults map[string]struct{} + strategies map[string]struct{} +} + +var ( + apyTraceConfigOnce sync.Once + apyTraceCfg apyTraceConfig + + apyTraceFileOnce sync.Once + apyTraceFile *os.File + apyTraceFileErr error + apyTraceFileWarn sync.Once + + apyTraceWriteMu sync.Mutex +) + +func apyTrace(scope string, chainID uint64, vaultAddr common.Address, strategyAddr common.Address, step string, value interface{}) { + cfg := apyTraceLoadConfig() + if !apyTraceMatches(cfg, chainID, vaultAddr, strategyAddr) { + return + } + + line := fmt.Sprintf( + "%s APY_TRACE scope=%s chain=%d vault=%s strategy=%s step=%s value=%v", + time.Now().UTC().Format(time.RFC3339Nano), + scope, + chainID, + vaultAddr.Hex(), + strategyAddr.Hex(), + step, + value, + ) + + if cfg.path != "" { + if file, err := apyTraceOpenFile(cfg.path); err == nil && file != nil { + apyTraceWriteMu.Lock() + fmt.Fprintln(file, line) + apyTraceWriteMu.Unlock() + return + } + } + + fmt.Fprintln(os.Stderr, line) +} + +func apyTraceLoadConfig() apyTraceConfig { + apyTraceConfigOnce.Do(func() { + enabled := false + if value, ok := os.LookupEnv("APY_TRACE"); ok { + enabled = apyTraceParseBool(value) + } else if _, ok := os.LookupEnv("APY_TRACE_PATH"); ok { + enabled = true + } + + apyTraceCfg = apyTraceConfig{ + enabled: enabled, + path: strings.TrimSpace(os.Getenv("APY_TRACE_PATH")), + chains: apyTraceParseList("APY_TRACE_CHAIN"), + vaults: apyTraceParseList("APY_TRACE_VAULT"), + strategies: apyTraceParseList("APY_TRACE_STRATEGY"), + } + }) + + return apyTraceCfg +} + +func apyTraceParseBool(value string) bool { + if strings.TrimSpace(value) == "" { + return true + } + switch strings.ToLower(strings.TrimSpace(value)) { + case "1", "true", "t", "yes", "y", "on": + return true + default: + return false + } +} + +func apyTraceParseList(envKey string) map[string]struct{} { + raw := strings.TrimSpace(os.Getenv(envKey)) + if raw == "" { + return nil + } + + items := strings.Split(raw, ",") + values := make(map[string]struct{}, len(items)) + for _, item := range items { + value := strings.ToLower(strings.TrimSpace(item)) + if value == "" { + continue + } + values[value] = struct{}{} + } + + return values +} + +func apyTraceMatches(cfg apyTraceConfig, chainID uint64, vaultAddr common.Address, strategyAddr common.Address) bool { + if !cfg.enabled { + return false + } + + if len(cfg.chains) > 0 { + chainKey := strings.ToLower(strconv.FormatUint(chainID, 10)) + if _, ok := cfg.chains[chainKey]; !ok { + return false + } + } + + if len(cfg.vaults) > 0 { + vaultKey := strings.ToLower(vaultAddr.Hex()) + if _, ok := cfg.vaults[vaultKey]; !ok { + return false + } + } + + if len(cfg.strategies) > 0 { + strategyKey := strings.ToLower(strategyAddr.Hex()) + if _, ok := cfg.strategies[strategyKey]; !ok { + return false + } + } + + return true +} + +func apyTraceOpenFile(path string) (*os.File, error) { + apyTraceFileOnce.Do(func() { + if path == "" { + return + } + apyTraceFile, apyTraceFileErr = os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if apyTraceFileErr != nil { + apyTraceFileWarn.Do(func() { + fmt.Fprintf(os.Stderr, "%s APY_TRACE error opening %s: %v\n", time.Now().UTC().Format(time.RFC3339Nano), path, apyTraceFileErr) + }) + } + }) + + return apyTraceFile, apyTraceFileErr +} diff --git a/processes/apr/forward.convex.go b/processes/apr/forward.convex.go index 8bf24296d..92ac5f5c8 100644 --- a/processes/apr/forward.convex.go +++ b/processes/apr/forward.convex.go @@ -59,11 +59,10 @@ func calculateConvexForwardAPY(args TCalculateConvexAPYDataStruct) TStrategyAPY ** 3. Adding the pool APY ** 4. Adding the CVX APR **********************************************************************************************/ - keepCRVRatio := bigNumber.NewFloat(0).Sub(storage.ONE, keepCrv) // 1 - keepCRV - grossAPY := bigNumber.NewFloat(0).Mul(crvAPY, keepCRVRatio) // 1 - baseAPY * keepCRV - grossAPY = bigNumber.NewFloat(0).Add(grossAPY, rewardsAPY) // 2 - (baseAPY * keepCRV) + rewardAPR - grossAPY = bigNumber.NewFloat(0).Add(grossAPY, args.poolWeeklyAPY) // 3 - (baseAPY * keepCRV) + rewardAPR + poolAPY - grossAPY = bigNumber.NewFloat(0).Add(grossAPY, cvxAPY) // 4 - (baseAPY * keepCRV) + rewardAPR + poolAPY + cvxAPR + keepCRVRatio := bigNumber.NewFloat(0).Sub(storage.ONE, keepCrv) // 1 - keepCRV + grossAPY := bigNumber.NewFloat(0).Mul(crvAPY, keepCRVRatio) // 1 - baseAPY * keepCRV + grossAPY = bigNumber.NewFloat(0).Add(grossAPY, rewardsAPY) // 2 - (baseAPY * keepCRV) + rewardAPR + grossAPY = bigNumber.NewFloat(0).Add(grossAPY, cvxAPY) // 4 - (baseAPY * keepCRV) + rewardAPR + poolAPY + cvxAPR /********************************************************************************************** ** Calculate the CRV Net APR: @@ -72,8 +71,11 @@ func calculateConvexForwardAPY(args TCalculateConvexAPYDataStruct) TStrategyAPY netAPY := bigNumber.NewFloat(0).Mul(grossAPY, oneMinusPerfFee) // grossAPR * (1 - perfFee) if netAPY.Gt(vaultManagementFee) { netAPY = bigNumber.NewFloat(0).Sub(netAPY, vaultManagementFee) // (grossAPR * (1 - perfFee)) - managementFee + netAPRFloat64, _ := netAPY.Float64() + netAPY = bigNumber.NewFloat(0).SetFloat64(convertFloatAPRToAPY(netAPRFloat64, 52)) + netAPY = bigNumber.NewFloat(0).Add(netAPY, args.poolWeeklyAPY) } else { - netAPY = bigNumber.NewFloat(0) + netAPY = bigNumber.NewFloat(0).Add(bigNumber.NewFloat(0), args.poolWeeklyAPY) } apyStruct := TStrategyAPY{ diff --git a/processes/apr/forward.curve.go b/processes/apr/forward.curve.go index 3bbbaed0e..663e533a9 100644 --- a/processes/apr/forward.curve.go +++ b/processes/apr/forward.curve.go @@ -37,7 +37,7 @@ func calculateCurveForwardAPY(args TCalculateCurveAPYDataStruct) TStrategyAPY { ** The CRV APR is simply the baseAPR (aka how much CRV we get from the gauge) scaled by the ** yBoost. We then add the extraRewards which are incentives/bribes on top of the base rewards. **********************************************************************************************/ - crvAPY := bigNumber.NewFloat(0).Mul(args.baseAPY, yBoost) // baseAPR * yBoost + crvAPY := bigNumber.NewFloat(0).Mul(args.baseAPY, yBoost) // baseAPR * yBoost crvAPY = bigNumber.NewFloat(0).Add(crvAPY, args.rewardAPY) // (baseAPR * yBoost) + rewardAPY /********************************************************************************************** @@ -48,10 +48,9 @@ func calculateCurveForwardAPY(args TCalculateCurveAPYDataStruct) TStrategyAPY { ** 3. Adding the pool APY **********************************************************************************************/ keepCRVRatio := bigNumber.NewFloat(0).Sub(storage.ONE, keepCrv) // 1 - keepCRV - grossAPY := bigNumber.NewFloat(0).Mul(args.baseAPY, yBoost) // 1 - baseAPR * yBoost - grossAPY = bigNumber.NewFloat(0).Mul(grossAPY, keepCRVRatio) // 1 - baseAPR * yBoost * keepCRV - grossAPY = bigNumber.NewFloat(0).Add(grossAPY, args.rewardAPY) // 2 - (baseAPR * yBoost * keepCRV) + rewardAPY - grossAPY = bigNumber.NewFloat(0).Add(grossAPY, args.poolAPY) // 3 - (baseAPR * yBoost * keepCRV) + rewardAPY + poolAPY + grossAPY := bigNumber.NewFloat(0).Mul(args.baseAPY, yBoost) // 1 - baseAPR * yBoost + grossAPY = bigNumber.NewFloat(0).Mul(grossAPY, keepCRVRatio) // 1 - baseAPR * yBoost * keepCRV + grossAPY = bigNumber.NewFloat(0).Add(grossAPY, args.rewardAPY) // 2 - (baseAPR * yBoost * keepCRV) + rewardAPY /********************************************************************************************** ** Calculate the CRV Net APR: @@ -60,8 +59,11 @@ func calculateCurveForwardAPY(args TCalculateCurveAPYDataStruct) TStrategyAPY { netAPY := bigNumber.NewFloat(0).Mul(grossAPY, oneMinusPerfFee) // grossAPY * (1 - perfFee) if netAPY.Gt(vaultManagementFee) { netAPY = bigNumber.NewFloat(0).Sub(netAPY, vaultManagementFee) // (grossAPY * (1 - perfFee)) - managementFee + netAPRFloat64, _ := netAPY.Float64() + netAPY = bigNumber.NewFloat(0).SetFloat64(convertFloatAPRToAPY(netAPRFloat64, 52)) + netAPY = bigNumber.NewFloat(0).Add(netAPY, args.poolAPY) } else { - netAPY = bigNumber.NewFloat(0) + netAPY = bigNumber.NewFloat(0).Add(bigNumber.NewFloat(0), args.poolAPY) } apyStruct := TStrategyAPY{ diff --git a/processes/apr/helpers.go b/processes/apr/helpers.go index 80a497d9e..8cd819cb7 100644 --- a/processes/apr/helpers.go +++ b/processes/apr/helpers.go @@ -8,18 +8,10 @@ import ( "github.com/yearn/ydaemon/common/logs" ) - func convertFloatAPRToAPY(apr float64, periodsPerYear float64) float64 { - - // Convert APR to decimal form - aprDecimal := apr / 100.0 - - // APY = (1 + r/n)^n - 1 - // where r is the APR in decimal form and n is the number of compounding periods - apy := math.Pow(1+(aprDecimal/periodsPerYear), periodsPerYear) - 1 - - // Convert back to percentage - return apy * 100 + // APR is expected as a decimal (e.g. 0.56 for 56%). + // APY = (1 + r/n)^n - 1, where r is the APR in decimal form. + return math.Pow(1+(apr/periodsPerYear), periodsPerYear) - 1 } /************************************************************************************************** From 063f87889cbb7421858ea3617aa57e5012eb176c Mon Sep 17 00:00:00 2001 From: Ross Date: Wed, 7 Jan 2026 20:09:35 -0500 Subject: [PATCH 2/3] remove trace helper --- processes/apr/apy_trace.go | 178 ------------------------------------- 1 file changed, 178 deletions(-) delete mode 100644 processes/apr/apy_trace.go diff --git a/processes/apr/apy_trace.go b/processes/apr/apy_trace.go deleted file mode 100644 index 9dcd1c897..000000000 --- a/processes/apr/apy_trace.go +++ /dev/null @@ -1,178 +0,0 @@ -package apr - -import ( - "fmt" - "os" - "strconv" - "strings" - "sync" - "time" - - "github.com/ethereum/go-ethereum/common" -) - -// APY trace logging helper (opt-in via env vars): -// -// - Enable tracing: -// APY_TRACE=1 -// -// - Write to a dedicated file (optional): -// APY_TRACE_PATH=/tmp/apy_trace.log -// -// - If this is a relative path, it is relative to the process working directory. -// -// - If not set, output goes to stderr. -// -// - Filter by chain, vault, and/or strategy (comma-separated): -// APY_TRACE_CHAIN=1,10 -// APY_TRACE_VAULT=0xabc...,0xdef... -// APY_TRACE_STRATEGY=0x123...,0x456... -// -// Notes: -// - Filters are case-insensitive and address strings should be hex with 0x prefix. -// - If a filter is set, only matching entries are emitted. -type apyTraceConfig struct { - enabled bool - path string - chains map[string]struct{} - vaults map[string]struct{} - strategies map[string]struct{} -} - -var ( - apyTraceConfigOnce sync.Once - apyTraceCfg apyTraceConfig - - apyTraceFileOnce sync.Once - apyTraceFile *os.File - apyTraceFileErr error - apyTraceFileWarn sync.Once - - apyTraceWriteMu sync.Mutex -) - -func apyTrace(scope string, chainID uint64, vaultAddr common.Address, strategyAddr common.Address, step string, value interface{}) { - cfg := apyTraceLoadConfig() - if !apyTraceMatches(cfg, chainID, vaultAddr, strategyAddr) { - return - } - - line := fmt.Sprintf( - "%s APY_TRACE scope=%s chain=%d vault=%s strategy=%s step=%s value=%v", - time.Now().UTC().Format(time.RFC3339Nano), - scope, - chainID, - vaultAddr.Hex(), - strategyAddr.Hex(), - step, - value, - ) - - if cfg.path != "" { - if file, err := apyTraceOpenFile(cfg.path); err == nil && file != nil { - apyTraceWriteMu.Lock() - fmt.Fprintln(file, line) - apyTraceWriteMu.Unlock() - return - } - } - - fmt.Fprintln(os.Stderr, line) -} - -func apyTraceLoadConfig() apyTraceConfig { - apyTraceConfigOnce.Do(func() { - enabled := false - if value, ok := os.LookupEnv("APY_TRACE"); ok { - enabled = apyTraceParseBool(value) - } else if _, ok := os.LookupEnv("APY_TRACE_PATH"); ok { - enabled = true - } - - apyTraceCfg = apyTraceConfig{ - enabled: enabled, - path: strings.TrimSpace(os.Getenv("APY_TRACE_PATH")), - chains: apyTraceParseList("APY_TRACE_CHAIN"), - vaults: apyTraceParseList("APY_TRACE_VAULT"), - strategies: apyTraceParseList("APY_TRACE_STRATEGY"), - } - }) - - return apyTraceCfg -} - -func apyTraceParseBool(value string) bool { - if strings.TrimSpace(value) == "" { - return true - } - switch strings.ToLower(strings.TrimSpace(value)) { - case "1", "true", "t", "yes", "y", "on": - return true - default: - return false - } -} - -func apyTraceParseList(envKey string) map[string]struct{} { - raw := strings.TrimSpace(os.Getenv(envKey)) - if raw == "" { - return nil - } - - items := strings.Split(raw, ",") - values := make(map[string]struct{}, len(items)) - for _, item := range items { - value := strings.ToLower(strings.TrimSpace(item)) - if value == "" { - continue - } - values[value] = struct{}{} - } - - return values -} - -func apyTraceMatches(cfg apyTraceConfig, chainID uint64, vaultAddr common.Address, strategyAddr common.Address) bool { - if !cfg.enabled { - return false - } - - if len(cfg.chains) > 0 { - chainKey := strings.ToLower(strconv.FormatUint(chainID, 10)) - if _, ok := cfg.chains[chainKey]; !ok { - return false - } - } - - if len(cfg.vaults) > 0 { - vaultKey := strings.ToLower(vaultAddr.Hex()) - if _, ok := cfg.vaults[vaultKey]; !ok { - return false - } - } - - if len(cfg.strategies) > 0 { - strategyKey := strings.ToLower(strategyAddr.Hex()) - if _, ok := cfg.strategies[strategyKey]; !ok { - return false - } - } - - return true -} - -func apyTraceOpenFile(path string) (*os.File, error) { - apyTraceFileOnce.Do(func() { - if path == "" { - return - } - apyTraceFile, apyTraceFileErr = os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if apyTraceFileErr != nil { - apyTraceFileWarn.Do(func() { - fmt.Fprintf(os.Stderr, "%s APY_TRACE error opening %s: %v\n", time.Now().UTC().Format(time.RFC3339Nano), path, apyTraceFileErr) - }) - } - }) - - return apyTraceFile, apyTraceFileErr -} From 477190be743949d67a4e1b973d0af2edd789abd2 Mon Sep 17 00:00:00 2001 From: dudesahn <23222916+dudesahn@users.noreply.github.com> Date: Thu, 8 Jan 2026 08:19:56 -0500 Subject: [PATCH 3/3] fix: remove extra compounding (#557) * fix: remove extra convex compounding * fix: remove extra compounding from curve helper --- processes/apr/forward.convex.helpers.go | 9 +++------ processes/apr/forward.curve.helpers.go | 3 +-- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/processes/apr/forward.convex.helpers.go b/processes/apr/forward.convex.helpers.go index f9c5550e0..24eaed94a 100644 --- a/processes/apr/forward.convex.helpers.go +++ b/processes/apr/forward.convex.helpers.go @@ -91,8 +91,7 @@ func getConvexRewardAPY( totalRewardsAPR = bigNumber.NewFloat(0).Add(totalRewardsAPR, rewardAPR) } } - totalRewardsAPRFloat64, _ := totalRewardsAPR.Float64() - totalRewardsAPY := bigNumber.NewFloat(0).SetFloat64(convertFloatAPRToAPY(totalRewardsAPRFloat64, 365/15)) + totalRewardsAPY := bigNumber.NewFloat(0).Add(bigNumber.NewFloat(0), totalRewardsAPR) return totalRewardsAPR, totalRewardsAPY } @@ -220,10 +219,8 @@ func getCVXPoolAPY( crvAPR = bigNumber.NewFloat(0).Mul(crvPerUnderlyingPerYear, crvPrice) cvxAPR = bigNumber.NewFloat(0).Mul(cvxPerYear, cvxPrice) - crvAPRFloat64, _ := crvAPR.Float64() - cvxAPRFloat64, _ := cvxAPR.Float64() - crvAPY = bigNumber.NewFloat(0).SetFloat64(convertFloatAPRToAPY(crvAPRFloat64, 365/15)) - cvxAPY = bigNumber.NewFloat(0).SetFloat64(convertFloatAPRToAPY(cvxAPRFloat64, 365/15)) + crvAPY = bigNumber.NewFloat(0).Add(bigNumber.NewFloat(0), crvAPR) + cvxAPY = bigNumber.NewFloat(0).Add(bigNumber.NewFloat(0), cvxAPR) return crvAPR, cvxAPR, crvAPY, cvxAPY } diff --git a/processes/apr/forward.curve.helpers.go b/processes/apr/forward.curve.helpers.go index c57d90124..2116c2485 100644 --- a/processes/apr/forward.curve.helpers.go +++ b/processes/apr/forward.curve.helpers.go @@ -172,8 +172,7 @@ func calculateGaugeBaseAPR( baseAPR = bigNumber.NewFloat(0).Mul(baseAPR, bigNumber.NewFloat(0).Div(perMaxBoost, poolPrice)) baseAPR = bigNumber.NewFloat(0).Mul(baseAPR, crvPrice) baseAPR = bigNumber.NewFloat(0).Div(baseAPR, baseAssetPrice) - baseAPRFloat64, _ := baseAPR.Float64() - baseAPY := bigNumber.NewFloat(0).SetFloat64(convertFloatAPRToAPY(baseAPRFloat64, 365/15)) + baseAPY := bigNumber.NewFloat(0).Add(bigNumber.NewFloat(0), baseAPR) return baseAPR, baseAPY }