SignalBuilder
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © Electrified

// @version=5

// @description Utility for building a collection of signal values and displaying them.
library("SignalBuilder", true)
import Electrified/TableBuilder/13 as TB
import Electrified/Color/8

////////////////////////////////////////////////////////////////
// @type An object for holding signal details.
// @field value Represents the signal value.
// @field description Describes the signal.
// @field group The group this signal belongs to.
export type Signal
	float value
	string description
	string group
////////////////////////////////////////////////////////////////	

////////////////////////////////////////////////////////////////
// @type An object for holding a group of signal details.
// @field group The signal group value/name.
// @field signals The grouped signals.
// @field color A color related to this group.
// @field symbol An optional symbol related to this group.
export type Group
	string group
	Signal[] signals
    color color
    string symbol
////////////////////////////////////////////////////////////////	

////////////////////////////////////////////////////////////////
// @function Creates an empty array of Signal objects.
// @returns The new array.
export newArray() =>
	array.new<Signal>()
////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////
// @function Creates a new Group object with a given name and an empty array of Signal objects.
// @param name The group value/name.
// @param color The color for the group.
// @param symbol The symbol for the group.
// @returns The new Group object.
export newGroup(string name, color color, string symbol) =>
	Group.new(name, newArray(), color, symbol)
////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////
// @function Creates a new Group object with a given name and an empty array of Signal objects.
// @param name The group value/name.
// @param color The color for the group.
// @returns The new Group object.
export newGroup(string name, color color) =>
	Group.new(name, newArray(), color, na)
////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////
// @function Creates a new Group object with a given name and an empty array of Signal objects.
// @param name The group value/name.
// @param symbol The symbol for the group.
// @returns The new Group object.
export newGroup(string name, string symbol) =>
	Group.new(name, newArray(), na, symbol)
////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////
// @function Creates a new Group object with a given name and an empty array of Signal objects.
// @param name The group value/name.
// @returns The new Group object.
export newGroup(string name) =>
	Group.new(name, newArray(), na, na)
////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////
// @function Searches through an array of Group objects and returns the index of the first group whose group value matches the provided group value.
// @param signals The array of Group objects to search through.
// @param group The group value to search for.
// @returns The index of the matching group, or -1 if no matching group is found.
export indexOfGroup(
  Group[] signals,
  string group) =>
	size = array.size(signals)
	index = -1
	i = 0
	while i < size and index == -1
		s = array.get(signals, i)
		g = s.group
		if g == group or na(group) and na(g)
			index := i
		i += 1
	index

////////////////////////////////////////////////////////////////
// @function Ensures that a Group with a given group value exists in an array of Group objects, and returns it. If a group with the given value does not exist, a new Group is created and added to the array.
// @param signals The array of Group objects to search through.
// @param group The group value to search for or create.
// @returns The matching or newly-created Group object.
export registerGroup(
  Group[] signals,
  string group) =>
	i = indexOfGroup(signals, group)
	// Ensure the group.
	if i == -1
		g = newGroup(group)
		array.push(signals, g)
		g
	else
		array.get(signals, i)
////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////
// @function Groups an array of Signal objects by their group value.
// @param signals The array of Signal objects to group.
// @returns An array of Group objects, grouping the signals by their group value.
export group(Signal[] signals) =>
	// Initialize the grouping.
	result = array.new<Group>()

	// Loop through all the signals.	
	for signal in signals	   
		group = registerGroup(result, signal.group)
		array.push(group.signals, signal)

	result
////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////
// @function Creates a new Signal object with a given value, description, and group value.
// @param value The value of the signal.
// @param description The description of the signal.
// @param group A value representing what group the signal belongs to.
// @returns The constructed Signal object.
export create(
  float value,
  string description,
  string group = na) =>
	Signal.new(value, description, group)
////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////
// @function Adds a new Signal object to a given Signal array, with a given value, description, and group value.
// @param collection The Signal array to add the new Signal object to.
// @param description The description of the signal.
// @param value The value of the signal.
// @param condition An optional boolean value that will decide if the signal is added or not.
// @param group A value representing what group the signal belongs to.
// @returns true if the condition was true; otherwise false.
export add(
  Signal[] collection,
  string description,
  float value = na,
  bool condition = true,
  string group = na) =>
	if condition
		array.push(collection, Signal.new(value, description, group))
	condition
