OrderLib
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © Hamster-Coder

//@version=5

// @description TODO: add library description here
library("OrderLib", overlay = true)

import Hamster-Coder/TextLib/6 as textLib

//#region Local methods

// // @function Add offset to price
// addOffset(int side, float price, float commision_prc) =>
//     price * (1 + side * commision_prc / 100)

// @function Get offset (% - "procent") from delta (1 = 100%)
getPercent(float delta) =>
    delta * 100

// @function Get offset delta (1 = 100%) from offset percent (% - "percent")
getDelta(float percent) =>
    percent / 100

addOffsetDelta(float price, float offset_delta) =>
    price * (1 + offset_delta)

addOffsetPercent(float price, float offset_prc) =>
    addOffsetDelta(price, getDelta(offset_prc))


// // @function Substrate offset from price
// subOffset(int side, float price, float commision_prc) =>
//     price * (1 - side * commision_prc / 100)

//#endregion Local methods

// @function TODO: add function description here
// @param x TODO: add parameter x description here
// @returns TODO: add what function returns

export type exchangeFeesModel
    float market_order_prc = 0
    float limit_order_prc = 0

export feesBinanceFutures() =>
    exchangeFeesModel result = exchangeFeesModel.new()
    result.limit_order_prc := 0.02
    result.market_order_prc := 0.04
    result

export type orderPriceModel
    float value = na
    float value_with_commision = na
    float offset = na
    float offset_with_commision = na

getOffsetByValue(int side, float value, float enter_price) =>
    side * getPercent(value / enter_price - 1)

//@param side (int) Order side. "1" - buy order, "-1" sell order
export createOrderPriceByValue(int position_side, int order_side, float value, float enter_price, float commision_prc) =>
    result = orderPriceModel.new()
    result.value := value
    result.offset := getOffsetByValue(side = position_side, value = result.value, enter_price =  enter_price)    
    result.offset_with_commision := result.offset + position_side * order_side * commision_prc
    result.value_with_commision := addOffsetPercent(enter_price, position_side * result.offset_with_commision)
    result

export createOrderPriceByPriceWithCommision(int position_side, int order_side, float value_with_commision, float enter_price, float commision_prc) =>
    result = orderPriceModel.new()
    result.value_with_commision := value_with_commision
    result.offset_with_commision := getOffsetByValue(position_side, result.value_with_commision, enter_price)
    result.offset := result.offset_with_commision - position_side * order_side * commision_prc
    result.value := addOffsetPercent(enter_price, position_side * result.offset_with_commision)
    result

export createOrderPriceByOffset(int position_side, int order_side, float offset, float enter_price, float commision_prc) =>
    result = orderPriceModel.new()
    result.offset := offset
    result.offset_with_commision := result.offset + position_side * order_side * commision_prc
    result.value := addOffsetPercent(enter_price, position_side * result.offset)
    result.value_with_commision := addOffsetPercent(enter_price, position_side * result.offset_with_commision)
    result

export createOrderPriceByOffsetWithCommision(int position_side, int order_side, float offset_with_commision, float enter_price, float commision_prc) =>
    result = orderPriceModel.new()
    result.offset_with_commision := offset_with_commision
    result.offset := result.offset_with_commision - position_side * order_side * commision_prc
    result.value := addOffsetPercent(enter_price, position_side * result.offset)
    result.value_with_commision := addOffsetPercent(enter_price, position_side * result.offset_with_commision)
    result

export type orderAmountModel
    // value in coin
    float coins = na
    //@field value in (usdt / busd / etc.)
    float value = na
    //
    float position_piece_prc = 100

export createOrderAmountFromCoins(float coins, float price) =>
    result = orderAmountModel.new()
    result.coins := coins
    result.value := price * coins
    result.position_piece_prc := 100
    result

export createOrderAmountFromValue(float value, float price) =>
    result = orderAmountModel.new()
    result.value := value
    result.coins := value / price
    result.position_piece_prc := 100
    result

export createOrderAmountFromOther(orderAmountModel other, float price, float piece_prc) =>
    result = orderAmountModel.new()
    result.coins := other.coins * getDelta(piece_prc)
    result.value := result.coins * price
    result.position_piece_prc := piece_prc
    result

export type orderStateModel
    bool is_completed = false
    int completed_on = na // time

export type orderPointModel
    orderPriceModel price
    orderAmountModel amount
    orderStateModel state

export type breakevenOrderModel
    orderPointModel target
    orderPriceModel trigger

