Matrix Functions test script - JD
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © jorisduyck
//@version=5

indicator("Matrix Functions test script - JD")
//@description This library converts 1D Pine arrays into 2D matrices and provides matrix manupulation and calculation functions.

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// The arrays provided in Pinescript are linear 1D strucures that can be seen either as a large vertical stack or
// a horizontal row containing a list of values, colors, bools,..
// 
// With the FUNCTIONS in this script the 1D ARRAY LIST can be CONVERTED INTO A 2D MATRIX form
// 
// 
///////////////////////////////////////////
/// BASIC INFO ON THE MATRIX STRUCTURE: ///
///////////////////////////////////////////
// 
// The matrix is set up as an 2D structure and is devided in ROWS and COLUMNS.
//  following the standard mathematical notation:
//
//      a 3 x 4 matrix =          4 columns
//                                 0 1 2 3  column index
//                              0 [a b c d]
//                       3 rows 1 [e f g h]
//                              2 [i j k l]
//                            row
//                           index
//
// With the use of some purpose-built functions, values can be placed or retrieved in a specific column of a certain row
//  this can be done by intuitively using row_nr and column_nr coördinates,
//  without having to worry on what exact index of the Pine array this value is located (the functions do these conversions for you)
//
//
// the syntax I propose for the 2D Matrix array has the following structure:
// 
//  - the array starts with 2 VALUES describing the DIMENSION INFORMATION, (rows, columns)
//      these are ignored in the actual calculations and serve as a metadata header (similar to the "location, time,... etc." data that is stored in photo files)
//      so the array always carries it's own info about the nr. of rows and columns and doesn't need is seperate "info" file!
//
//      To stay consistent with the standard Pinescript (array and [lookback]) indexing:
//      - indexes for sheets and columns start from 0 (first) and run up to the (total nr of sheets or columns) - 1
//      - indexes for rows also start from 0 (most recent, cfr.[lookback]) and run up to the (total nr of rows) - 1
// 
//  - this 2 value metadata header is followed by the actual df data
//      the actual data array can consist of (100,000 - 2) usable items,
//      
//      In a theoretical example, you can have a matrix with almost 20,000 rows with each 5 columns of data (eg. open, high, low, close, volume) in it!!!
//
///////////////////////////////////
/// SCHEMATIC OF THE STRUCTURE: ///
///////////////////////////////////
//
////// (metadata header with dimensions info)
//
//                (0)             (1)            (array index)
//       [nr_of_rows/records, nr_of_columns,
//
//
////// (actual matrix array data)
//  
//                  0             1             2               3                   column_index
//
//                 (2)           (3)           (4)             (5)            (array index)
//        0    record0_val0, record0_val1, record0_val2, record0_val3, ...,
//      
//                 (6)           (7)           (8)             (9)            (array index)
//        1    record1_val0, record1_val1, record1_val2, record1_val3, ...,
//      
//                 (10)          (11)          (12)            (13)            (array index)
//        2    record2_val0, record2_val1, record2_val2, record2_val3, ...,
//      
//        3    ...
//
// row_index
//
////////////////////////////////////
/// DATA MANIPULATION FUNCTIONS: ///
////////////////////////////////////
//
// A set of functions are implemented to write info to and retrieve info from the dataframe.
//  Of course the possibilities are not limited to the functions here, these were written because they were needed for the script!
//  A whole list of other functions can be easily written for other manipulation purposes.
//  The ones written in the script are 2D versions of most of the functions that are provided for normal 1D arrays.
//
//  Feel free to use the functions library below in your scripts! A little shoutout would be nice, of course!!
//
//
///////////////////////////////////////////////////////////////////////
/// LIST OF FUNCTIONS contained in this script:                     ///
/// (many other can of course be written, using the same structure) ///
///////////////////////////////////////////////////////////////////////
//
// - INFO functions
//      * get the number of COLUMNS
//      * get the number of ROWS
//      * get the total SIZE (number of columns, number of rows)
// 
// - INITIALISATION and SET functions (commands to build the 2D matrix)
//      * MATRIX INITIALISATION function, builds 2D matrix with dimensional metadata/size in first 2 values //
//          a new matrix array is built (cfr. the conventions I propose above) containing a certain nr of rows and columns
//      * function to SET a single value in a certain row/lookback period and column
//
// - GET functions (to retrieve info from the 2D matrix)
//      * function to GET/retrieve a single VALUE from a certain row/lookback period and column
//      * function to cut of the metadata header and return the array body (as an array)
//      * function to GET/retrieve the values from a ROW/RECORD from a certain row/lookback period, the values are returned as an array
//
// - 1D - 2D COORDINATE CONVERSION FUNCTIONS (handy with for loop indexes) ///
//      * function to get the ROW INDEX in a 2D matrix from the 1D array index
//      * function to get the COLUMN INDEX in a 2D matrix from the 1D array index
//      * function to get (row, column) coordinates in a 2D matrix from the 1D array index
//      * function to get the 1D array index from (row, column) coordinates in a 2D matrix
//
// - Matrix MANIPULATION functions
//      * function to ADD a ROW/RECORD on the TOP of the array, shift the whole list one down and REMOVE the OLDEST row/record
//          (2D version of "unshift" + "pop" but with a whole row at once)
//      * function to REMOVE one or more ROWS/RECORDS from a 2D matrix
//          (if from_row == to_row, only this row is removed)
//      * function to REMOVE one or more COLUMNS from a 2D matrix
//          (if from_column == to_column, only this column is removed)
//      * function to INSERT an ARRAY of ROWS/RECORDS at a certain row number in a 2D matrix
//      * function to INSERT an ARRAY of COLUMNS at a certain column number in a 2D matrix
//      * function to APPEND/ADD an ARRAY of ROWS/RECORDS to the BOTTOM of a 2D matrix
//      * function to APPEND/ADD an ARRAY of COLUMNS to the SIDE of a 2D matrix
//      * function to POP/REMOVE and return the last ROW/RECORD from the BOTTOM of a 2D matrix
//      * function to POP/REMOVE and return the last column from the SIDE of a 2D matrix
//
// - function to matrix.print a matrix
//      This function is mainly used for debugging purposes and displays the array as a 2D matrix notation
//
//
// Enjoy!
// JD.
//
// #NotTradingAdvice #DYOR
//
//  Disclaimer.
// I AM NOT A FINANCIAL ADVISOR.
// THESE IDEAS ARE NOT ADVICE AND ARE FOR EDUCATION PURPOSES ONLY.
// ALWAYS DO YOUR OWN RESEARCH!
//}

