본문으로 건너뛰기
← 모든 예제

예제 42

Vwap monitor

a VWAP that watches, as an institutional desk computes it -- every trade at the price it printed, merged bar by bar into a running mean and

분류
Workspace: 차트와 패널을 읽거나 변경
권한
market.readworkspace.readworkspace.write
실행하기 전에
a terminal connected to a backend that records the footprint of the chart's symbol; the script runs as an "Always on" hook
하는 일
DRAWS levels named "vwap_monitor" on the active chart (replaced at every bar); sounds the alert chime on a close beyond a band.
실행 방법
Script Hooks -> add this script to "Always on (monitor)", grant market.read, workspace.read and workspace.write. It starts with the terminal and
언어
>=0.1.1, <0.2.0
호스트
>=0.1.8, <0.2.0
필요한 것
flow >=0.1.0, <0.2.0live >=0.1.0, <0.2.0workspace >=0.1.0, <0.2.0plot >=0.1.0, <0.2.0alert >=0.1.0, <0.2.0market >=0.1.3, <0.2.0

소스

// Purpose: a VWAP that watches, as an institutional desk computes it -- every trade at the price it printed, merged bar by bar into a running mean and
// spread (West's update: exact, and stable at a price of 60 000) -- kept in the program's own variables between bars. It draws the session VWAP and its
// two-deviation bands on the active chart, and raises an alert when price closes beyond a band, once per excursion (it is armed again when price comes
// back inside by half a deviation). **It states what it did not see**: a bar the capture did not record is counted, and while any is missing the VWAP is
// labelled incomplete and no alert is raised on it.
// Category: workspace
// Permissions: market.read, workspace.read, workspace.write
// Preconditions: a terminal connected to a backend that records the footprint of the chart's symbol; the script runs as an "Always on" hook
// Side effects: DRAWS levels named "vwap_monitor" on the active chart (replaced at every bar); sounds the alert chime on a close beyond a band.
// Run: Script Hooks -> add this script to "Always on (monitor)", grant market.read, workspace.read and workspace.write. It starts with the terminal and
// rests between bars. Set MAX_WAKES to a number to make it stop by itself (that is how it is tested); 0 is for ever.

lang ">=0.1.1, <0.2.0";
host "ttt" ">=0.1.8, <0.2.0";
requires "flow" ">=0.1.0, <0.2.0";
requires "live" ">=0.1.0, <0.2.0";
requires "workspace" ">=0.1.0, <0.2.0";
requires "plot" ">=0.1.0, <0.2.0";
requires "alert" ">=0.1.0, <0.2.0";
requires "market" ">=0.1.3, <0.2.0";
import std.time;
import std.math;

const OWNER: String = "vwap_monitor";
const MAX_WAKES: Int = 0;
const SESSION: Duration = 1d;
const CATCH_UP: Duration = 6h;
const STEP: Duration = 1min;
const BAR: String = "1m";
const BAND: Float = 2.0;
const REARM: Float = 1.5;

// Everything the monitor remembers between bars.
struct State {
    session: Timestamp,
    cursor: Timestamp,
    weight: Float,
    mean: Float,
    m2: Float,
    bars: Int,
    missing: Int,
    side: Int,
}

fn fresh(session: Timestamp) -> State {
    return State { session: session, cursor: session, weight: 0.0, mean: 0.0, m2: 0.0, bars: 0, missing: 0, side: 0 };
}

// Merges one bar's cells into the running mean and spread, trade price by trade price.
fn merge(state: State, cells: List<flow.Cell>) -> State {
    let mut next = state;
    for cell in cells {
        let volume = to_float(cell.buy.value()) + to_float(cell.sell.value());
        if volume > 0.0 {
            let at = to_float(cell.price.amount());
            let total = next.weight + volume;
            let delta = at - next.mean;
            next.mean = next.mean + delta * volume / total;
            next.m2 = next.m2 + volume * delta * (at - next.mean);
            next.weight = total;
        }
    }
    return next;
}

