本文へ移動
← すべてのサンプル

サンプル 44

Absorption zones monitor

absorption zones with a life, watched bar by bar. A closed one-minute bar that traded at least 2.5 times the recent average volume, with

カテゴリ
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 zones named "absorption_zones" on the active chart (replaced at every bar); sounds the alert chime on a retest.
実行方法
Script Hooks -> add this script to "Always on (monitor)", grant market.read, workspace.read and workspace.write. Set MAX_WAKES to a number to
言語
>=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.0

ソース

// Purpose: absorption zones with a life, watched bar by bar. A closed one-minute bar that traded at least 2.5 times the recent average volume, with
// at least 70% of it on one side, and barely moved (its range is no more than the recent average) is a zone: aggressive buyers (or sellers) were met by
// passive size that did not give way. The zone is drawn from the bar to now; the first time price comes back into it the monitor raises an alert (a
// retest, the moment these zones are traded from) and says so on the zone; and the zone ends when a bar trades wholly through it, or after two hours.
// **A bar the capture did not record is neither a zone nor evidence against one**: it is counted and passed over, and a zone is never invalidated or
// retested on the strength of a bar that was not there.
// 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 zones named "absorption_zones" on the active chart (replaced at every bar); sounds the alert chime on a retest.
// Run: Script Hooks -> add this script to "Always on (monitor)", grant market.read, workspace.read and workspace.write. 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";
import std.time;

const OWNER: String = "absorption_zones";
const MAX_WAKES: Int = 0;
const CATCH_UP: Duration = 6h;
const STEP: Duration = 1min;
const BAR: String = "1m";
const RECENT: Int = 30;
const WARM_UP: Int = 10;
const HEAVY: Float = 2.5;
const ONE_SIDED: Float = 0.7;
const LIFE: Duration = 2h;

struct Zone {
    born: Timestamp,
    low: Float,
    high: Float,
    // 1: aggressive buyers were absorbed (resistance); -1: aggressive sellers were absorbed (support).
    side: Int,
    retested: Bool,
}

struct State {
    cursor: Timestamp,
    volumes: List<Float>,
    ranges: List<Float>,
    zones: List<Zone>,
    born: Int,
    retests: Int,
    ended: Int,
    not_recorded: Int,
}

fn average(xs: List<Float>) -> Float {
    let mut sum: Float = 0.0;
    let mut n = 0;
    let mut i = xs.len() - RECENT;
    if i < 0 {
        i = 0;
    }
    while i < xs.len() {
        sum = sum + xs.get(i).expect("a bar");
        n = n + 1;
        i = i + 1;
    }
    if n == 0 {
        return 0.0;
    }
    return sum / to_float(n);
}

fn point(x: Float) -> Price {
    return price(decimal_from_float(x, 2), USDT);
}

// What one recorded bar does to the zones: it may end some, retest some, and found one.
fn watch(chart: workspace.Chart, state: State, bar: flow.Bar) -> Result<State, Error> {
    let mut next = state;
    let mut high = to_float(bar.cells.first().expect("a cell").price.amount());
    let mut low = high;
    let mut buy: Float = 0.0;
    let mut sell: Float = 0.0;
    for cell in bar.cells {
        let at = to_float(cell.price.amount());
        if at > high {
            high = at;
        }
        if at < low {
            low = at;
        }
        buy = buy + to_float(cell.buy.value());
        sell = sell + to_float(cell.sell.value());
    }
    let volume = buy + sell;
    let range = high - low;

    // 1. The zones already there: ended, or retested.
    let mut kept: List<Zone> = [];
    for zone in next.zones {
        let through = (zone.side == 1 && low > zone.high) || (zone.side == -1 && high < zone.low);
        let old = bar.open_time - zone.born > LIFE;
        if through || old {
            next.ended = next.ended + 1;
        } else {
            let mut alive = zone;
            if !alive.retested && bar.open_time - alive.born >= 3min && high >= alive.low && low <= alive.high {
                alive.retested = true;
                next.retests = next.retests + 1;
                let what = if alive.side == 1 { "resistance" } else { "support" };
                alert.notify("Absorption retest", chart.symbol + ": price is back in the " + what + " zone " + to_string(alive.low) + " - " + to_string(alive.high))?;
                print("retest of the " + what + " zone " + to_string(alive.low) + " - " + to_string(alive.high));
            }
            kept.push(alive);
        }
    }
    next.zones = kept;

    // 2. This bar, against the recent ones -- before it joins them.
    if next.volumes.len() >= WARM_UP && volume > 0.0 {
        let share = if buy >= sell { buy / volume } else { sell / volume };
        if volume >= HEAVY * average(next.volumes) && share >= ONE_SIDED && range <= average(next.ranges) {
            let side = if buy >= sell { 1 } else { -1 };
            next.zones.push(Zone { born: bar.open_time, low: low, high: high, side: side, retested: false });
            next.born = next.born + 1;
            print("zone " + to_string(low) + " - " + to_string(high) + ": " + to_string(volume) + " traded, " + to_string(share) + " on one side");
        }
    }
    next.volumes.push(volume);
    next.ranges.push(range);
    return Ok(next);
}

fn catch_up(chart: workspace.Chart, state: State, 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(chart.symbol, BAR, next.cursor, to, price(0.1, USDT))?;
        for bar in fp.bars {
            // A bar nothing traded in is not a zone and not a hole; one the capture lost is counted.
            if bar.known && !bar.cells.is_empty() {
                next = watch(chart, next, bar)?;
            } else if !bar.known {
                next.not_recorded = next.not_recorded + 1;
            }
            next.cursor = bar.open_time + STEP;
        }
        // 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);
}

// The zones that are alive, drawn from where they were born to now.
fn draw(chart: workspace.Chart, state: State, now: Timestamp) -> Result<Unit, Error> {
    plot.clear(chart.chart, OWNER)?;
    for zone in state.zones {
        let what = if zone.side == 1 { "resistance" } else { "support" };
        let label = if zone.retested { what + " (retested)" } else { what };
        let colour = if zone.side == 1 { "red" } else { "green" };
        plot.zone(chart.chart, OWNER, zone.born, now, point(zone.low), point(zone.high), label, colour)?;
    }
    return Ok(());
}

fn main() -> Result<Unit, Error> {
    let mut state = State { cursor: time.floor(live.now(), 1h), volumes: [], ranges: [], zones: [], born: 0, retests: 0, ended: 0, not_recorded: 0 };
    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) => {
                match catch_up(chart, state, wake.now) {
                    Ok(next) => {
                        state = next;
                        draw(chart, state, wake.now)?;
                    }
                    Err(why) => print("no reading this bar: " + why.code + ": " + why.message),
                }
            }
            None => {}
        }
    }
    print(to_string(state.born) + " zone(s) found, " + to_string(state.retests) + " retest(s), " + to_string(state.ended) + " ended, " + to_string(state.not_recorded) + " bar(s) not recorded");
    return Ok(());
}