You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
154 lines
4.5 KiB
154 lines
4.5 KiB
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 |
|
}
|
|
|