///|
pub(all) enum ChartAnnotationKind {
SnapshotSummary
StructureLevelSummary
DecisionSummary
RiskNote
} derive(Debug, Eq, ToJson, FromJson)
///|
pub(all) struct CandlePoint {
seq : Int
open : Double
high : Double
low : Double
close : Double
volume : Double
} derive(Debug, Eq, ToJson, FromJson)
///|
pub(all) struct PriceRange {
low : Double
high : Double
} derive(Debug, Eq, ToJson, FromJson)
///|
pub(all) struct ChartAnnotation {
kind : ChartAnnotationKind
label : String
body : String
} derive(Debug, Eq, ToJson, FromJson)
///|
pub(all) struct ChartFrame {
artifact_path : String
source_id : @domain.SourceId
symbol : @domain.Symbol
timeframe : @domain.Timeframe
width : Int
height : Int
candles : Array[CandlePoint]
price_range : PriceRange?
annotations : Array[ChartAnnotation]
} derive(Debug, Eq, ToJson, FromJson)
///|
pub fn candle_point(bar : @domain.KlineBar) -> CandlePoint {
{
seq: bar.seq,
open: bar.open,
high: bar.high,
low: bar.low,
close: bar.close,
volume: bar.volume,
}
}
///|
pub fn price_range(low : Double, high : Double) -> PriceRange {
{ low, high }
}
///|
pub fn chart_annotation(
kind : ChartAnnotationKind,
label : String,
body : String,
) -> ChartAnnotation {
{ kind, label, body }
}
///|
fn min_double(a : Double, b : Double) -> Double {
if a < b {
a
} else {
b
}
}
///|
fn max_double(a : Double, b : Double) -> Double {
if a > b {
a
} else {
b
}
}
///|
fn closed_candles(snapshot : @domain.KlineSnapshot) -> Array[CandlePoint] {
snapshot.bars
.filter(fn(bar) { bar.closed })
.map(fn(bar) { candle_point(bar) })
}
///|
fn candle_price_range(candles : Array[CandlePoint]) -> PriceRange? {
if candles.is_empty() {
return None
}
let mut low = candles[0].low
let mut high = candles[0].high
for candle in candles {
low = min_double(low, candle.low)
high = max_double(high, candle.high)
}
Some(price_range(low, high))
}
///|
fn snapshot_annotation(
snapshot : @domain.KlineSnapshot,
candles : Array[CandlePoint],
) -> ChartAnnotation {
chart_annotation(
SnapshotSummary,
"snapshot",
"\{snapshot.symbol} \{snapshot.timeframe} closed_bars=\{candles.length()}",
)
}
///|
fn structure_annotation(
levels : @domain.StructureLevelFrame?,
) -> Array[ChartAnnotation] {
match levels {
None => []
Some(frame) =>
[
chart_annotation(
StructureLevelSummary,
"structure",
"\{frame.summary}; midline=\{frame.midline is Some(_)}",
),
]
}
}
///|
fn action_label(action : @domain.DecisionAction) -> String {
match action {
NoOrder => "NoOrder"
Wait => "Wait"
LimitOrder => "LimitOrder"
BreakoutOrder => "BreakoutOrder"
MarketOrder => "MarketOrder"
}
}
///|
fn decision_annotations(
decision : @domain.Stage2Decision?,
) -> Array[ChartAnnotation] {
match decision {
None => []
Some(decision) => {
let annotations = [
chart_annotation(
DecisionSummary,
"decision",
"action=\{action_label(decision.action)}\nconfidence=\{decision.confidence}\nrationale=\{decision.rationale}",
),
]
for note in decision.risk_notes {
annotations.push(chart_annotation(RiskNote, "risk", note))
}
annotations
}
}
}
///|
pub fn render_chart_frame(
snapshot : @domain.KlineSnapshot,
decision? : @domain.Stage2Decision,
structure_levels? : @domain.StructureLevelFrame,
width? : Int = 1280,
height? : Int = 720,
) -> ChartFrame {
let candles = closed_candles(snapshot)
{
artifact_path: "apps/moondesk-price-action/chart-frames/\{snapshot.source_id}.json",
source_id: snapshot.source_id,
symbol: snapshot.symbol,
timeframe: snapshot.timeframe,
width,
height,
candles,
price_range: candle_price_range(candles),
annotations: [snapshot_annotation(snapshot, candles)] +
structure_annotation(structure_levels) +
decision_annotations(decision),
}
}