export type trailingStopOrderModel   
    orderPriceModel trigger
    float lag_offset = na

export type tradePositionModel
    string ticker = ""
    int side = 1 // "1" - buy , "-1" - sell
    int leverage = 20    
    int created_on = na
    //
    orderPointModel enter = na
    orderPointModel stop_loss = na
    breakevenOrderModel breakeven = na
    trailingStopOrderModel trailing_stop = na
    orderPointModel[] take_profit_collection = na
    // position state
    orderStateModel state

export type lineView
    line _line = na
    label[] labels = na
    orderPriceModel model = na
    bool is_completed = false
    color color = na

export type lineViewCollection
    lineView[] collection = na
    color color = na
    bool is_completed = false

export type tradePositionView
    lineView enter = na    
    lineView stop_loss = na    
    lineViewCollection take_profit_collection = na
    // is completed (not need to repaint)
    bool is_completed = false    
    // reference to order model
    tradePositionModel model = na

//#region Line view logic
createLineView(float price, color color) =>
    lineView result = lineView.new()    
    result.labels := array.new<label>()    
    result.color := color
    result._line := line.new(time, price, time, price, xloc = xloc.bar_time, extend = extend.none, color = color, style = line.style_solid, width = 1)
    result.is_completed := false
    result

createLineViewCollection(color color) =>
    result = lineViewCollection.new()
    result.collection := array.new<lineView>()
    result.color := color
    result.is_completed := false
    result

addLineView(lineViewCollection src, float price) =>
    lineView result = na
    if (na(src))    
        result := na
    else if (na(src.collection))
        result := na
    else
        result := createLineView(price, src.color)
        src.collection.push(result)
    result

descructLineView(lineView src) =>
    if (na(src) == false)
        line.delete(src._line)
        if (src.labels.size() > 0)
            for index = src.labels.size() - 1 to 0
                label.delete(src.labels.get(index))
                src.labels.remove(index)

descructLineViews(lineViewCollection src) =>
    if (na(src) == false)
        if (src.collection.size() > 0)
            for index = src.collection.size() - 1 to 0
                descructLineView(src.collection.get(index))
                src.collection.remove(index)
    src
//#endregion Line view logic    

//#region Order view logic

repaintLine(line src, int x1, int time_now) =>
    bool result = if (na(src))
        false
    else
        src.set_x1(na(x1) ? time_now : x1)
        src.set_x2(time_now)
        true
    result

repaintEnterLine(tradePositionView src, int time_now) =>    
    bool result = if (na(src))
        false
    else if (na(src.model))
        false
    else
        if (src.is_completed == false)        
            repaintLine(src.enter._line, src.model.created_on, time_now)
        true
    result

repaintStopLossLine(tradePositionView src, int time_now) =>
    bool result = if (na(src))
        false
    else if (na(src.model))
        false
    else if (na(src.model.stop_loss))
        false
    else    
        if (src.is_completed == false and src.stop_loss.is_completed == false)
            repaintLine(src.stop_loss._line, src.model.created_on, time_now)
            
            if (src.model.stop_loss.state.is_completed)
                src.stop_loss.is_completed := true
                src.stop_loss.labels.push(label.new(x = time_now, xloc = xloc.bar_time, y = src.model.stop_loss.price.value, style = label.style_xcross, color = color.red, size = size.tiny))
        true
    result

repaintTakeProfitLines(tradePositionView src, int time_now) =>
    bool result = if (na(src))
        false
    else if (na(src.model))
        false
    else if (na(src.model.take_profit_collection))
        false    
    else
        if (src.is_completed == false)
            for [index, line_view] in src.take_profit_collection.collection
                if (line_view.is_completed == false)
                    tp_model = src.model.take_profit_collection.get(index)
                    repaintLine(line_view._line, src.model.created_on, time_now)
                    if (tp_model.state.is_completed)
                        line_view.is_completed := true
                        line_view.labels.push(label.new(x = time_now, xloc = xloc.bar_time, y = tp_model.price.value, style = label.style_diamond, color = color.orange, size = size.tiny))       
        true
    src