// Function declarations //
//{
/////////////////////////////////////////////////////////////////////////////////
///                         Import Matrix Functions                           ///
/////////////////////////////////////////////////////////////////////////////////
//{
import Duyck/Matrix_Functions_Lib_JD/1 as matrix
/////////////////////////////////////////////////////////////////////////////////

/// test section ///
// set test matrices
//{
a = matrix.from_list(3, 4, array.from(
                      11, 12, 13, 14,
                      21, 22, 23, 24,
                      31, 32, 33, 34))

b = array.copy(a)

[_rows, _columns] = matrix.get_size(a)
if barstate.isfirst
    var matrix_layout = table.new("bottom_left", _columns + 4, _rows + 2, bgcolor = color.teal, frame_color = color.gray)
    table.cell(matrix_layout, 0, 0, " ")
    table.cell(matrix_layout, _columns + 3, 1, " ")
    for i = 1 to _rows
        table.cell(matrix_layout, 1, i, "[")
        table.cell(matrix_layout, _columns + 2, i, "]")
        if i > 0 and i < _rows + 1 
            for j = 0 to _columns - 1
                table.cell(matrix_layout, j + 2, i, str.tostring(matrix.get(a, i - 1, j)))
                table.cell_set_text_color(matrix_layout, j + 1, i + 1, color.black)

// array to test add row functions
insert_row = matrix.from_list( 1, 4, array.from(
                                 10, 20, 30, 40))

// array to test insert/append row functions
insert_row_array = matrix.from_list(2, 4, array.from(
                                      10, 20 ,30, 40,
                                      50, 60, 70, 80))

// array to test add column functions
insert_column = matrix.from_list(3, 1, array.from(
                                  10,
                                  20,
                                  30,
                                  40))

// array to test insert/append column functions
insert_column_array = matrix.from_list(3, 2, array.from(
                                         10, 20,
                                         30, 40,
                                         50, 60))

// array to test element wise multiplication function
elem_multiplication_array = matrix.from_list(3, 4, array.from(
                                      10, 20 ,30, 40,
                                      20, 30, 40, 50,
                                      30, 40, 50, 60))
                                      
// array to test multiplication function
multiplication_array = matrix.from_list(4, 2, array.from(
                                          2, 3,
                                          2, 3,
                                          2, 3,
                                          2, 3))

// array to test determinant function
determinant_array = matrix.from_list(4, 4, array.from(
                                      4, 3, 2, 2,
                                      0, 1,-3, 3,
                                      0,-1, 3, 3,
                                      0, 3, 1, 1))
//}

// parameters to check behavior of functions
//{
x = input.int(0, title="from")
y = input.int(0, title="to (to chose only 1 row or column, set the same value for 'from' and 'to')")
c_type = input.string("none", title = "result matrix", options = ["none", "add rows to top and shift list", "get row/record from list", "remove array of rows", "remove array of columns", "insert array of rows",
                                                           "insert array of columns", "append array of rows", "append array of columns", "pop last row" , "pop last column", "element multiplication", "matrix multiplication",
                                                           "matrix transposition", "matrix determinant"])