////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////
// @function Adds a new Signal object to a Group.
// @param group The Group object to add the Signal object to.
// @param description The description of the signal.
// @param value The value of the signal. If not provided, the value will be set to `na`.
// @param condition A boolean condition that determines whether the signal will be added to the group. If not provided, the default value is `true`.
// @returns The constructed Signal object.
export add(
  Group group,
  string description,
  float value = na,
  bool condition = true) =>
	add(group.signals, description, value, condition, group.group)
////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////
// @function Adds a new Signal object to a Group.
// @param posGroup The Group object to add the Signal object to if the value is positive.
// @param negGroup The Group object to add the Signal object to if the value is negative.
// @param description The description of the signal.
// @param value The value of the signal. If not provided, the value will be set to `na`.
// @param condition A boolean condition that determines whether the signal will be added to the group. If not provided, the default value is `true`.
// @returns The constructed Signal object.
export add(
  Group posGroup,
  Group negGroup,
  string description,
  float value = na,
  bool condition = true) =>
	if value > 0
		add(posGroup, description, value, condition)
	else if value < 0
		add(negGroup, description, value, condition)
	else
		false
////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////
// @function Adds a new Signal object to a given array of Group objects, with a given value, description, and group value.
// @param collection The array of Group objects to add the new Signal object to.
// @param description The description of the signal.
// @param value The value of the signal.
// @param condition An optional boolean value that will decide if the signal is added or not.
// @param group A value representing what group the signal belongs to.
// @returns true if the condition was true; otherwise false.
export add(
  Group[] collection,
  string description,
  float value = na,
  bool condition = true,
  string group = na) =>
	if condition
		g = registerGroup(collection, group)
		array.push(g.signals, Signal.new(value, description, group))
	condition
////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////
// @function Adds a new Group object to an array of Group objects.
// @param collection The array of Group objects to add the new Group object to.
// @param group The group value/name of the Group object.
// @param color An optional color for the group.
// @param symbol An optional symbol for the group.
// @returns The constructed Group object.
export addGroup(
  Group[] collection,
  string group,
  color color = na,
  string symbol = na) =>
	g = Group.new(group, array.new<Signal>(), color, symbol)
	array.push(collection, g)
	g
////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////
// @function Adds a new Signal object to a given Signal array under the "Warning" group, with a given value and description.
// @param collection The Signal array to add the new Signal object to.
// @param description The description of the signal.
// @param value The value of the signal.
// @param condition An optional boolean value that will decide if the signal is added or not.
// @returns true if the condition was true; otherwise false.
export addWarning(
  Signal[] collection,
  string description,
  float value,
  bool condition = true) =>
	add(collection, description, value, condition, "Warning")
////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////
// @function Adds a new Signal object to a given array of Group objects under the "Warning" group, with a given value and description.
// @param collection The array of Group objects to add the new Signal object to.
// @param description The description of the signal.
// @param value The value of the signal.
// @param condition An optional boolean value that will decide if the signal is added or not.
// @returns true if the condition was true; otherwise false.
export addWarning(
  Group[] collection,
  string description,
  float value,
  bool condition = true) =>
    add(collection, description, value, condition, "Warning")
////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////
// @function Adds a new Signal object to a given Signal array, with a given value, description, and group value, but only if the value is positive.
// @param collection The Signal array to add the new Signal object to.
// @param description The description of the signal.
// @param value The value of the signal.
// @param group A value representing what group the signal belongs to.
// @returns true if the value was positive and the signal was added; otherwise false.
export addPosOnly(
  Signal[] collection,
  string description,
  float value,
  string group = na) =>
    add(collection, description, value, value > 0, group)
////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////
// @function Adds a new Signal object to a given Signal array, with a given value, description, and group value, but only if the value is negative.
// @param collection The Signal array to add the new Signal object to.
// @param description The description of the signal.
// @param value The value of the signal.
// @param group A value representing what group the signal belongs to.
// @returns true if the value was negative and the signal was added; otherwise false.
export addNegOnly(
  Signal[] collection,
  string description,
  float value,
  string group = na) =>
	add(collection, description, value, value < 0, group)