export createTradePositionView(tradePositionModel src, simple color profit_color = color.green, simple color loss_color = color.red, simple color enter_color = color.gray) =>     
    tradePositionView result = na
    if (na(src))
        result := na
    else if (na(src.stop_loss))
        result := na
    else if (na(src.take_profit_collection))
        result := na
    else if (src.take_profit_collection.size() == 0)
        result := na
    else 
        result := tradePositionView.new()
        result.model := src
        result.take_profit_collection := createLineViewCollection(profit_color)
        result.enter := createLineView(src.enter.price.value, enter_color)
        result.stop_loss := createLineView(src.stop_loss.price.value, loss_color)
        // add take profits
        lineView last_line_view = na
        for [index, take_profit_model] in src.take_profit_collection
            last_line_view := addLineView(result.take_profit_collection, take_profit_model.price.value)            
        // add fill        
        linefill.new(result.stop_loss._line, result.enter._line, color = color.new(result.stop_loss.color, 80))
        linefill.new(last_line_view._line, result.enter._line, color = color.new(result.take_profit_collection.color, 80))
    result

export disposeTradePositionView(tradePositionView src) =>    
    if (na(src) == false)
        descructLineView(src.enter)        
        descructLineView(src.stop_loss)
        descructLineViews(src.take_profit_collection)
        src.model := na

export disposeTradePositionViews(tradePositionView[] src, bool completed_only = true, int skip = 0) =>
    int result = 0
    if (na(src))
        result := 0
    else if (skip < 0)
        result := 0
    else if (src.size() <= skip)
        result := 0
    else        
        skipped = 0
        for index = src.size() - 1 to 0        
            view = src.get(index)
            if (skipped == skip and (completed_only == false or view.is_completed == true))                
                disposeTradePositionView(view)
                result += 1
                src.remove(index)
            else if (view.is_completed == true)
                skipped += 1                      
    result        

export repaintTradePositionView(tradePositionView src, int time_now) =>
    int result = if (na(src))
        0
    else if (na(src.model))
        0
    else if (src.is_completed == true)
        0
    else
        repaintEnterLine(src, time_now)
        repaintStopLossLine(src, time_now)
        repaintTakeProfitLines(src, time_now)
        if (src.model.state.is_completed)
            src.is_completed := true
        1
    result

export repaintTradePositionViews(tradePositionView[] views, int time_now, bool remove_completed) =>
    int result = 0
    if (na(views))
        result := 0
    else             
        if (views.size() > 0)           
            for index = views.size() - 1 to 0
                view = views.get(index)
                result += repaintTradePositionView(view, time_now)
                if (remove_completed and view.is_completed)
                    views.remove(index)
    result

export updateTradePositionState(tradePositionModel src, float top_price, float bottom_price, int time_now) =>
    bool result = if (na(src))
        0
    else if (na(src.stop_loss))
        0
    else if (na(src.take_profit_collection))
        0
    else 
        if (src.state.is_completed == false)
            if (na(src.stop_loss) == false)
                if (src.side == 1 and src.stop_loss.price.value >= bottom_price)
                    src.stop_loss.state.is_completed := true
                    src.stop_loss.state.completed_on := time_now
                if (src.side == -1 and src.stop_loss.price.value <= top_price)
                    src.stop_loss.state.is_completed := true
                    src.stop_loss.state.completed_on := time_now
                if (src.stop_loss.state.is_completed)            
                    src.state.is_completed := true
                    src.state.completed_on := time_now

            if (src.take_profit_collection.size() > 0)
                for [index, take_profit] in src.take_profit_collection
                    if (take_profit.state.is_completed == false)
                        if (src.side == 1 and take_profit.price.value <= top_price)
                            take_profit.state.is_completed := true
                            take_profit.state.completed_on := time_now
                        if (src.side == -1 and take_profit.price.value >= bottom_price)
                            take_profit.state.is_completed := true
                            take_profit.state.completed_on := time_now
                if (src.take_profit_collection.last().state.is_completed)
                    src.state.is_completed := true
                    src.state.completed_on := time_now
            1
        0
    result

export updateTradePositionStates(tradePositionModel[] models, float top_price, float bottom_price, int time_now, bool remove_completed = true) =>
    result = 0
    if (na(models) == false)
        if (models.size() > 0)
            for index = models.size() - 1 to 0            
                updateTradePositionState(models.get(index), top_price, bottom_price, time_now)
                result += 1
                if (remove_completed and models.get(index).state.is_completed)
                    models.remove(index)
    result

createOrderPoint(orderPriceModel price, orderAmountModel amount) =>
    result = orderPointModel.new()
    result.state := orderStateModel.new()
    result.price := price
    result.amount := amount
    result

export configureEnterPoint(tradePositionModel src, orderPriceModel price, orderAmountModel amount) =>
    // assert        
    bool result = if (na(src))
        argument_null_exception = "src" // argument null exception
        false
    else if (na(price))
        argument_null_exception = "price" // argument null exception
        false
    else if (na(amount))
        argument_null_exception = "amount"    
        false
    // act 
    else
        src.enter := createOrderPoint(price, amount)
        true
    result

