package strategy import ( "sort" "sig-pub/pkg/types" ) // MeanReversionV1 type MeanReversionV1 struct { IIntervalSigStrategy interval types.Interval period int threshold float64 buckets int dominanceRatio float64 // POC volume dominance ratio (vs 2nd highest) rsiPeriod int rsiThreshold float64 } func (s *MeanReversionV1) New() ISigStrategy { return &MeanReversionV1{} } func (s *MeanReversionV1) Meta() StrategyMeta { return StrategyMeta{ Name: "MeanReversionV1", Desc: "VRVP Mean Reversion Strategy with Volume Dominance and RSI Filter", Input: []types.InputArg{ {Name: "interval", Type: types.InputTypeString, Desc: "Target Interval (e.g., 1m, 1h)", Default: "1m"}, {Name: "period", Type: types.InputTypeInt, Desc: "VRVP calculation window", Default: 100}, {Name: "threshold", Type: types.InputTypeUFloat, Desc: "Reversion Threshold Ratio (e.g. 0.01)", Default: 0.01}, {Name: "buckets", Type: types.InputTypeInt, Desc: "VRVP Buckets", Default: 24}, {Name: "dominance_ratio", Type: types.InputTypeUFloat, Desc: "POC Volume Dominance Ratio (e.g. 1.2)", Default: 1.2}, {Name: "rsi_period", Type: types.InputTypeInt, Desc: "RSI Period", Default: 14}, {Name: "rsi_threshold", Type: types.InputTypeUFloat, Desc: "RSI Threshold (e.g. 30 for 30/70)", Default: 30}, }, } } func (s *MeanReversionV1) Init(input types.Input) (err error) { s.interval = types.Interval(input.String("interval")) if _, ok := types.SupportedIntervals[s.interval]; !ok { s.interval = types.Interval1m } s.period = input.Int("period") if s.period <= 0 { s.period = 100 } s.threshold = input.Float("threshold") s.buckets = input.Int("buckets") if s.buckets <= 0 { s.buckets = 24 } s.dominanceRatio = input.Float("dominance_ratio") if s.dominanceRatio < 1.0 { s.dominanceRatio = 1.0 } s.rsiPeriod = input.Int("rsi_period") if s.rsiPeriod <= 0 { s.rsiPeriod = 14 } s.rsiThreshold = input.Float("rsi_threshold") if s.rsiThreshold <= 0 || s.rsiThreshold >= 50 { s.rsiThreshold = 30 // Default to standard 30 (implying 70 upper) } return } func (s *MeanReversionV1) CandlePeriods(ctx IIntervalSigStrategyContext) (iss *types.IntervalState[int16]) { iss = types.NewIntervalState[int16]() // We need enough candles for both VRVP and RSI // VRVP needs 'period' candles. // RSI needs 'rsiPeriod' candles (maybe +1). // To be safe, we take the max. needed := int16(s.period) if int16(s.rsiPeriod+5) > needed { needed = int16(s.rsiPeriod + 5) } iss.Set(s.interval, needed) return } func (s *MeanReversionV1) Update(ctx IIntervalSigStrategyContext) (side types.Side) { // 1. Get VRVP Summary summaryObj := ctx.SummaryIndicator(s.interval, "VRVP", map[string]any{"buckets": s.buckets}) // Calculate for the last 'period' candles summaryAny, ok := summaryObj.Summary(0, int16(s.period)) if !ok { return } vrvpSummary, ok := summaryAny.(*types.VRVPSummary) if !ok || vrvpSummary == nil || len(vrvpSummary.Buckets) < 2 { return } // 2. Find POC (Point of Control) and Second Highest Volume // Create a slice of buckets to sort type volBucket struct { Price float64 Volume float64 } sortedBuckets := make([]volBucket, len(vrvpSummary.Buckets)) for i, b := range vrvpSummary.Buckets { sortedBuckets[i] = volBucket{Price: b.Price, Volume: b.Volume} } // Sort descending by volume sort.Slice(sortedBuckets, func(i, j int) bool { return sortedBuckets[i].Volume > sortedBuckets[j].Volume }) pocBucket := sortedBuckets[0] secondBucket := sortedBuckets[1] // Check Dominance if pocBucket.Volume < secondBucket.Volume*s.dominanceRatio { // POC is not dominant enough return } pocPrice := pocBucket.Price if pocPrice <= 0 { return } // 3. Get Current Price k := ctx.Get(s.interval, 0) currentPrice := k.CloseF64() // 4. Calculate RSI for Confirmation rsiSeries := ctx.Indicator(s.interval, "RSI", map[string]any{"window": s.rsiPeriod}) currentRSI := rsiSeries.Get(0) // 5. Generate Signal deviation := (currentPrice - pocPrice) / pocPrice if deviation > s.threshold { // Price is significantly higher than POC, expect reversion (Sell) // Filter: RSI should be overbought (> 100 - threshold, e.g. > 70) if currentRSI > (100 - s.rsiThreshold) { side = types.SideShort } } else if deviation < -s.threshold { // Price is significantly lower than POC, expect reversion (Buy) // Filter: RSI should be oversold (< threshold, e.g. < 30) if currentRSI < s.rsiThreshold { side = types.SideLong } } return }