////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////
// @function Adds a new Signal object to a given Signal array, with a given value, description, and group value, but only if the value is positive.
// @param group The group of signals to add the new Signal object to.
// @param description The description of the signal.
// @param value The value of the signal.
// @param group A value representing what group the signal belongs to.
// @returns true if the value was positive and the signal was added; otherwise false.
export addPosOnly(
  Group group,
  string description,
  float value) =>
    add(group, description, value, value > 0)
////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////
// @function Adds a new Signal object to a given Signal array, with a given value, description, and group value, but only if the value is negative.
// @param group The group of signals to add the new Signal object to.
// @param description The description of the signal.
// @param value The value of the signal.
// @param group A value representing what group the signal belongs to.
// @returns true if the value was negative and the signal was added; otherwise false.
export addNegOnly(
  Group group,
  string description,
  float value) =>
	add(group, description, value, value < 0)
////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////
// @function Adds a new Signal object to a given array of Group objects, with a given value, description, and group value, but only if the value is positive.
// @param collection The array of Group objects to add the new Signal object to.
// @param description The description of the signal.
// @param value The value of the signal.
// @param group A value representing what group the signal belongs to.
// @returns true if the value was positive and the signal was added; otherwise false.
export addPosOnly(
  Group[] collection,
  string description,
  float value,
  string group = na) =>
    add(collection, description, value, value > 0, group)
////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////
// @function Adds a new Signal object to a given array of Group objects, with a given value, description, and group value, but only if the value is negative.
// @param collection The array of Group objects to add the new Signal object to.
// @param description The description of the signal.
// @param value The value of the signal.
// @param group A value representing what group the signal belongs to.
// @returns true if the value was negative and the signal was added; otherwise false.
export addNegOnly(
  Group[] collection,
  string description,
  float value,
  string group = na) =>
    add(collection, description, value, value < 0, group)
////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////
// @function A utility for standardizing the string format of numeric values.
export digits(float value, bool addSign = false) =>
	if na(value)
		""
	else
		sign = value > 0 ? "+" : value < 0 ? "-" : ""
		a = math.abs(value)
		v = a < 1 ? 4 : a < 10 ? 3 : 2
		d = math.pow(10, v)
		s = str.tostring(math.floor(a * d) / d)
		addSign ? (sign + s) : s
////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////
// @function A utility for splitting the integer value from its decimal portion.
export splitDecimal(float value) =>
	if na(value)
		[na, na]
	else
		int sign = value > 0 ? 1 : value < 0 ? -1 : 0
		a = math.abs(value)
		int whole = math.floor(a)
		float remain = a - whole
		[sign * whole, remain]
////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////
// @function Gets the values of the signal objects in the and returns the sum.
// @param signals The array of Signal objects to sum the values.
export sumValues(Signal[] signals) =>
	if na(signals)
		0.0
	else
		s = 0.0
		for signal in signals
			s += nz(signal.value)
		s
////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////
// @function Gets the values of the signal objects in the and returns the sum.
// @param signals The array of Signal objects to sum the values.
export sumValues(Group group) =>
	na(group) ? 0.0 : sumValues(group.signals)
////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////
// @function Gets the sum of the all the signal objects in each group.
// @param signals The array of Signal objects to sum the values.
export sumValues(Group[] groups) =>
	if na(groups)
		0.0
	else
		s = 0.0
		for group in groups
			s += sumValues(group.signals)
		s
////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////
// @function Combines an array of Signal objects into a single Signal object, with the descriptions concatenated and the value being the sum of all values of the Signal objects that have a value. The value of each signal is appended to the end of its description in parenthesis.
// @param signals The array of Signal objects to combine.
// @param group An optional group value to set for the combined Signal object.
// @param prefix An optional prefix to use as a prefix for each line in the resultant description.
// @returns The combined Signal object.
export combine(Signal[] signals, string group = na, string prefix = "") =>
	float value = 0
	description = ""
	for signal in signals
		value += nz(signal.value)
		if description != ""
			description += "\n"
		description += prefix + signal.description
		if not na(signal.value)
			description += " (" + digits(signal.value) + ")"
	Signal.new(value, description, group)