export configureMarketOpenOrderByPrice(tradePositionModel src, float price_value, float amount_value, exchangeFeesModel exchange_fees) =>
    // assert        
    bool result = if (na(src))
        argument_null_exception = "src" // argument null exception
        false
    else if (na(price_value))
        argument_null_exception = "price_value" // argument null exception
        false
    else if (na(amount_value))
        argument_null_exception = "amount_value"    
        false
    else if (na(exchange_fees))
        argument_null_exception = "exchange_fees"    
        false
    // act 
    else   
        price = createOrderPriceByValue(src.side, src.side, price_value, price_value, exchange_fees.market_order_prc)
        amount = createOrderAmountFromValue(amount_value, price.value_with_commision)
        configureEnterPoint(src, price, amount)        
    result

export configureStopLoss(tradePositionModel src, orderPriceModel price, orderAmountModel amount) =>
    bool result = false
    // validate arguments
    if (na(src))
        argument_null_exception = "src" // argument null exception
        result := false
    else if (na(price))
        argument_null_exception = "price" // argument null exception
        result := false
    else if (na(amount))
        argument_null_exception = "amount" // argument null exception        
        result := false
    // method body
    else        
        src.stop_loss := createOrderPoint(price, amount)
        result := true
    // result
    result

export configureStopLossByPrice(tradePositionModel src, float price_value, float max_loss, exchangeFeesModel fees) =>
    bool result = if (na(src))
        false
    else if (na(src.enter))
        false
    else 
        orderPriceModel price = createOrderPriceByValue(src.side, -src.side, price_value, src.enter.price.value_with_commision, fees.market_order_prc)
        if (na(max_loss) == false)
            src.enter.amount := createOrderAmountFromValue(math.min(max_loss / getDelta(-price.offset_with_commision), src.enter.amount.value), src.enter.price.value)
        orderAmountModel amount = createOrderAmountFromCoins(src.enter.amount.coins, price.value_with_commision)
        configureStopLoss(src, price, amount)
    result

export addTakeProfit(tradePositionModel src, orderPriceModel price, orderAmountModel amount) =>
    bool result = false
    // validate arguments
    if (na(src))
        argument_null_exception = "src" // argument null exception
        result := false
    else if (na(price))
        argument_null_exception = "price" // argument null exception
        result := false
    else if (na(amount))
        argument_null_exception = "amount" // argument null exception        
        result := false
    // method body
    else        
        src.take_profit_collection.push(createOrderPoint(price, amount))
        result := true
    // result
    result

export addTakeProfitByRR(tradePositionModel src, float rr, float position_piece_prc, exchangeFeesModel exchange_fees) =>
    bool result = false
    // validate arguments
    if (na(src))
        argument_null_exception = "src" // argument null exception
        result := false
    else if (na(src.stop_loss))
        argument_null_exception = "src.stop_loss" // argument null exception        
        result := false
    else if (na(src.enter))
        argument_null_exception = "src.enter" // argument null exception        
        result := false
    else if (na(rr))
        argument_null_exception = "rr" // argument null exception
        result := false    
    else if (na(rr))
        argument_null_exception = "rr" // argument null exception
        result := false    
    else if (na(exchange_fees))
        argument_null_exception = "exchange_fees" // argument null exception
        false        
    // method body
    else  
        price = createOrderPriceByOffset(
                   position_side = src.side, 
                   order_side = -src.side, 
                   offset = - src.stop_loss.price.offset_with_commision * rr,
                   //value = src.enter.price.value_with_commision + (src.enter.price.value_with_commision - src.stop_loss.price.value_with_commision) * rr, 
                   enter_price = src.enter.price.value, 
                   commision_prc = exchange_fees.limit_order_prc
                   )   
        amount = createOrderAmountFromOther(src.enter.amount, price.value, position_piece_prc)
        addTakeProfit(src, price, amount)
    result

export configureBreakeven(
   tradePositionModel src, 
   orderPointModel target,
   orderPriceModel trigger
   ) =>
    bool result = if (na(src))
        false
    else 
        src.breakeven := breakevenOrderModel.new()
        src.breakeven.target := target
        src.breakeven.trigger := trigger
        true
    result

