Bar Index & TimeLibrary to convert a bar index to a timestamp and vice versa.
Utilizes runtime memory to store the ๐๐๐๐ and ๐๐๐๐_๐๐๐๐๐ values of every bar on the chart (and optional future bars), with the ability of storing additional custom values for every chart bar.
โโ PREFACE
This library aims to tackle some problems that pine coders (from beginners to advanced) often come across, such as:
I'm trying to draw an object with a ๐๐๐_๐๐๐๐๐ก that is more than 10,000 bars into the past, but this causes my script to fail. How can I convert the ๐๐๐_๐๐๐๐๐ก to a UNIX time so that I can draw visuals using xloc.bar_time ?
I have a diagonal line drawing and I want to get the "y" value at a specific time, but line.get_price() only accepts a bar index value. How can I convert the timestamp into a bar index value so that I can still use this function?
I want to get a previous ๐๐๐๐ value that occurred at a specific timestamp. How can I convert the timestamp into a historical offset so that I can use ๐๐๐๐ ?
I want to reference a very old value for a variable. How can I access a previous value that is older than the maximum historical buffer size of ๐๐๐๐๐๐๐๐ ?
This library can solve the above problems (and many more) with the addition of a few lines of code, rather than requiring the coder to refactor their script to accommodate the limitations.
โโ OVERVIEW
The core functionality provided is conversion between xloc.bar_index and xloc.bar_time values.
The main component of the library is the ๐ฒ๐๐๐๐๐ณ๐๐๐ object, created via the ๐๐๐๐๐๐๐๐ฒ๐๐๐๐๐ณ๐๐๐() function which basically stores the ๐๐๐๐ and ๐๐๐๐_๐๐๐๐๐ of every bar on the chart, and there are 3 more overloads to this function that allow collecting and storing additional data. Once a ๐ฒ๐๐๐๐๐ณ๐๐๐ object is created, use any of the exported methods:
Methods to convert a UNIX timestamp into a bar index or bar offset:
๐๐๐๐๐๐๐๐๐๐๐๐ฑ๐๐๐ธ๐๐๐๐ก(), ๐๐๐๐ฝ๐๐๐๐๐๐พ๐๐ฑ๐๐๐๐ฑ๐๐๐()
Methods to retrieve the stored data for a bar index:
๐๐๐๐๐ฐ๐๐ฑ๐๐๐ธ๐๐๐๐ก(), ๐๐๐๐๐ฒ๐๐๐๐๐ฐ๐๐ฑ๐๐๐ธ๐๐๐๐ก(), ๐๐๐๐๐๐ฐ๐๐ฑ๐๐๐ธ๐๐๐๐ก(), ๐๐๐๐ฐ๐๐๐
๐๐๐๐๐๐๐๐๐ฐ๐๐ฑ๐๐๐ธ๐๐๐๐ก()
Methods to retrieve the stored data at a number of bars back (i.e., historical offset):
๐๐๐๐(), ๐๐๐๐๐ฒ๐๐๐๐(), ๐๐๐๐๐()
Methods to retrieve all the data points from the earliest bar (or latest bar) stored in memory, which can be useful for debugging purposes:
๐๐๐๐ด๐๐๐๐๐๐๐๐๐๐๐๐๐๐ณ๐๐๐(), ๐๐๐๐ป๐๐๐๐๐๐๐๐๐๐๐๐ณ๐๐๐()
Note: the library's strong suit is referencing data from very old bars in the past, which is especially useful for scripts that perform its necessary calculations only on the last bar.
โโ USAGE
Step 1
Import the library. Replace with the latest available version number for this library.
//@version=6
indicator("Usage")
import n00btraders/ChartData/
Step 2
Create a ๐ฒ๐๐๐๐๐ณ๐๐๐ object to collect data on every bar. Do not declare as `var` or `varip`.
chartData = ChartData.collectChartData() // call on every bar to accumulate the necessary data
Step 3
Call any method(s) on the ๐ฒ๐๐๐๐๐ณ๐๐๐ object. Do not modify its fields directly.
if barstate.islast
int firstBarTime = chartData.timeAtBarIndex(0)
int lastBarTime = chartData.time(0)
log.info("First `time`: " + str.format_time(firstBarTime) + ", Last `time`: " + str.format_time(lastBarTime))
โโ EXAMPLES
โข Collect Future Times
The overloaded ๐๐๐๐๐๐๐๐ฒ๐๐๐๐๐ณ๐๐๐() functions that accept a ๐๐๐๐๐ต๐๐๐ ๐๐๐ argument can additionally store time values for up to 500 bars into the future.
//@version=6
indicator("Example `collectChartData(barsForward)`")
import n00btraders/ChartData/1
chartData = ChartData.collectChartData(barsForward = 500)
var rectangle = box.new(na, na, na, na, xloc = xloc.bar_time, force_overlay = true)
if barstate.islast
int futureTime = chartData.timeAtBarIndex(bar_index + 100)
int lastBarTime = time
box.set_lefttop(rectangle, lastBarTime, open)
box.set_rightbottom(rectangle, futureTime, close)
box.set_text(rectangle, "Extending box 100 bars to the right. Time: " + str.format_time(futureTime))
โข Collect Custom Data
The overloaded ๐๐๐๐๐๐๐๐ฒ๐๐๐๐๐ณ๐๐๐() functions that accept a ๐๐๐๐๐๐๐๐๐ argument can additionally store custom user-specified values for every bar on the chart.
//@version=6
indicator("Example `collectChartData(variables)`")
import n00btraders/ChartData/1
var map variables = map.new()
variables.put("open", open)
variables.put("close", close)
variables.put("open-close midpoint", (open + close) / 2)
variables.put("boolean", open > close ? 1 : 0)
chartData = ChartData.collectChartData(variables = variables)
var fgColor = chart.fg_color
var table1 = table.new(position.top_right, 2, 9, color(na), fgColor, 1, fgColor, 1, true)
var table2 = table.new(position.bottom_right, 2, 9, color(na), fgColor, 1, fgColor, 1, true)
if barstate.isfirst
table.cell(table1, 0, 0, "ChartData.value()", text_color = fgColor)
table.cell(table2, 0, 0, "open ", text_color = fgColor)
table.merge_cells(table1, 0, 0, 1, 0)
table.merge_cells(table2, 0, 0, 1, 0)
for i = 1 to 8
table.cell(table1, 0, i, text_color = fgColor, text_halign = text.align_left, text_font_family = font.family_monospace)
table.cell(table2, 0, i, text_color = fgColor, text_halign = text.align_left, text_font_family = font.family_monospace)
table.cell(table1, 1, i, text_color = fgColor)
table.cell(table2, 1, i, text_color = fgColor)
if barstate.islast
for i = 1 to 8
float open1 = chartData.value("open", 5000 * i)
float open2 = i < 3 ? open : -1
table.cell_set_text(table1, 0, i, "chartData.value(\"open\", " + str.tostring(5000 * i) + "): ")
table.cell_set_text(table2, 0, i, "open : ")
table.cell_set_text(table1, 1, i, str.tostring(open1))
table.cell_set_text(table2, 1, i, open2 >= 0 ? str.tostring(open2) : "Error")
โข xloc.bar_index โ xloc.bar_time
The ๐๐๐๐ value (or ๐๐๐๐_๐๐๐๐๐ value) can be retrieved for any bar index that is stored in memory by the ๐ฒ๐๐๐๐๐ณ๐๐๐ object.
//@version=6
indicator("Example `timeAtBarIndex()`")
import n00btraders/ChartData/1
chartData = ChartData.collectChartData()
if barstate.islast
int start = bar_index - 15000
int end = bar_index - 100
// line.new(start, close, end, close) // !ERROR - `start` value is too far from current bar index
start := chartData.timeAtBarIndex(start)
end := chartData.timeAtBarIndex(end)
line.new(start, close, end, close, xloc.bar_time, width = 10)
โข xloc.bar_time โ xloc.bar_index
Use ๐๐๐๐๐๐๐๐๐๐๐๐ฑ๐๐๐ธ๐๐๐๐ก() to find the bar that a timestamp belongs to.
If the timestamp falls in between the close of one bar and the open of the next bar,
the ๐๐๐๐ parameter can be used to determine which bar to choose:
๐๐๐๐.๐ป๐ด๐ต๐ - prefer to choose the leftmost bar (typically used for closing times)
๐๐๐๐.๐๐ธ๐ถ๐ท๐ - prefer to choose the rightmost bar (typically used for opening times)
๐๐๐๐.๐ณ๐ด๐ต๐ฐ๐๐ป๐ (or ๐๐) - copies the same behavior as xloc.bar_time uses for drawing objects
//@version=6
indicator("Example `timestampToBarIndex()`")
import n00btraders/ChartData/1
startTimeInput = input.time(timestamp("01 Aug 2025 08:30 -0500"), "Session Start Time")
endTimeInput = input.time(timestamp("01 Aug 2025 15:15 -0500"), "Session End Time")
chartData = ChartData.collectChartData()
if barstate.islastconfirmedhistory
int startBarIndex = chartData.timestampToBarIndex(startTimeInput, ChartData.Snap.RIGHT)
int endBarIndex = chartData.timestampToBarIndex(endTimeInput, ChartData.Snap.LEFT)
line1 = line.new(startBarIndex, 0, startBarIndex, 1, extend = extend.both, color = color.new(color.green, 60), force_overlay = true)
line2 = line.new(endBarIndex, 0, endBarIndex, 1, extend = extend.both, color = color.new(color.green, 60), force_overlay = true)
linefill.new(line1, line2, color.new(color.green, 90))
// using Snap.DEFAULT to show that it is equivalent to drawing lines using `xloc.bar_time` (i.e., it aligns to the same bars)
startBarIndex := chartData.timestampToBarIndex(startTimeInput)
endBarIndex := chartData.timestampToBarIndex(endTimeInput)
line.new(startBarIndex, 0, startBarIndex, 1, extend = extend.both, color = color.yellow, width = 3)
line.new(endBarIndex, 0, endBarIndex, 1, extend = extend.both, color = color.yellow, width = 3)
line.new(startTimeInput, 0, startTimeInput, 1, xloc.bar_time, extend.both, color.new(color.blue, 85), width = 11)
line.new(endTimeInput, 0, endTimeInput, 1, xloc.bar_time, extend.both, color.new(color.blue, 85), width = 11)
โข Get Price of Line at Timestamp
The pine script built-in function line.get_price() requires working with bar index values. To get the price of a line in terms of a timestamp, convert the timestamp into a bar index or offset.
//@version=6
indicator("Example `line.get_price()` at timestamp")
import n00btraders/ChartData/1
lineStartInput = input.time(timestamp("01 Aug 2025 08:30 -0500"), "Line Start")
chartData = ChartData.collectChartData()
var diagonal = line.new(na, na, na, na, force_overlay = true)
if time <= lineStartInput
line.set_xy1(diagonal, bar_index, open)
if barstate.islastconfirmedhistory
line.set_xy2(diagonal, bar_index, close)
if barstate.islast
int timeOneWeekAgo = timenow - (7 * timeframe.in_seconds("1D") * 1000)
// Note: could also use `timetampToBarIndex(timeOneWeekAgo, Snap.DEFAULT)` and pass the value directly to `line.get_price()`
int barsOneWeekAgo = chartData.getNumberOfBarsBack(timeOneWeekAgo)
float price = line.get_price(diagonal, bar_index - barsOneWeekAgo)
string formatString = "Time 1 week ago: {0,number,#} - Equivalent to {1} bars ago ๐๐๐๐.๐๐๐_๐๐๐๐๐(): {2,number,#.##}"
string labelText = str.format(formatString, timeOneWeekAgo, barsOneWeekAgo, price)
label.new(timeOneWeekAgo, price, labelText, xloc.bar_time, style = label.style_label_lower_right, size = 16, textalign = text.align_left, force_overlay = true)
โโ RUNTIME ERROR MESSAGES
This library's functions will generate a custom runtime error message in the following cases:
๐๐๐๐๐๐๐๐ฒ๐๐๐๐๐ณ๐๐๐() is not called consecutively, or is called more than once on a single bar
Invalid ๐๐๐๐๐ต๐๐๐ ๐๐๐ argument in the ๐๐๐๐๐๐๐๐ฒ๐๐๐๐๐ณ๐๐๐() function
Invalid ๐๐๐๐๐๐๐๐๐ argument in the ๐๐๐๐๐๐๐๐ฒ๐๐๐๐๐ณ๐๐๐() function
Invalid ๐๐๐๐๐๐ argument in any of the functions that accept a number of bars back
Note: there is no runtime error generated for an invalid ๐๐๐๐๐๐๐๐๐ or ๐๐๐๐ธ๐๐๐๐ก argument in any of the functions. Instead, the functions will assign ๐๐ to the returned values.
Any other runtime errors are due to incorrect usage of the library.
โโ NOTES
โข Function Descriptions
The library source code uses Markdown for the exported functions. Hover over a function/method call in the Pine Editor to display formatted, detailed information about the function/method.
//@version=6
indicator("Demo Function Tooltip")
import n00btraders/ChartData/1
chartData = ChartData.collectChartData()
int barIndex = chartData.timestampToBarIndex(timenow)
log.info(str.tostring(barIndex))
โข Historical vs. Realtime Behavior
Under the hood, the data collector for this library is declared as `var`. Because of this, the ๐ฒ๐๐๐๐๐ณ๐๐๐ object will always reflect the latest available data on realtime updates. Any data that is recorded for historical bars will remain unchanged throughout the execution of a script.
//@version=6
indicator("Demo Realtime Behavior")
import n00btraders/ChartData/1
var map variables = map.new()
variables.put("open", open)
variables.put("close", close)
chartData = ChartData.collectChartData(variables)
if barstate.isrealtime
varip float initialOpen = open
varip float initialClose = close
varip int updateCount = 0
updateCount += 1
float latestOpen = open
float latestClose = close
float recordedOpen = chartData.valueAtBarIndex("open", bar_index)
float recordedClose = chartData.valueAtBarIndex("close", bar_index)
string formatString = "# of updates: {0} ๐๐๐๐ at update #1: {1,number,#.##} ๐๐๐๐๐ at update #1: {2,number,#.##} "
+ "๐๐๐๐ at update #{0}: {3,number,#.##} ๐๐๐๐๐ at update #{0}: {4,number,#.##} "
+ "๐๐๐๐ stored in memory: {5,number,#.##} ๐๐๐๐๐ stored in memory: {6,number,#.##}"
string labelText = str.format(formatString, updateCount, initialOpen, initialClose, latestOpen, latestClose, recordedOpen, recordedClose)
label.new(bar_index, close, labelText, style = label.style_label_left, force_overlay = true)
โข Collecting Chart Data for Other Contexts
If your use case requires collecting chart data from another context, avoid directly retrieving the ๐ฒ๐๐๐๐๐ณ๐๐๐ object as this may exceed memory limits .
//@version=6
indicator("Demo Return Calculated Results")
import n00btraders/ChartData/1
timeInput = input.time(timestamp("01 Sep 2025 08:30 -0500"), "Time")
var int oneMinuteBarsAgo = na
// !ERROR - Memory Limits Exceeded
// chartDataArray = request.security_lower_tf(syminfo.tickerid, "1", ChartData.collectChartData())
// oneMinuteBarsAgo := chartDataArray.last().getNumberOfBarsBack(timeInput)
// function that returns calculated results (a single integer value instead of an entire `ChartData` object)
getNumberOfBarsBack() =>
chartData = ChartData.collectChartData()
chartData.getNumberOfBarsBack(timeInput)
calculatedResultsArray = request.security_lower_tf(syminfo.tickerid, "1", getNumberOfBarsBack())
oneMinuteBarsAgo := calculatedResultsArray.size() > 0 ? calculatedResultsArray.last() : na
if barstate.islast
string labelText = str.format("The selected timestamp occurs 1-minute bars ago", oneMinuteBarsAgo)
label.new(bar_index, hl2, labelText, style = label.style_label_left, size = 16, force_overlay = true)
โข Memory Usage
The library's convenience and ease of use comes at the cost of increased usage of computational resources. For simple scripts, using this library will likely not cause any issues with exceeding memory limits. But for large and complex scripts, you can reduce memory issues by specifying a lower ๐๐๐๐_๐๐๐๐_๐๐๐๐๐ amount in the indicator() or strategy() declaration statement.
//@version=6
// !ERROR - Memory Limits Exceeded using the default number of bars available (~20,000 bars for Premium plans)
//indicator("Demo `calc_bars_count` parameter")
// Reduce number of bars using `calc_bars_count` parameter
indicator("Demo `calc_bars_count` parameter", calc_bars_count = 15000)
import n00btraders/ChartData/1
map variables = map.new()
variables.put("open", open)
variables.put("close", close)
variables.put("weekofyear", weekofyear)
variables.put("dayofmonth", dayofmonth)
variables.put("hour", hour)
variables.put("minute", minute)
variables.put("second", second)
// simulate large memory usage
chartData0 = ChartData.collectChartData(variables)
chartData1 = ChartData.collectChartData(variables)
chartData2 = ChartData.collectChartData(variables)
chartData3 = ChartData.collectChartData(variables)
chartData4 = ChartData.collectChartData(variables)
chartData5 = ChartData.collectChartData(variables)
chartData6 = ChartData.collectChartData(variables)
chartData7 = ChartData.collectChartData(variables)
chartData8 = ChartData.collectChartData(variables)
chartData9 = ChartData.collectChartData(variables)
log.info(str.tostring(chartData0.time(0)))
log.info(str.tostring(chartData1.time(0)))
log.info(str.tostring(chartData2.time(0)))
log.info(str.tostring(chartData3.time(0)))
log.info(str.tostring(chartData4.time(0)))
log.info(str.tostring(chartData5.time(0)))
log.info(str.tostring(chartData6.time(0)))
log.info(str.tostring(chartData7.time(0)))
log.info(str.tostring(chartData8.time(0)))
log.info(str.tostring(chartData9.time(0)))
if barstate.islast
result = table.new(position.middle_right, 1, 1, force_overlay = true)
table.cell(result, 0, 0, "Script Execution Successful โ
", text_size = 40)
โโ EXPORTED ENUMS
Snap
โโBehavior for determining the bar that a timestamp belongs to.
โโFields:
โโโโ LEFT : Snap to the leftmost bar.
โโโโ RIGHT : Snap to the rightmost bar.
โโโโ DEFAULT : Default `xloc.bar_time` behavior.
Note: this enum is used for the ๐๐๐๐ parameter of ๐๐๐๐๐๐๐๐๐๐๐๐ฑ๐๐๐ธ๐๐๐๐ก().
โโ EXPORTED TYPES
Note: users of the library do not need to worry about directly accessing the fields of these types; all computations are done through method calls on an object of the ๐ฒ๐๐๐๐๐ณ๐๐๐ type.
Variable
โโRepresents a user-specified variable that can be tracked on every chart bar.
โโFields:
โโโโ name (series string) : Unique identifier for the variable.
โโโโ values (array) : The array of stored values (one value per chart bar).
ChartData
โโRepresents data for all bars on a chart.
โโFields:
โโโโ bars (series int) : Current number of bars on the chart.
โโโโ timeValues (array) : The `time` values of all chart (and future) bars.
โโโโ timeCloseValues (array) : The `time_close` values of all chart (and future) bars.
โโโโ variables (array) : Additional custom values to track on all chart bars.
โโ EXPORTED FUNCTIONS
collectChartData()
โโCollects and tracks the `time` and `time_close` value of every bar on the chart.
โโReturns: `ChartData` object to convert between `xloc.bar_index` and `xloc.bar_time`.
collectChartData(barsForward)
โโCollects and tracks the `time` and `time_close` value of every bar on the chart as well as a specified number of future bars.
โโParameters:
โโโโ barsForward (simple int) : Number of future bars to collect data for.
โโReturns: `ChartData` object to convert between `xloc.bar_index` and `xloc.bar_time`.
collectChartData(variables)
โโCollects and tracks the `time` and `time_close` value of every bar on the chart. Additionally, tracks a custom set of variables for every chart bar.
โโParameters:
โโโโ variables (simple map) : Custom values to collect on every chart bar.
โโReturns: `ChartData` object to convert between `xloc.bar_index` and `xloc.bar_time`.
collectChartData(barsForward, variables)
โโCollects and tracks the `time` and `time_close` value of every bar on the chart as well as a specified number of future bars. Additionally, tracks a custom set of variables for every chart bar.
โโParameters:
โโโโ barsForward (simple int) : Number of future bars to collect data for.
โโโโ variables (simple map) : Custom values to collect on every chart bar.
โโReturns: `ChartData` object to convert between `xloc.bar_index` and `xloc.bar_time`.
โโ EXPORTED METHODS
method timestampToBarIndex(chartData, timestamp, snap)
โโConverts a UNIX timestamp to a bar index.
โโNamespace types: ChartData
โโParameters:
โโโโ chartData (series ChartData) : The `ChartData` object.
โโโโ timestamp (series int) : A UNIX time.
โโโโ snap (series Snap) : A `Snap` enum value.
โโReturns: A bar index, or `na` if unable to find the appropriate bar index.
method getNumberOfBarsBack(chartData, timestamp)
โโConverts a UNIX timestamp to a history-referencing length (i.e., number of bars back).
โโNamespace types: ChartData
โโParameters:
โโโโ chartData (series ChartData) : The `ChartData` object.
โโโโ timestamp (series int) : A UNIX time.
โโReturns: A bar offset, or `na` if unable to find a valid number of bars back.
method timeAtBarIndex(chartData, barIndex)
โโRetrieves the `time` value for the specified bar index.
โโNamespace types: ChartData
โโParameters:
โโโโ chartData (series ChartData) : The `ChartData` object.
โโโโ barIndex (int) : The bar index.
โโReturns: The `time` value, or `na` if there is no `time` stored for the bar index.
method time(chartData, length)
โโRetrieves the `time` value of the bar that is `length` bars back relative to the latest bar.
โโNamespace types: ChartData
โโParameters:
โโโโ chartData (series ChartData) : The `ChartData` object.
โโโโ length (series int) : Number of bars back.
โโReturns: The `time` value `length` bars ago, or `na` if there is no `time` stored for that bar.
method timeCloseAtBarIndex(chartData, barIndex)
โโRetrieves the `time_close` value for the specified bar index.
โโNamespace types: ChartData
โโParameters:
โโโโ chartData (series ChartData) : The `ChartData` object.
โโโโ barIndex (series int) : The bar index.
โโReturns: The `time_close` value, or `na` if there is no `time_close` stored for the bar index.
method timeClose(chartData, length)
โโRetrieves the `time_close` value of the bar that is `length` bars back from the latest bar.
โโNamespace types: ChartData
โโParameters:
โโโโ chartData (series ChartData) : The `ChartData` object.
โโโโ length (series int) : Number of bars back.
โโReturns: The `time_close` value `length` bars ago, or `na` if there is none stored.
method valueAtBarIndex(chartData, name, barIndex)
โโRetrieves the value of a custom variable for the specified bar index.
โโNamespace types: ChartData
โโParameters:
โโโโ chartData (series ChartData) : The `ChartData` object.
โโโโ name (series string) : The variable name.
โโโโ barIndex (series int) : The bar index.
โโReturns: The value of the variable, or `na` if that variable is not stored for the bar index.
method value(chartData, name, length)
โโRetrieves a variable value of the bar that is `length` bars back relative to the latest bar.
โโNamespace types: ChartData
โโParameters:
โโโโ chartData (series ChartData) : The `ChartData` object.
โโโโ name (series string) : The variable name.
โโโโ length (series int) : Number of bars back.
โโReturns: The value `length` bars ago, or `na` if that variable is not stored for the bar index.
method getAllVariablesAtBarIndex(chartData, barIndex)
โโRetrieves all custom variables for the specified bar index.
โโNamespace types: ChartData
โโParameters:
โโโโ chartData (series ChartData) : The `ChartData` object.
โโโโ barIndex (series int) : The bar index.
โโReturns: Map of all custom variables that are stored for the specified bar index.
method getEarliestStoredData(chartData)
โโGets all values from the earliest bar data that is currently stored in memory.
โโNamespace types: ChartData
โโParameters:
โโโโ chartData (series ChartData) : The `ChartData` object.
โโReturns: A tuple:
method getLatestStoredData(chartData, futureData)
โโGets all values from the latest bar data that is currently stored in memory.
โโNamespace types: ChartData
โโParameters:
โโโโ chartData (series ChartData) : The `ChartData` object.
โโโโ futureData (series bool) : Whether to include the future data that is stored in memory.
โโReturns: A tuple:
Bar_index
Vertical line by bar_indexThis indicator helps in letting the trader focus on a particular date/candle across many symbols.
Tradingview currently has a bug in Bar Replay - when we switch symbols, the bar replay resets.
Hence if you are backtesting & running through a bunch of symbols, it is nearly impossible to focus on a particular historical candle across all symbols.
This indicator plots a vertical line on a given bar_index which remains as it is while you are switching symbols.
Feel free to copy, modify and use as you like!