////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////
// @function Combines a Group object into a single Signal object, with the descriptions concatenated and the value being the sum of all values for the group.
// @param group The Group object to combine.
// @returns The combined Signal object.
export combine(Group group, bool prefixWithSymbol = false) =>
	combine(group.signals, group.group, prefixWithSymbol ? group.symbol + "  " : "")
////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////
// @function Combines an array of Group objects into an array of Signal objects, with the descriptions concatenated and the value being the sum of all values for each group.
// @param groups The array of Group objects to combine.
// @returns The array of combined Signal objects.
export combine(Group[] groups, bool prefixWithSymbol = false) =>
	result = array.new<Signal>()
	for group in groups
		// Combine the signals within the group.
		combined = combine(group, prefixWithSymbol)
		// Add the combined Signal object to the result array.
		array.push(result, combined)
	result
////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////
// @function Counts the number of signals in an array of Group objects.
// @param groups The array of Group objects to count signals from.
// @returns The number of signals in the array of Group objects.
export countSignals(Group[] groups) =>
	count = 0
	for group in groups
		for signal in group.signals
			count += 1
	count
////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////
// @function Utility for adding rows to a Row array and color array.
export addRow(TB.Row[] rows, color[] rowColors, string desc, float value, color color, string tooltip = na) =>
	row = TB.addRow(rows, tooltip = tooltip)
	TB.addCell(row, desc)
	[whole, decimal] = splitDecimal(value)
	sign = value > 0 ? "+" : value < 0 ? "-" : ""
	TB.addCell(row, sign + digits(whole))
	if nz(decimal) != 0
		TB.addCell(row, math.round(decimal, 4))
	
	array.push(rowColors, color)
	row
////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////
// @function A simple table display for showing signal values.
// @param groups All the signal groups in order.
// @param headGroup Optional group to combine in the header.
// @param footGroup Optional group to combine in the footer.
// @param position The position of the display. Default is position.middle_right.
// @returns A TableBuilder.Table object containing the resultant configuration and visible PineScript table.
export display(Group[] groups, Group headGroup = na, Group footGroup = na, string position = position.middle_right) =>
	rows = array.new<TB.Row>()
	rowColors = array.new_color()
	sumUp = sumValues(headGroup)
	sumDn = sumValues(footGroup)

	// Header
	if sumUp != 0
		addRow(rows, rowColors, headGroup.symbol, sumUp, Color.darken(headGroup.color, 50), na(headGroup.group) or headGroup.group == headGroup.symbol ? na : headGroup.group)
	// Body
	for group in groups
		for signal in group.signals
			addRow(rows, rowColors, signal.description, signal.value, group.color)
	// Footer
	if sumDn != 0
		addRow(rows, rowColors, footGroup.symbol, sumDn, Color.darken(footGroup.color, 50), na(footGroup.group) or footGroup.group == footGroup.symbol ? na : footGroup.group)

	tbl = TB.initialize(rows, position)//, frame = TB.LineStyle.new(color.white, 1))
	TB.style(tbl, align = TB.CellAlign.new(text.align_left))
	TB.styleColumn(tbl, 1, align = TB.CellAlign.new(text.align_right))
	TB.styleRows(tbl, rowColors)
	tbl
////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////
// @function Creates a default set of groups to use with the display function.
export defaultGroups() =>
	groups = array.new<Group>()
	posGroup = addGroup(groups, "▲", color.rgb(54, 122, 56), "▲")
	warnGroup = addGroup(groups, "-", color.orange,'-') 
	negGroup = addGroup(groups, "▼", #a92323, "▼")
	[groups, posGroup, warnGroup, negGroup]
////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////
//// DEMO //////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////

if barstate.islast
	// First setup the signal groups to accept values.
	[groups, posGroup, warnGroup, negGroup]  = defaultGroups()

	// Add any signals desired.
	add(posGroup, "Momentum", +1.1)
	add(posGroup, "Oversold", +1)
	add(warnGroup, "Unconfirmed")
	add(negGroup, "Downtrend", -2)
	add(negGroup, "Below VWAP", -1)

	// Show the values (last bar only).
	display(groups, posGroup, negGroup)
