Executing Scripts
Candlestick
The Candlestick struct represents one OHLCV bar:
use openpine_vm::{Candlestick, market::TradeSession};
let candle = Candlestick::new(
1700000000000, // time: epoch milliseconds
150.0, // open
155.0, // high
149.0, // low
153.0, // close
1_000_000.0, // volume
0.0, // turnover
TradeSession::Regular, // trade session
);Candlestick also provides derived price calculations:
candle.hl2(); // (high + low) / 2
candle.hlc3(); // (high + low + close) / 3
candle.ohlc4(); // (open + high + low + close) / 4
candle.hlcc4(); // (high + low + close + close) / 4TimeFrame
TimeFrame specifies the chart period. It can be created using convenience methods or parsed from a string:
use openpine_vm::TimeFrame;
// Convenience constructors
let tf = TimeFrame::days(1);
let tf = TimeFrame::minutes(5);
let tf = TimeFrame::weeks(1);
let tf = TimeFrame::months(1);
let tf = TimeFrame::seconds(30);
// Parse from string (TradingView format)
let tf: TimeFrame = "D".parse().unwrap(); // 1 day
let tf: TimeFrame = "5".parse().unwrap(); // 5 minutes
let tf: TimeFrame = "60".parse().unwrap(); // 60 minutes (1 hour)
let tf: TimeFrame = "W".parse().unwrap(); // 1 week
let tf: TimeFrame = "3M".parse().unwrap(); // 3 months
// Inspect
let seconds = tf.in_seconds(); // Option<u64>
let display = tf.to_string(); // "D", "5", "W", etc.SymbolInfo
SymbolInfo carries metadata about the trading instrument. You no longer construct it manually — just pass the symbol string to Instance::builder and the builder resolves the info automatically (from DataProvider::symbol_info if a provider is configured, otherwise from built-in defaults).
Supported market prefixes: NYSE, NASDAQ, SHSE, SZSE, HKEX, SGX.
To supply custom symbol metadata (description, currency, min tick, etc.), implement DataProvider::symbol_info and return a PartialSymbolInfo. See Instance Builder for details.
Feeding Data
Historical Backtest
Pass a Vec<Candlestick> directly to Instance::builder(), then call instance.run_to_end():
use openpine_vm::TimeFrame;
let mut instance = Instance::builder(historical_data, source, TimeFrame::days(1), "NASDAQ:AAPL")
.build().await?;
instance.run_to_end("NASDAQ:AAPL", TimeFrame::days(1)).await?;For lazy or streaming data, collect into a Vec<Candlestick> first, or implement a custom DataProvider:
// Collect from an iterator
let bars: Vec<Candlestick> = load_bars_from_db().collect();
let mut instance = Instance::builder(bars, source, timeframe, "NASDAQ:AAPL")
.build().await?;Live Streaming
For live data, implement DataProvider and yield CandlestickItem::Confirmed(candle) for closed bars and CandlestickItem::Realtime(candle) for the current forming bar. Emit CandlestickItem::HistoryEnd after all historical bars to signal that live data follows. The VM automatically handles rollback between realtime updates and confirms the bar when the stream ends.
Event Stream
Instance::run() returns a Stream<Item = Result<Event, Error>> that yields events as the script executes bar by bar.
Event Types
| Event | When emitted |
|---|---|
BarStart(BarStartEvent) | Before each bar's execution begins. Contains bar_index, timestamp (epoch ms), and bar_state (see below). |
BarEnd | After each bar's execution completes (strategy processing, script body, etc.). Always paired with a preceding BarStart. |
HistoryEnd | Once, when the DataProvider yields CandlestickItem::HistoryEnd. Signals the transition from historical replay to live data. Not preceded by BarStart. |
Log(LogEvent) | During bar execution, when the script calls log.info(), log.warning(), or log.error(). Emitted between BarStart and BarEnd. |
Alert(AlertEvent) | During bar execution, when the script calls alert() or an alertcondition() triggers. Emitted between BarStart and BarEnd. |
Draw(DrawEvent) | During bar execution in OutputMode::Stream only, when the script calls plot(). AddPlot is emitted once on the first bar; UpdatePlot is emitted on every bar. |
Bar States
BarStartEvent.bar_state tells you why this bar is being executed:
| State | Meaning |
|---|---|
History | A closed historical bar from CandlestickItem::Confirmed. Each bar executes exactly once. bar_index advances after each bar. This is the state during the initial backtest replay. |
RealtimeNew | The first tick of a new real-time bar. Either the very first CandlestickItem::Realtime after HistoryEnd, or a Realtime whose timestamp is later than the previous one (which implicitly confirms the previous bar first). bar_index does not advance until the bar is confirmed. |
RealtimeUpdate | A subsequent tick for the same real-time bar (same timestamp as the previous Realtime). The VM rolls back variable state to the bar's initial snapshot, then re-executes the script with updated OHLCV data. Same bar_index as the preceding RealtimeNew. |
RealtimeConfirmed | The current real-time bar is being confirmed (closed). This happens in two cases: (1) a new Realtime tick arrives with a later timestamp — the VM auto-confirms the previous bar before opening the new one; (2) the data stream ends while a real-time bar is still open — the VM confirms it as the final step. After confirmation, bar_index advances. |
Event Ordering
For a typical historical-then-realtime session, the stream yields:
BarStart(0, History) → Log/Alert/Draw... → BarEnd
BarStart(1, History) → Log/Alert/Draw... → BarEnd
...
HistoryEnd
BarStart(N, RealtimeNew) → ... → BarEnd ← first tick of bar N
BarStart(N, RealtimeUpdate) → ... → BarEnd ← same-timestamp tick update
BarStart(N, RealtimeConfirmed) → ... → BarEnd ← bar N confirmed (new timestamp arrived)
BarStart(N+1, RealtimeNew) → ... → BarEnd ← first tick of bar N+1
BarStart(N+1, RealtimeConfirmed) → ... → BarEnd ← stream ended, final confirmUsage
use std::pin::pin;
use futures_util::StreamExt;
use openpine_vm::{Event, Instance, TimeFrame};
let mut stream = pin!(instance.run("NASDAQ:AAPL", TimeFrame::days(1)));
while let Some(result) = stream.next().await {
match result? {
Event::BarStart(bs) => {
println!("bar {} started (state={:?})", bs.bar_index, bs.bar_state);
}
Event::BarEnd => { /* bar finished */ }
Event::HistoryEnd => {
println!("history replay complete, live bars follow");
}
Event::Log(log) => println!("[{:?}] {}", log.level, log.message),
Event::Alert(alert) => println!("ALERT: {}", alert.message),
Event::Draw(draw) => { /* plot data, only in Stream mode */ }
}
}If you don't need the event stream, use the convenience method run_to_end() which consumes the stream internally:
instance.run_to_end("NASDAQ:AAPL", TimeFrame::days(1)).await?;
// Results available via instance.chart(), instance.strategy_report(), etc.Output Mode
OutputMode controls how plot() data is produced. Set it on the builder:
use openpine_vm::{Instance, OutputMode, TimeFrame};
let mut instance = Instance::builder(provider, source, TimeFrame::days(1), "NASDAQ:AAPL")
.with_output_mode(OutputMode::Stream)
.build().await?;| Mode | plot() behavior | Use case |
|---|---|---|
OutputMode::Chart (default) | Writes to instance.chart() | Read the full chart after execution |
OutputMode::Stream | Emits DrawEvent in the event stream | Stream plot data without building a chart |
Stream Mode Example
In OutputMode::Stream, each plot() call emits events instead of writing to the chart:
use std::pin::pin;
use futures_util::StreamExt;
use openpine_vm::{DrawEvent, Event, Instance, OutputMode, TimeFrame};
let mut instance = Instance::builder(bars, source, TimeFrame::days(1), "NASDAQ:AAPL")
.with_output_mode(OutputMode::Stream)
.build().await?;
let mut stream = pin!(instance.run("NASDAQ:AAPL", TimeFrame::days(1)));
while let Some(result) = stream.next().await {
match result? {
Event::Draw(DrawEvent::AddPlot { id, title }) => {
println!("new plot: id={}, title={:?}", id, title);
}
Event::Draw(DrawEvent::UpdatePlot { id, bar_index, timestamp, value }) => {
println!("plot {} bar {}: {:?}", id, bar_index, value);
}
_ => {}
}
}BarStart, BarEnd, HistoryEnd, Log, and Alert events are emitted in both modes. Only Draw events are affected by the output mode.
Next Steps
- Reading Outputs — plots, labels, strategy orders
- Error Handling — handling compile and runtime errors