Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

v0 source pure

Package twap is a fixed-window trailing average of an integer series: cheap to keep, cheap to read, and hard to move ...

Overview

Package twap is a fixed-window trailing average of an integer series: cheap to keep, cheap to read, and hard to move with a one-block spike.

It answers one question — "what has this number averaged over the last W?" — which is what an on-chain market wants from a price oracle and what a token vote wants from a supply-or-turnout figure: a value that a flash spike cannot shift, because moving the average requires HOLDING the pushed value for a real fraction of the window.

Not checkpoint

The sibling p/kourt/checkpoint answers a different question — "what was this value at sealed epoch N?" — from unbounded, paged history. This is a bounded rolling window with no archive: the last N buckets and nothing older. Different query, different structure, no overlap.

A value, not an object

A Ring is a plain value the caller stores inline on a record it was going to write anyway — a market's own row. It is never a heap object of its own, and never held by pointer in realm state: every method takes a Ring by value and returns an updated one. The reason to keep it a value is purely GAS, not safety: gno charges per object touched, so a Ring that rides its host's write costs nothing extra to keep, whereas a *Ring would be a second object (a second key) — and it would buy nothing, since every method is a value receiver that hands back a copy anyway. There is no mutator to borrow and so no security question here at all; the value shape is a cost decision.

Example
1r = r.Observe(height, price)   // store r back onto your own record
2avg, ok := r.Average(height, window)

Buckets, and why a spike does not move the average

Time is quantised into fixed-width buckets. Each bucket remembers the last value seen in it; a bucket with no observation carries the previous value forward, so a quiet series reads as "unchanged", not as a gap. The average over a window of W is the mean of the W/width most recent buckets.

A spike therefore lands in at most one bucket — one part in W/width of the average — and only if it is still the last value in that bucket when the bucket closes. To move the average by a fraction f of the series' range you must keep it moved across about f·(W/width) buckets, i.e. hold the pushed value for f of the whole window against everyone trading back. One block is ~1/(W/width) of that, which for a week of hourly buckets is under a thousandth of a spike.

The freshness contract

That guarantee holds only while observations keep arriving. An empty bucket carries the last value forward, so if the caller STOPS observing, the last value — a spike included — persists across the whole window and the average becomes that single value, still reported mature. So the caller must Observe on every change to the tracked quantity, OR Observe at the height it later reads. StaleBy reports how far a read has drifted from the newest observation, for a caller that cannot guarantee the former. `mature` means "the window is covered", never "the data is recent".

Functions 2

func Load

1func Load(width int64, n, bpp int, head int, base, last int64, filled int, buf string) Ring
source

Load reconstructs a ring from a previously stored header and its buf.

The three shape numbers are the caller's own constants; only buf is data, so a caller that keeps width/n/bpp as package constants stores just the head fields and the bytes.

Load validates SHAPE — buf length, and head/filled in range — and panics on a mismatch. It TRUSTS base and last: a wrong value there only skews a later average (never reads out of bounds), so pass back exactly what the accessors returned rather than hand-built numbers.

func New

1func New(width int64, n, bpp int) Ring
source

New builds an empty ring of n buckets, each width height-units wide, storing bpp bytes per sample.

The window a caller later asks Average for must be at most n*width, or the ring cannot cover it and the read is reported immature. Size n to the longest window you will read.

width is the resolution-versus-cost trade: a finer width puts more buckets in a window (more storage, a longer Average scan) but resists a spike that is held for less real time; a coarser width is cheaper and blunter. n*width fixes the span, so the two are chosen together — e.g. a week resisted at hourly grain is width=1h, n=168.

Types 1

type Ring

struct
 1type Ring struct {
 2	width  int64  // bucket width, in the caller's height units
 3	n      int    // number of buckets
 4	bpp    int    // bytes per sample
 5	head   int    // ring index of the newest bucket
 6	base   int64  // bucket number (height/width) at head
 7	last   int64  // most recent value, carried across empty buckets
 8	filled int    // buckets ever written, for maturity
 9	buf    string // n*bpp packed big-endian samples
10}
source

Ring is a fixed-window trailing average. The zero value is not usable; build one with New.

bpp is bytes per sample: 1 for a 0..255 value such as a percentage price, up to 8 for a full int64. Samples are packed big-endian into buf so the whole history is one []byte-shaped field rather than N times the 40 bytes gno spends on a slice element of any wider type.

Methods on Ring

func Average

method on Ring
1func (r Ring) Average(height, window int64) (avg int64, mature bool)
source

Average returns the trailing average over [height-window, height] and whether the ring held enough history to cover the whole window.

mature means the window is COVERED by real buckets — not that those buckets are RECENT. An empty tail carries the last value forward (see StaleBy and the freshness contract), so a lone spike that is the last observation before a quiet spell fills the window and still reads mature. A caller acting on the average against manipulation — gating a vote, pricing a payout — must BOTH refuse to act while mature is false AND keep the read fresh: Observe at (or near) the height it reads, or gate on StaleBy. Reading is pure — a transient decode, no object touched — so it is safe inside a Render.

func Base

method on Ring
1func (r Ring) Base() int64
source

func Bytes

method on Ring
1func (r Ring) Bytes() string
source

func Filled

method on Ring
1func (r Ring) Filled() int
source

func Head

method on Ring
1func (r Ring) Head() int
source

Head, Base, Last, Filled, Bytes expose the fields a caller stores alongside buf to reconstruct the ring with Load.

func Last

method on Ring
1func (r Ring) Last() int64
source

func Observe

method on Ring
1func (r Ring) Observe(height, value int64) Ring
source

Observe records value as of height and returns the updated ring.

Height must not go backwards below the newest bucket already written; a chain height only ever increases, and a caller that resets it (a test harness) is the one case this refuses, loudly, because a backwards write would corrupt every later average.

func StaleBy

method on Ring
1func (r Ring) StaleBy(height int64) int64
source

StaleBy reports how many buckets the read height lies beyond the newest observation. Zero means the newest real value sits in height's own bucket; a larger number means the window a caller is about to Average is that many buckets of carried-forward value rather than fresh data. Because `mature` only says the window is covered, a caller acting against manipulation should require StaleBy to be small — ideally 0, i.e. Observe at the height it reads (see the freshness contract on the package).

Imports 1

  • math/overflow stdlib

Source Files 2