export configureBreakevenByValue(
   tradePositionModel src,
   float trigger_price,
   float target_offset = na,   
   exchangeFeesModel exchange_fees
   ) =>
    bool result = if (na(src))
        false
    else if (na(src.enter))
        false
    else if (na(trigger_price))
        argument_null_exception = "trigger_offset" // argument null exception
        false            
    else if (na(exchange_fees))
        argument_null_exception = "exchange_fees" // argument null exception
        false
    else 
        expected_offset = na(target_offset) ? exchange_fees.market_order_prc : target_offset        
        target_price = createOrderPriceByOffset(src.side, -src.side, expected_offset, src.enter.price.value_with_commision, exchange_fees.market_order_prc)
        target_amount = createOrderAmountFromOther(src.enter.amount, target_price.value_with_commision, 100) 
        target = createOrderPoint(target_price, target_amount)
        trigger = createOrderPriceByValue(src.side, src.side, trigger_price, src.enter.price.value_with_commision, 0)
        configureBreakeven(src, target, trigger)
    result

export configureBreakevenByOffset(
   tradePositionModel src,
   float trigger_offset,
   float target_offset = na,   
   exchangeFeesModel exchange_fees
   ) =>
    bool result = if (na(src))
        false
    else if (na(src.enter))
        false
    else if (na(trigger_offset))
        argument_null_exception = "trigger_offset" // argument null exception
        false            
    else if (na(exchange_fees))
        argument_null_exception = "exchange_fees" // argument null exception
        false
    else 
        expected_offset = na(target_offset) ? exchange_fees.market_order_prc : target_offset        
        target_price = createOrderPriceByOffset(src.side, -src.side, expected_offset, src.enter.price.value_with_commision, exchange_fees.market_order_prc)
        target_amount = createOrderAmountFromOther(src.enter.amount, target_price.value_with_commision, 100) 
        target = createOrderPoint(target_price, target_amount)
        trigger = createOrderPriceByOffset(src.side, src.side, trigger_offset, src.enter.price.value_with_commision, 0)
        configureBreakeven(src, target, trigger)
    result


export configureBreakevenBySL(
   tradePositionModel src,
   float target_offset = na,
   float rr = 1,
   exchangeFeesModel exchange_fees
   ) =>
    bool result = if (na(src))
        false
    else if (na(src.enter))
        false
    else if (na(src.stop_loss))        
        false
    else if (na(exchange_fees))
        false
    else 
        expected_offset = na(target_offset) ? exchange_fees.market_order_prc : target_offset
        expected_rr = na(rr) ? 1 : rr
        target_price = createOrderPriceByOffset(src.side, -src.side, expected_offset, src.enter.price.value_with_commision, exchange_fees.market_order_prc)
        target_amount = createOrderAmountFromOther(src.enter.amount, target_price.value_with_commision, 100) 
        target = createOrderPoint(target_price, target_amount)
        trigger = createOrderPriceByOffset(src.side, src.side, -src.stop_loss.price.offset_with_commision * expected_rr, src.enter.price.value, 0)
        configureBreakeven(src, target, trigger)
    result

// @function                Configures "Trailing Stop"
// @param src               (orderModel, required) target order model
// @param lag_offset        (float, percent, required) Execute stop order if price reverse back on [lag_offset] percents
// @param trigger_offset    (float, percent) "Trailing Stop" starts working if price moved by [trigger_offset] related to [src.enter.price]
// @param trigger_price     (float) "Trailing Stop" starts working if price moved to [trigger_price]
// @param commision_percent (float, percent, required, default) Exchange commision for open + close orders togather (default = 0.08)
export configureTrailingStop(tradePositionModel src, orderPriceModel trigger, float lag_offset) =>  
    // validate arguments
    bool result = if (na(src))
        argument_null_exception = "src" // argument null exception
        false
    else if (na(trigger))
        argument_null_exception = "trigger"        
        false
    else if (na(lag_offset))
        argument_null_exception = "lag_offset"        
        false 
    // method body
    else
        src.trailing_stop := trailingStopOrderModel.new()
        src.trailing_stop.lag_offset := lag_offset 
        src.trailing_stop.trigger := trigger
        true
    // return result
    result

