Tops & Bottoms - Day of Week Report.pine
Copy
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © sbtnc
// Created: 2023-11-29
// Last modified: 2023-12-09
// Version 1.0
//@version=5
indicator("Tops & Bottoms - Day of Week", format = format.percent, max_labels_count = 21)
//--------------------------------------------------------------------
//#region Constants
//--------------------------------------------------------------------
int COLUMN_WIDTH = 4
int COLUMN_GAP = 4
color COLUMN_BORDERCOLOR = color.new(chart.fg_color, 80)
color COLUMN_TOP_MAX_COLOR = color.green
color COLUMN_TOP_MIN_COLOR = #0c3299
color COLUMN_BOTTOM_MAX_COLOR = color.red
color COLUMN_BOTTOM_MIN_COLOR = #311b92
color CATEGORY_LABEL_BGCOLOR = color.new(chart.bg_color, 50)
color CATEGORY_LABEL_TEXTCOLOR = chart.fg_color
array<string> TIMEFRAMES = array.from("D", "1440")
array<int> DAYSOFWEEK = array.from(dayofweek.sunday, dayofweek.monday, dayofweek.tuesday, dayofweek.wednesday, dayofweek.thursday, dayofweek.friday, dayofweek.saturday)
//#endregion
//--------------------------------------------------------------------
//#region Inputs
//--------------------------------------------------------------------
intervalTooltip = "By default, the indicator identifies the top and bottom over the symbol's trading week. " +
"You can specify your custom interval."
noticeTitle = "Navigate to the 1-day timeframe (or 24-hour for including the extended/electronic trading hours) " +
"for the indicator to analyze the bars and collect tops and bottoms."
customIntervalInput = input.bool (false, "Custom Interval", inline = "Interval")
intervalStartInput = input.string ("Monday", "", ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], inline = "Interval")
intervalEndInput = input.string ("Sunday", " — ", ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], tooltip = intervalTooltip, inline = "Interval")
intervalTypeInput = input.string ("All", "Interval", ["All", "Up", "Down"], group = "Filters")
startDateInput = input.time (timestamp("01 Jan 1975"), "From", group = "Filters")
endDateInput = input.time (timestamp("31 Dec 2050"), "To", group = "Filters")
noticeInput = input.bool (true, noticeTitle, group = "How To Use?", confirm = true, display = display.none)
//#endregion
//--------------------------------------------------------------------
//#region Types
//--------------------------------------------------------------------
type Marker
float price = na
int time = na
//#endregion
//--------------------------------------------------------------------
//#region Variables declarations
//--------------------------------------------------------------------
var topsByDayOfWeekMap = map.new<int, int>()
var bottomsByDayOfWeekMap = map.new<int, int>()
var openTimesByDayOfWeekMap = map.new<int, int>()
var closeTimesByDayOfWeekMap = map.new<int, int>()
var top = Marker.new()
var bottom = Marker.new()
//#endregion
//--------------------------------------------------------------------
//#region Functions & methods
//--------------------------------------------------------------------
// @function Get the day of the week index from a given UNIX time.
// @returns (int) Day of the week
getDayOfWeek(int barTime) =>
dayofweek(barTime, "UTC+0")
// @function Get the day of the week short string from a given index.
// @returns (string) Day of the week
getDayOfWeekShortString(int dayOfWeek) =>
switch dayOfWeek
dayofweek.sunday => "Sun"
dayofweek.monday => "Mon"
dayofweek.tuesday => "Tue"
dayofweek.wednesday => "Wed"
dayofweek.thursday => "Thu"
dayofweek.friday => "Fri"
dayofweek.saturday => "Sat"
// @function Get the day of the week long string from a given index.
// @returns (string) Day of the week
getDayOfWeekLongString(int dayOfWeek) =>
switch dayOfWeek
dayofweek.sunday => "Sunday"
dayofweek.monday => "Monday"
dayofweek.tuesday => "Tuesday"
dayofweek.wednesday => "Wednesday"
dayofweek.thursday => "Thursday"
dayofweek.friday => "Friday"
dayofweek.saturday => "Saturday"
// @function Check if the market has an overnight session based on its opening and closing times.
// @returns bool
hasOvernightSession(int openTime, int closeTime) =>
dayofweek(openTime) != dayofweek(closeTime)
// @function Get the legend's tooltip text.
// @returns string
getSessionTooltip(int openTime, int closeTime) =>
string _dayOfWeekAtOpen = getDayOfWeekLongString(dayofweek(openTime))
string _dayOfWeekAtClose = getDayOfWeekLongString(dayofweek(closeTime))
string _timeAtOpen = str.format_time(openTime, "HH:mm", syminfo.timezone)
string _timeAtClose = str.format_time(closeTime, "HH:mm", syminfo.timezone)
switch hasOvernightSession(openTime, closeTime)
true => str.format("{0} {1} to {2} {3} (Exchange time)", _dayOfWeekAtOpen, _timeAtOpen, _timeAtClose, _dayOfWeekAtClose)
false => str.format("{0} {1} to {2} (Exchange time)", _dayOfWeekAtOpen, _timeAtOpen, _timeAtClose)
// @function Get the color based on a gradient between the minimum and maximum values.
// @returns color
getGradientColor(float value, float minValue, float maxValue, color minColor, color maxColor) =>
// When `minValue` and `maxValue` are identical (on the first collected values), return the mid gradient color instead of no color.
minValue != maxValue ? color.from_gradient(value, minValue, maxValue, minColor, maxColor) : color.from_gradient(0.5, 0, 1, minColor, maxColor)
// @function Draw the columns for a given day of the week.
// @returns void
drawDayOfWeekColumns(int key) =>
var _upperColumnBox = box.new(na, na, na, na, COLUMN_BORDERCOLOR)
var _lowerColumnBox = box.new(na, na, na, na, COLUMN_BORDERCOLOR)
var _upperDataLabel = label.new(na, na, color = color(na), style = label.style_label_down)
var _lowerDataLabel = label.new(na, na, color = color(na), style = label.style_label_up)
var _categoryLabel = label.new(na, na, color = CATEGORY_LABEL_BGCOLOR, style = label.style_label_center, textcolor = CATEGORY_LABEL_TEXTCOLOR)
var _gridMultiplier = COLUMN_WIDTH + COLUMN_GAP
if barstate.islast
_topsArray = topsByDayOfWeekMap.values()
_bottomsArray = bottomsByDayOfWeekMap.values()
// Sum the number of data sampled.
_dataSum = _topsArray.sum()
// Calculate the shares of occurrence of tops and bottoms.
_top = topsByDayOfWeekMap.get(key)
_bottom = bottomsByDayOfWeekMap.get(key)
_topY = nz(_top / _dataSum * 100)
_bottomY = nz(_bottom / _dataSum * 100)
// Get the coordinates for plotting the columns chart (on the left or right side of the last bar, given enough space).
_columnsCount = 7
_hasSpace = bar_index - _columnsCount * _gridMultiplier > 0
_x1 = _hasSpace ? bar_index + (key - _columnsCount - 1) * _gridMultiplier : bar_index + key * _gridMultiplier
_x2 = _x1 + COLUMN_WIDTH
_center = _x1 + COLUMN_WIDTH / 2
// Get the coloring of the columns and data labels.
_topsMin = _topsArray.min() / _dataSum * 100
_topsMax = _topsArray.max() / _dataSum * 100
_bottomsMin = _bottomsArray.min() / _dataSum * 100
_bottomsMax = _bottomsArray.max() / _dataSum * 100
_topColor = getGradientColor(_topY, _topsMin, _topsMax, COLUMN_TOP_MIN_COLOR, COLUMN_TOP_MAX_COLOR)
_bottomColor = getGradientColor(_bottomY, _bottomsMin, _bottomsMax, COLUMN_BOTTOM_MIN_COLOR, COLUMN_BOTTOM_MAX_COLOR)
// Plot the upper and lower columns for tops and bottoms.
_upperColumnBox.set_lefttop (_x1, _topY)
_upperColumnBox.set_rightbottom (_x2, 0)
_upperColumnBox.set_bgcolor (_topColor)
_lowerColumnBox.set_lefttop (_x1, 0)
_lowerColumnBox.set_rightbottom (_x2, -_bottomY)
_lowerColumnBox.set_bgcolor (_bottomColor)
// Plot the X-axis category label.
_openTime = openTimesByDayOfWeekMap.get(key)
_closeTime = closeTimesByDayOfWeekMap.get(key)
_isCategoryDefined = not (na(_openTime) or na(_closeTime))
_categoryLabel.set_xy (_center, 0)
_categoryLabel.set_text (getDayOfWeekShortString(key))
_categoryLabel.set_tooltip (_isCategoryDefined ? getSessionTooltip(_openTime, _closeTime) : "Not Collected")
// Plot the data labels for tops and bottoms.
if not na(_top)
_upperDataLabel.set_xy (_center, _topY)
_upperDataLabel.set_text (str.tostring(math.round(_topY)))
_upperDataLabel.set_textcolor (_topColor)
_upperDataLabel.set_tooltip (str.format("{0} based on {1} sampled weeks", str.tostring(_topY, format.percent), _dataSum))
if not na(_bottom)
_lowerDataLabel.set_xy (_center, -_bottomY)
_lowerDataLabel.set_text (str.tostring(math.round(_bottomY)))
_lowerDataLabel.set_textcolor (_bottomColor)
_lowerDataLabel.set_tooltip (str.format("{0} based on {1} sampled weeks", str.tostring(_bottomY, format.percent), _dataSum))
// @function Check if the week/custom interval matches the user-selected interval type.
// @returns bool
isValidIntervalType(start, end, isInRange) =>
var float _open = na
bool _isValid = na
// The first bar of the user-selected time range can occur after the interval start.
if ta.change(isInRange) and isInRange
_open := open
if start
_open := open
if end
_isValid := switch intervalTypeInput
"All" => not na(_open[1])
"Up" =>
close[1] > _open[1]
"Down" =>
close[1] < _open[1]
_isValid
// @function Check if the current bar time is in the user-selected time range.
// @returns bool
isInTimeRange() =>
time >= startDateInput and time <= endDateInput
// @function Get the day of the week index from a given string.
// @returns (int) Day of the week
getDayOfWeek(string dayOfWeek) =>
switch dayOfWeek
"Sunday" => dayofweek.sunday
"Monday" => dayofweek.monday
"Tuesday" => dayofweek.tuesday
"Wednesday" => dayofweek.wednesday
"Thursday" => dayofweek.thursday
"Friday" => dayofweek.friday
"Saturday" => dayofweek.saturday
// @function Get the end day of the week index from the user-selected interval (shifted by one to be inclusive).
// @returns (int) Day of the week
getEndDayOfWeek() =>
switch intervalEndInput
"Sunday" => dayofweek.monday
"Monday" => dayofweek.tuesday
"Tuesday" => dayofweek.wednesday
"Wednesday" => dayofweek.thursday
"Thursday" => dayofweek.friday
"Friday" => dayofweek.saturday
"Saturday" => dayofweek.sunday
// @function Construct a suite of days of the week for a given index.
// @returns array<int>
method getSuite(array<int> this, int dayOfWeek) =>
_src = this.copy()
_index = _src.indexof(dayOfWeek)
_part1 = _src.slice(_index, 7)
_part2 = _src.slice(0, _index)
_part1.concat(_part2)
// @function Detect the changes in the week/custom interval.
// @returns ([bool, bool, bool]) Start, end, time is in interval
intervalChange() =>
if customIntervalInput
_dayOfWeek = dayofweek(time_tradingday, "UTC+0")
// Construct the lists of following days of the week.
_suiteFromStart = DAYSOFWEEK.getSuite(getDayOfWeek(intervalStartInput))
_suiteFromEnd = DAYSOFWEEK.getSuite(getEndDayOfWeek())
// Determine if the current day of the week is in the interval.
_dayOfWeekList = _suiteFromStart.copy().slice(0, _suiteFromStart.indexof(getDayOfWeek(intervalEndInput)) +1)
_isInInterval = _dayOfWeekList.includes(_dayOfWeek)
// Calculate the next start and end times of the interval.
_barsToStart = 7 - _suiteFromStart.indexof(_dayOfWeek)
_barsToEnd = 7 - _suiteFromEnd.indexof(_dayOfWeek)
_startTime = timestamp(year, month, dayofmonth + _barsToStart, hour, minute)
_endTime = timestamp(year, month, dayofmonth + _barsToEnd, hour, minute)
// Determine if the day is the first day of the interval / after the interval.
// Which are not necessarily on the interval's start and end day of week (e.g., on EURUSD, Monday is the first trading day in the interval [Saturday - Wednesday]).
_start = ta.change(_startTime) > 0 and _isInInterval
_end = ta.change(_endTime) > 0 and _isInInterval[1]
[_start, _end, _isInInterval]
else
// Start and end on the week change.
_weekChange = timeframe.change("W")
[_weekChange, _weekChange, true]
//#endregion
//--------------------------------------------------------------------
//#region Logic
//--------------------------------------------------------------------
if TIMEFRAMES.indexof(timeframe.period) == -1
runtime.error("The report can not compute data on the chart's timeframe. Please navigate to the 1-day or 24-hour timeframe.")
//@variable Is true when the current bar is inside the time range filter.
isInRange = isInTimeRange()
// Interval variables from the week/custom interval.
[intervalStart, intervalEnd, isInInterval] = intervalChange()
// @variable Is true when the week/custom interval closes per the interval type filter.
isValidType = isValidIntervalType(intervalStart, intervalEnd, isInRange)
// Track the top and bottom of the week/custom interval.
if isInRange
// Reset the top and bottom on the first bar of a new week/custom interval.
if intervalStart
top.price := na
top.time := na
bottom.price := na
bottom.time := na
// Track the top and bottom.
if na(top.price) or high > top.price
top.price := high
top.time := time_tradingday
if na(bottom.price) or low < bottom.price
bottom.price := low
bottom.time := time_tradingday
// Collect the top, bottom, and time data.
if isInRange and barstate.isconfirmed
// @variable Is true on the first bar of the time range.
bool _isFirstBar = na(top.time[1]) or na(bottom.time[1])
// Collect the top and bottom on a week/custom interval change if it conforms to the user-selected type.
if isValidType and not _isFirstBar
int _topWeekday = getDayOfWeek(top.time[1])
int _bottomWeekday = getDayOfWeek(bottom.time[1])
int _topCount = nz(topsByDayOfWeekMap.get(_topWeekday)) +1
int _bottomCount = nz(bottomsByDayOfWeekMap.get(_bottomWeekday)) +1
topsByDayOfWeekMap.put(_topWeekday, _topCount)
bottomsByDayOfWeekMap.put(_bottomWeekday, _bottomCount)
log.info(
"\n▲ Top on {0} ({1}/{4})\n▼ Bottom on {2} ({3}/{4})",
getDayOfWeekLongString(_topWeekday), _topCount, getDayOfWeekLongString(_bottomWeekday), _bottomCount, topsByDayOfWeekMap.values().sum()
)
// Collect the opening and closing times.
// The times are be displayed in the tooltips of category labels.
int _weekday = getDayOfWeek(time_tradingday)
if isInInterval
openTimesByDayOfWeekMap.put(_weekday, time)
closeTimesByDayOfWeekMap.put(_weekday, time_close)
//#endregion
//--------------------------------------------------------------------
//#region Visuals
//--------------------------------------------------------------------
drawDayOfWeekColumns(dayofweek.sunday)
drawDayOfWeekColumns(dayofweek.monday)
drawDayOfWeekColumns(dayofweek.tuesday)
drawDayOfWeekColumns(dayofweek.wednesday)
drawDayOfWeekColumns(dayofweek.thursday)
drawDayOfWeekColumns(dayofweek.friday)
drawDayOfWeekColumns(dayofweek.saturday)
//#endregion
Show all 420 lines