github.com/diadata-org/diadata@v1.4.593/internal/pkg/filtersBlockService/FilterMAIR.go (about) 1 package filters 2 3 import ( 4 "strconv" 5 "time" 6 7 "github.com/diadata-org/diadata/pkg/dia" 8 models "github.com/diadata-org/diadata/pkg/model" 9 log "github.com/sirupsen/logrus" 10 ) 11 12 // FilterMAIR implements a trimmed moving average. 13 // Outliers are eliminated using interquartile range. 14 // see: https://en.wikipedia.org/wiki/Interquartile_range 15 type FilterMAIR struct { 16 asset dia.Asset 17 exchange string 18 currentTime time.Time 19 prices []float64 20 volumes []float64 21 lastTrade dia.Trade 22 memory int 23 value float64 24 filterName string 25 nativeDenomination bool 26 modified bool 27 } 28 29 // NewFilterMAIR returns a FilterMAIR 30 func NewFilterMAIR(asset dia.Asset, exchange string, currentTime time.Time, memory int, nativeDenomination bool) *FilterMAIR { 31 filter := &FilterMAIR{ 32 asset: asset, 33 exchange: exchange, 34 prices: []float64{}, 35 volumes: []float64{}, 36 currentTime: currentTime, 37 memory: memory, 38 filterName: "MAIR" + strconv.Itoa(memory), 39 nativeDenomination: nativeDenomination, 40 } 41 return filter 42 } 43 44 func (filter *FilterMAIR) Compute(trade dia.Trade) { 45 filter.compute(trade) 46 } 47 48 func (filter *FilterMAIR) compute(trade dia.Trade) { 49 filter.modified = true 50 if filter.lastTrade != (dia.Trade{}) { 51 if trade.Time.Before(filter.currentTime) { 52 log.Errorln("FilterMAIR: Ignoring Trade out of order ", filter.currentTime, trade.Time) 53 return 54 } 55 } 56 filter.fill(trade) 57 filter.lastTrade = trade 58 } 59 60 // fill fills up the 120 seconds slots with trades. 61 func (filter *FilterMAIR) fill(trade dia.Trade) { 62 // filter.currentTime is the timestamp of the last filled trade. 63 // It is initialized with begin time of tradesblock upon creation of the filter. 64 diff := int(trade.Time.Sub(filter.currentTime).Seconds()) 65 if diff >= 1 { 66 for diff >= 1 { 67 filter.processDataPoint(trade) 68 diff-- 69 } 70 } else { 71 if diff == 0.0 { 72 if len(filter.prices) >= 1 { 73 /// Remove latest data point and update with newer 74 filter.prices = filter.prices[:len(filter.prices)-1] 75 filter.volumes = filter.volumes[:len(filter.volumes)-1] 76 } 77 } 78 filter.processDataPoint(trade) 79 } 80 filter.currentTime = time.Unix(int64(trade.Time.Unix()), 0) 81 } 82 83 func (filter *FilterMAIR) processDataPoint(trade dia.Trade) { 84 /// first remove extra value from buffer if already full 85 if len(filter.prices) >= filter.memory { 86 filter.prices = filter.prices[0 : filter.memory-1] 87 filter.volumes = filter.volumes[0 : filter.memory-1] 88 } 89 if !filter.nativeDenomination { 90 filter.prices = append(filter.prices, trade.EstimatedUSDPrice) 91 } else { 92 filter.prices = append(filter.prices, trade.Price) 93 } 94 95 filter.volumes = append(filter.volumes, trade.Volume) 96 } 97 98 func (filter *FilterMAIR) FinalCompute(t time.Time) float64 { 99 return filter.finalCompute(t) 100 } 101 102 func (filter *FilterMAIR) finalCompute(t time.Time) float64 { 103 if filter.lastTrade == (dia.Trade{}) { 104 return 0.0 105 } 106 107 if len(filter.prices) < 2 { 108 filter.value = filter.prices[0] 109 return filter.prices[0] 110 } 111 112 // Add the last trade again to compensate for the delay since measurement to EOB 113 // adopted behaviour from FilterMA 114 filter.processDataPoint(filter.lastTrade) 115 cleanPrices, bounds := removeOutliers(filter.prices) 116 mean, err := computeMean(cleanPrices, filter.volumes[bounds[0]:bounds[1]]) 117 if err != nil { 118 return 0.0 119 } 120 filter.value = mean 121 // Reduce the filter values to the last recorded value for the next tradesblock. 122 if len(filter.prices) > 0 && len(filter.volumes) > 0 { 123 if !filter.nativeDenomination { 124 filter.prices = []float64{filter.lastTrade.EstimatedUSDPrice} 125 } else { 126 filter.prices = []float64{filter.lastTrade.Price} 127 } 128 filter.volumes = []float64{filter.lastTrade.Volume} 129 } 130 return filter.value 131 } 132 133 func (filter *FilterMAIR) FilterPointForBlock() *dia.FilterPoint { 134 return &dia.FilterPoint{ 135 Asset: filter.asset, 136 Value: filter.value, 137 Name: filter.filterName, 138 Time: filter.currentTime, 139 } 140 } 141 142 func (filter *FilterMAIR) filterPointForBlock() *dia.FilterPoint { 143 if filter.exchange != "" || filter.filterName != dia.FilterKing { 144 return nil 145 } 146 return &dia.FilterPoint{ 147 Asset: filter.asset, 148 Value: filter.value, 149 Name: filter.filterName, 150 Time: filter.currentTime, 151 } 152 } 153 154 func (filter *FilterMAIR) save(ds models.Datastore) error { 155 if filter.modified { 156 filter.modified = false 157 err := ds.SetFilter(filter.filterName, filter.asset, filter.exchange, filter.value, filter.currentTime) 158 if err != nil { 159 log.Errorln("FilterMAIR: Error:", err) 160 } 161 162 // Additionally, the price across exchanges is saved in influx as a quotation. 163 // This price is used for the estimation of quote tokens' prices in the tradesBlockService. 164 if filter.exchange == "" { 165 err = ds.SetAssetPriceUSD(filter.asset, filter.value, filter.currentTime) 166 if err != nil { 167 log.Errorln("FilterMAIR: Error:", err) 168 } 169 } 170 return err 171 } 172 return nil 173 }