← すべてのサンプル
サンプル 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(()); }