//}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/// test function calls ///                                                                                                   //
///////////////////////////
//                                                                                                                            //
// almost all functions can be called with or without a return array //                                                       //
// for example, both notations below can be used:                                                                             //
//                                                                                                                            //
//              c = append_array_of_rows(a, append_array) -> returns the appended matrix as array "c"                //
//                                                                                                                            //
//              append_array_of_rows(a, append_array)     -> just runs the function without creating a new array "c" //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////



manipulation_array =
 c_type == "add rows to top and shift list" ? insert_row :
 c_type == "remove array of rows"           ? insert_row_array :
 c_type == "remove array of columns"        ? insert_column_array :
 c_type == "insert array of rows"           ? insert_row_array :
 c_type == "insert array of columns"        ? insert_column_array :
 c_type == "append array of rows"           ? insert_row_array :
 c_type == "append array of columns"        ? insert_column_array :
 c_type == "element multiplication"         ? elem_multiplication_array :
 c_type == "matrix multiplication"          ? multiplication_array :
  insert_row

c = 
 c_type == "add rows to top and shift list" ? matrix.add_row(a, manipulation_array) :
 c_type == "remove array of rows"           ? matrix.remove_rows(a, x, y) :
 c_type == "remove array of columns"        ? matrix.remove_columns(a, x, y) :
 c_type == "insert array of rows"           ? matrix.insert_array_of_rows(a, manipulation_array, x) :
 c_type == "insert array of columns"        ? matrix.insert_array_of_columns(a, manipulation_array, x) :
 c_type == "append array of rows"           ? matrix.append_array_of_rows(a, manipulation_array) :
 c_type == "append array of columns"        ? matrix.append_array_of_columns(a, manipulation_array) :
 c_type == "element multiplication"         ? matrix.multiply_elem(a, manipulation_array) :
 c_type == "matrix multiplication"          ? matrix.multiply(a, manipulation_array):
 c_type == "matrix transposition"           ? matrix.transpose(a) :
 array.copy(a)

e =
 c_type == "get row/record from list" ? matrix.get_record(a, x) :
 c_type == "pop last row"             ? matrix.pop_row(a) :
 c_type == "pop last column"          ? matrix.pop_column(a) :
  array.copy(a)

// plot matrices
//{
// copy of original matrix before "manipulation"
if c_type != "matrix determinant"
    if barstate.isfirst
        matrix.print(b, pos = "middle_left", title = "original")

// "manipulation" matrix
if c_type != "remove array of rows" and c_type != "remove array of columns" and c_type != "get row/record from list"
 and c_type != "pop last row" and c_type != "pop last column" and c_type != "matrix transposition" and c_type != "matrix determinant" or c_type == "add rows to top and shift list"
    if barstate.isfirst
        matrix.print(manipulation_array, pos = "top_center", title = "work array")

// original matrix
if c_type != "matrix multiplication" and c_type != "element multiplication" and c_type != "matrix determinant" and c_type != "matrix transposition"
    if barstate.isfirst
        matrix.print(a, pos = "bottom_left", title = "modified original")

// function title
from    = " (from = " + str.tostring(x)
from_to = from + " -> to = " +  str.tostring(y) + " )"
title_txt = c_type +
 (c_type == "remove array of rows" or c_type == "remove array of columns" ? from_to :
  c_type == "insert array of rows" or c_type == "insert array of columns" or  c_type == "get row/record from list" ? from + " )" :
  c_type == "matrix determinant" ? " det = " + str.tostring(matrix.determinant_4x4(determinant_array)) : "")
if barstate.isfirst
    title_tab = table.new("bottom_center", 1, 1, bgcolor = color.orange)
    table.cell(title_tab, 0, 0, text = title_txt, text_color = color.black, text_size = size.large)

// copy of resulting matrix
if c_type != "get row/record from list" and c_type != "pop last row" and c_type != "pop last column" or c_type == "add rows to top and shift list"
    if barstate.isfirst
        matrix.print(c, pos = "middle_center", title = c_type == "matrix determinant" ? "original" : "result array")

// copy of resulting matrix
if c_type == "get row/record from list" or c_type == "pop last row" or c_type == "pop last column" and c_type != "matrix determinant"
    if barstate.isfirst
        result = array.copy(e)
        array.unshift(result, c_type == "pop last row" or c_type == "get row/record from list"? array.size(e) : 1)
        array.unshift(result, c_type == "pop last row" or c_type == "get row/record from list"? 1 : array.size(e))
        matrix.print(result, pos = "middle_center", title = "result array")
//}