export configureTrailingStopByStopLoss(tradePositionModel src, float lag_offset_rr, float trigger_offset_rr, exchangeFeesModel exchange_fees) =>
    // validate arguments
    bool result = if (na(src))        
        argument_null_exception = "src" // argument null exception        
        false
    else if (na(src.enter))
        argument_null_exception = "src.stop_loss" // argument null exception        
        false
    else if (na(src.stop_loss))
        argument_null_exception = "src.stop_loss" // argument null exception        
        false
    else if (na(lag_offset_rr))
        argument_null_exception = "lag_offset_rr" // argument null exception
        false
    else if (na(trigger_offset_rr))
        argument_null_exception = "trigger_offset_rr" // argument null exception
        false
    else if (na(exchange_fees))
        argument_null_exception = "exchange_fees" // argument null exception
        false
    // method body
    else
        trigger = createOrderPriceByOffset(src.side, src.side, -src.stop_loss.price.offset_with_commision * trigger_offset_rr, src.enter.price.value_with_commision, 0)
        lag_offset = -src.stop_loss.price.offset_with_commision * lag_offset_rr

        configureTrailingStop(
           src              = src,
           trigger          = trigger,
           lag_offset       = lag_offset
           )
    // return result
    result

// @function Creates empty order
export createTradePosition(string ticker, int side, int started_on, int leverage = 1) =>
    tradePositionModel result = tradePositionModel.new()    
    result.created_on := started_on
    result.leverage := leverage
    result.ticker := ticker
    result.side := side
    result.take_profit_collection := array.new<orderPointModel>()     
    result.state := orderStateModel.new()
    result

// // @function                Creates order with enter point
// // @param ticker            (string) Name of the ticker
// // @param side              (int) "1" - buy , "-1" - sell
// // @param enter             (float) enter point price
// // @param max_amount_usd    (float) Maximum order amount in USDT 
// // @param leverage          (int) Order leverage (default x1)
// export createTradePositionWithEnterPoint(string ticker, int side, int started_on, float enter, float max_amount_usd, int leverage = 1) =>
//     tradePositionModel result = createTradePosition(ticker, side, started_on, leverage)    
//     configureEnterPoint(result, createOrderPriceByPrice(enter, enter, commision),  enter, amount_usd = max_amount_usd)
//     result

// // @function Creates order with enter point, stop loss and take profit
// export createTradePositionWithEST(string ticker, int side, int started_on, float enter, float max_amount_usd, float sl, float rr, float max_loss = na, int leverage = 1, float commision_percent = 0.08) =>
//     tradePositionModel result = createOrderWithEnterPoint(ticker, side, started_on, enter, max_amount_usd, leverage)
//     configureStopLoss(result, sl, max_loss = max_loss, commision_percent = commision_percent)
//     addTakeProfit(result, 100, rr = rr, commision_percent = commision_percent)
//     result

//#region === TRADE DATA SECTION ===

export type tradeStatisticsModel
    float income = 0
    //
    int error = 0
    int completed = 0
    int in_progress = 0
    int win = 0
    int win_partial = 0
    int loss = 0

export type tradeDebugDataModel
    int updated_views = 0
    int updated_models = 0
    int updated_statistics = 0

export type tradeDataModel
    tradePositionModel[] buy_model_collection
    tradePositionModel[] sell_model_collection
    tradePositionView[] buy_view_collection
    tradePositionView[] sell_view_collection
    tradeStatisticsModel statistics
    tradeDebugDataModel debug

export initTradeData() => 
    tradeDataModel result           = tradeDataModel.new()
    result.buy_model_collection     := array.new<tradePositionModel>()
    result.sell_model_collection    := array.new<tradePositionModel>()
    result.buy_view_collection      := array.new<tradePositionView>()
    result.sell_view_collection     := array.new<tradePositionView>()
    result.statistics               := tradeStatisticsModel.new()
    result.debug                    := tradeDebugDataModel.new()
    result

export clearTradeData(tradeDataModel src) =>
    if (na(src) == false)
        src.buy_model_collection.clear()
        src.sell_model_collection.clear()
        disposeTradePositionViews(src.buy_view_collection, completed_only = false)
        disposeTradePositionViews(src.sell_view_collection, completed_only = false)

calculateTradePositionIncome(tradePositionModel src) =>
    float result = 0
    float p_piece = 0
    for [index, tp] in src.take_profit_collection
        if (tp.state.is_completed)
            p_piece += tp.amount.position_piece_prc / 100
            result += math.abs(tp.price.offset_with_commision / -src.stop_loss.price.offset_with_commision * tp.amount.position_piece_prc) / 100,
    if (src.stop_loss.state.is_completed)
        result -= p_piece > 0 ? 0 : 1 // actuall this means we have breakeven stop loss after reachin any take profit
    // truncating to 0.01
    result := math.ceil(result * 100) / 100
    result