// What the closed bars from the cursor to `now` add. Read in pieces so a whole day is never one request.
fn catch_up(state: State, symbol: String, now: Timestamp) -> Result<State, Error> {
    let mut next = state;
    while next.cursor + STEP <= now {
        let to = if next.cursor + CATCH_UP < now { next.cursor + CATCH_UP } else { now };
        let fp = flow.footprint(symbol, BAR, next.cursor, to, price(0.1, USDT))?;
        for bar in fp.bars {
            if bar.known {
                next = merge(next, bar.cells);
                next.bars = next.bars + 1;
            } else {
                next.missing = next.missing + 1;
            }
            next.cursor = bar.open_time + STEP;
        }
        // Nothing more in this piece: on to the next (and never standing still).
        // Only to the close of the last whole bar: the one still forming is read when it has closed.
        let reached = time.floor(to, STEP);
        if next.cursor < reached {
            next.cursor = reached;
        }
    }
    return Ok(next);
}

// Draws the VWAP and its bands, and raises the alert when a close went beyond one. Returns the state with its side updated.
fn report(chart: workspace.Chart, state: State) -> Result<State, Error> {
    let mut next = state;
    plot.clear(chart.chart, OWNER)?;
    if next.weight <= 0.0 {
        return Ok(next);
    }
    let spread = math.sqrt(next.m2 / next.weight);
    let incomplete = next.missing > 0;
    let note = if incomplete { " (incomplete: " + to_string(next.missing) + " bar(s) not recorded)" } else { "" };
    plot.level(chart.chart, OWNER, price(decimal_from_float(next.mean, 2), USDT), "VWAP" + note, "#e6b43c")?;
    plot.level(chart.chart, OWNER, price(decimal_from_float(next.mean + BAND * spread, 2), USDT), "+" + to_string(BAND) + " sigma", "#9a7a2c")?;
    plot.level(chart.chart, OWNER, price(decimal_from_float(next.mean - BAND * spread, 2), USDT), "-" + to_string(BAND) + " sigma", "#9a7a2c")?;
    if spread <= 0.0 || incomplete {
        return Ok(next);
    }
    let candles = market.chart_candles(chart.symbol, chart.timeframe, 1)?;
    match candles.last() {
        Some(candle) => {
            let z = (to_float(candle.close.amount()) - next.mean) / spread;
            if z >= BAND && next.side != 1 {
                next.side = 1;
                alert.notify("VWAP band", chart.symbol + " closed " + to_string(z) + " sigma above the session VWAP")?;
            }
            if z <= 0.0 - BAND && next.side != -1 {
                next.side = -1;
                alert.notify("VWAP band", chart.symbol + " closed " + to_string(0.0 - z) + " sigma below the session VWAP")?;
            }
            if z < REARM && z > 0.0 - REARM {
                next.side = 0;
            }
        }
        None => {}
    }
    return Ok(next);
}

fn main() -> Result<Unit, Error> {
    let mut state = fresh(time.floor(live.now(), SESSION));
    let mut wakes = 0;
    while MAX_WAKES == 0 || wakes < MAX_WAKES {
        let wake = live.wait(30s)?;
        wakes = wakes + 1;
        match workspace.active_chart() {
            Some(chart) => {
                // A new session starts from nothing: yesterday's trades are not today's VWAP.
                let session = time.floor(wake.now, SESSION);
                if session != state.session {
                    state = fresh(session);
                }
                match catch_up(state, chart.symbol, wake.now) {
                    Ok(next) => {
                        state = report(chart, next)?;
                    }
                    Err(why) => print("no reading this bar: " + why.code + ": " + why.message),
                }
            }
            None => {}
        }
    }
    print("vwap " + to_string(state.mean) + " over " + to_string(state.bars) + " bar(s), " + to_string(state.missing) + " not recorded");
    return Ok(());
}