calculateTradePositionResult(tradePositionModel src) =>
    string result = na
    if (na(src))
        result := na
    else if (src.state.is_completed == false)
        result := "in-progress"
    else 
        float p_piece = 0
        for [index, tp] in src.take_profit_collection
            if (tp.state.is_completed)
                p_piece += tp.amount.position_piece_prc / 100
        if (p_piece >= 1)
            result := "win"
        else if (p_piece <= 0)
            result := "loss"
        else
            result := "win-partial"
    result

filterByCreatedOn(tradePositionModel[] collection, int start_date = na, int end_date = na) =>
    result = array.new<tradePositionModel>()    
    if (na(collection))
        result := array.new<tradePositionModel>()
    else if (na(start_date) and na(end_date))
        result := collection.copy()
    else     
        for [index, item] in collection
            if ((na(start_date) or item.created_on >= start_date) and (na(end_date) or item.created_on < end_date))
                result.push(item)
    result    

getOrderCollectionResults(tradePositionModel[] collection) =>
    tradeStatisticsModel result = tradeStatisticsModel.new()    
    if (na(collection) == false)
        for [index, src] in collection
            result.income += calculateTradePositionIncome(src)
            switch calculateTradePositionResult(src)                    
                "in-progress" => 
                    result.in_progress += 1
                "win" => 
                    result.completed += 1
                    result.win += 1
                "loss" => 
                    result.completed += 1
                    result.loss += 1
                "win-partial" => 
                    result.completed += 1
                    result.win_partial += 1
                => 
                    result.error += 1
    result

updateTradeDataStatistics(tradeDataModel src) =>
    buy_orders_result = getOrderCollectionResults(src.buy_model_collection)
    sell_orders_result = getOrderCollectionResults(src.sell_model_collection)
    src.statistics := tradeStatisticsModel.new()
    src.statistics.completed := buy_orders_result.completed + sell_orders_result.completed
    src.statistics.error := buy_orders_result.error + sell_orders_result.error
    src.statistics.income := buy_orders_result.income + sell_orders_result.income
    src.statistics.in_progress := buy_orders_result.in_progress + sell_orders_result.in_progress
    src.statistics.win := buy_orders_result.win + sell_orders_result.win
    src.statistics.win_partial := buy_orders_result.win_partial + sell_orders_result.win_partial
    src.statistics.loss := buy_orders_result.loss + sell_orders_result.loss
    // calculate scanned models
    buy_orders_result.completed + sell_orders_result.completed +  buy_orders_result.error + sell_orders_result.error

export refreshTradeData(tradeDataModel src, float high_price, float low_price, int time, bool remove_completed) =>
    src.debug.updated_models += updateTradePositionStates(src.buy_model_collection, high_price, low_price, time, remove_completed)
    src.debug.updated_models += updateTradePositionStates(src.sell_model_collection, high_price, low_price, time, remove_completed)
    src.debug.updated_views += repaintTradePositionViews(src.buy_view_collection, time, remove_completed)
    src.debug.updated_views += repaintTradePositionViews(src.sell_view_collection, time, remove_completed)
    src.debug.updated_statistics += updateTradeDataStatistics(src)

export addBuyPosition(tradeDataModel src, tradePositionModel position) =>
    if (na(src))
        false
    else if (na(position))
        false
    else 
        src.buy_model_collection.push(position)
        view = createTradePositionView(position)
        src.buy_view_collection.push(view)
        true

export addSellPosition(tradeDataModel src, tradePositionModel position) =>
    if (na(src))
        false
    else if (na(position))
        false
    else 
        src.sell_model_collection.push(position)
        view = createTradePositionView(position)
        src.sell_view_collection.push(view)
        true   

//#endregion === TRADE INFO SECTION ===

//#region === SERIALIZE POSITION  ===

export serializeToText(tradePositionModel src, textLib.textFormatOptions text_format_options) =>
    result = ""
    result := textLib.concatLine(result, str.format("Ticker: {0}", src.ticker))
    result := textLib.concatLine(result, str.format("Side: {0}", src.side == 1 ? "BUY" : "SELL"))
    result := textLib.concatLine(result, str.format("Leverage: {0}x", src.leverage))
    
    if (na(src.enter) == false)
        result := textLib.concatLine(result, "===")        
        result := textLib.concatLine(result, str.format("Enter: {0}", str.tostring(src.enter.price.value, text_format_options.currency_format)))
        result := textLib.concatLine(result, str.format("Amount: {0}", str.tostring(src.enter.amount.value, text_format_options.currency_format)))
        result := textLib.concatLine(result, str.format("Coins: {0}", str.tostring(src.enter.amount.coins, text_format_options.basecurrency_format)))
        result := textLib.concatLine(result, str.format("Real Enter: {0}", str.tostring(src.enter.price.value_with_commision, text_format_options.currency_format)))
        
    if (na(src.stop_loss) == false)
        result := textLib.concatLine(result, "===")
        result := textLib.concatLine(result, str.format("Expected Stop Loss ({0}): {1}", syminfo.currency, str.tostring(src.stop_loss.price.value, text_format_options.currency_format)))
        result := textLib.concatLine(result, str.format("Expected Stop Loss (%): {0}", str.tostring(src.stop_loss.price.offset, text_format_options.percent_format)))
        result := textLib.concatLine(result, str.format("Actual Stop Loss ({0}): {1}", syminfo.currency, str.tostring(src.stop_loss.price.value_with_commision, text_format_options.currency_format)))
        result := textLib.concatLine(result, str.format("Actual Stop Loss (%): {0}", str.tostring(src.stop_loss.price.offset_with_commision, text_format_options.percent_format)))
        result := textLib.concatLine(result, str.format("Stop Loss Amount ({0}): {1}", syminfo.currency, str.tostring(src.stop_loss.amount.value, text_format_options.currency_format)))
        result := textLib.concatLine(result, str.format("Stop Loss Amount ({0}): {1}", syminfo.basecurrency, str.tostring(src.stop_loss.amount.coins, text_format_options.basecurrency_format)))
        result := textLib.concatLine(result, str.format("Stop Loss Position (%): {0}", str.tostring(src.stop_loss.amount.position_piece_prc, text_format_options.percent_format)))

    if (na(src.take_profit_collection) == false)
        result := textLib.concatLine(result, "===")
        for [index, tp] in src.take_profit_collection
            result := textLib.concatLine(result, str.format("Expected Take Profit #{0} ({1}): {2}", index, syminfo.currency, str.tostring(tp.price.value, text_format_options.currency_format)))
            result := textLib.concatLine(result, str.format("Expected Take Profit #{0} (%): {1}", index, str.tostring(tp.price.offset, text_format_options.percent_format)))
            result := textLib.concatLine(result, str.format("Actual Take Profit #{0} ({1}): {2}", index, syminfo.currency, str.tostring(tp.price.value_with_commision, text_format_options.currency_format)))
            result := textLib.concatLine(result, str.format("Actual Take Profit #{0} (%): {1}", index, str.tostring(tp.price.offset_with_commision, text_format_options.percent_format)))
            result := textLib.concatLine(result, str.format("Take Profit #{0} Amount ({1}): {2}", index, syminfo.currency, str.tostring(tp.amount.value, text_format_options.currency_format)))
            result := textLib.concatLine(result, str.format("Take Profit #{0} Amount ({1}): {2}", index, syminfo.basecurrency, str.tostring(tp.amount.coins, text_format_options.basecurrency_format)))
            result := textLib.concatLine(result, str.format("Take Profit #{0} Position (%): {1}", index, str.tostring(tp.amount.position_piece_prc, text_format_options.percent_format)))
    
    if (na(src.breakeven) == false)
        result := textLib.concatLine(result, "===")
        result := textLib.concatLine(result, str.format("Breakeven Trigger (%): {0}", str.tostring(src.breakeven.trigger.offset, text_format_options.percent_format)))
        result := textLib.concatLine(result, str.format("Breakeven Trigger ({0}): {1}", syminfo.currency, str.tostring(src.breakeven.trigger.value, text_format_options.currency_format)))
        result := textLib.concatLine(result, str.format("Breakeven Target (%): {0}", str.tostring(src.breakeven.target.price.offset, text_format_options.percent_format)))
    
    if (na(src.trailing_stop) == false)
        result := textLib.concatLine(result, "===")
        result := textLib.concatLine(result, str.format("Trailing Stop Trigger (%): {0}", str.tostring(src.trailing_stop.trigger.offset, text_format_options.percent_format)))
        result := textLib.concatLine(result, str.format("Trailing Stop Trigger ({0}): {1}", syminfo.currency, str.tostring(src.trailing_stop.trigger.value, text_format_options.currency_format)))
        result := textLib.concatLine(result, str.format("Trailing Stop Lag (%): {0}", str.tostring(src.trailing_stop.lag_offset, text_format_options.percent_format)))
    
    result

//#endregion === SERIALIZE POSITION  ===