Commission-aware Trade Labels
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © Andrei Bunulu

// @version=5

library("tradelabels", overlay=true)

labelSize(size) =>
    switch size
        "tiny" => size.tiny
        "small" => size.small
        "normal" => size.normal
        "large" => size.large
        "huge" => size.huge
        =>
            size.auto

// @type Order
// @field entry_price Entry price
// @field stop_loss_price Stop loss price
// @field stop_loss_percent Stop loss percent, default 2%
// @field take_profit_price Take profit price
// @field take_profit_percent Take profit percent, default 6%
// @field entry_amount Entry amount, default 5000$
// @field shares Shares
// @field commission Commission, default 0.04%
export type Order
    float entry_price = na
    float stop_loss_price = na
    float stop_loss_percent = 2.0 // 2%
    float take_profit_price = na
    float take_profit_percent = 6.0 // 6%
    float entry_amount = 5000.00
    float shares = na
    float commission = 0.04 // 0.04%
    float risk_reward_ratio = 0.00 // 3.00

// @method entry_price
// @param this Order object
// @return entry_price
export method entry_price(Order this) =>
    entry_price = na(this.entry_price) ? close : this.entry_price
    entry_price

// @method stop_loss_price
// @param this Order object
// @return stop_loss_price
export method stop_loss_price(Order this) =>
    if not this.stop_loss_price and not this.stop_loss_percent
        runtime.error("No stop loss price/percent set")

    entry_price = this.entry_price()
    stop_loss_price = na(this.stop_loss_price) ? entry_price * (1 - this.stop_loss_percent) : this.stop_loss_price
    stop_loss_price

// @method take_profit_price
// @param this Order object
// @return take_profit_price
export method take_profit_price(Order this) =>
    if not this.take_profit_price and not this.take_profit_percent and not this.risk_reward_ratio == 0.00
        runtime.error("No take profit price/percent set")

    entry_price = this.entry_price()
    stop_loss_price = this.stop_loss_price()
    take_profit_price = this.take_profit_price
    take_profit_percent = this.take_profit_percent
    risk_reward_ratio = this.risk_reward_ratio == 0.00 ? na : this.risk_reward_ratio

    if na(take_profit_price) and not na(take_profit_percent)
        take_profit_price := entry_price * (1 + take_profit_percent)
    
    if not na(risk_reward_ratio) and not na(stop_loss_price)
        take_profit_price := entry_price + (entry_price - stop_loss_price) * risk_reward_ratio
    
    take_profit_price

// @method is_long
// @param this Order object
// @return entry_price
export method is_long(Order this) =>
    entry_price = this.entry_price()
    take_profit_price = this.take_profit_price()
    is_long = entry_price < take_profit_price
    is_long

// @method is_short
// @param this Order object
// @return entry_price
export method is_short(Order this) =>
    entry_price = this.entry_price()
    take_profit_price = this.take_profit_price()
    is_short = entry_price > take_profit_price
    is_short

// @method percent_to_target
// @param this Order object
// @param target Target price
// @return percent
export method percent_to_target(Order this, float target) =>
    entry_price = this.entry_price()
    percent = math.abs((target - entry_price) / entry_price) * 100
    percent

// @method risk_reward
// @param this Order object
// @return risk_reward_ratio
export method risk_reward(Order this) =>
    entry_price = this.entry_price()
    take_profit_price = this.take_profit_price()
    stop_loss_price = this.stop_loss_price()
    risk_reward_ratio = this.risk_reward_ratio == 0.00 ? na : this.risk_reward_ratio

    if (not na(take_profit_price) and not na(stop_loss_price)) and
       ((this.is_long() and take_profit_price > entry_price and stop_loss_price < entry_price) or
       (this.is_short() and take_profit_price < entry_price and stop_loss_price > entry_price))
        // Calculate risk/reward
        risk = entry_price - stop_loss_price
        reward = take_profit_price - entry_price
        risk_reward = reward / risk
        risk_reward_ratio := math.round(risk_reward, 2)

    risk_reward_ratio

// @method shares
// @param this Order object
// @return shares
export method shares(Order this) =>
    entry_price = this.entry_price()
    shares = na(this.shares) ? this.entry_amount / entry_price : this.shares
    shares

// @method position_size
// @param this Order object
// @return position_size
export method position_size(Order this) =>
    entry_price = this.entry_price()
    shares = this.shares()
    position_size = shares * entry_price
    position_size

// @method commission_cost
// @param this Order object
// @return commission_cost
export method commission_cost(Order this, float target_price) =>
    entry_price = this.entry_price()
    shares = this.shares()

    // Calculate commission for entry and exit transactions
    commission_cost = ((this.commission / 100) * (entry_price * shares)) + ((this.commission / 100) * (target_price * shares))
    commission_cost

// @method net_result
// @param this Order object
// @param target_price The target price to calculate net result for (either take_profit_price or stop_loss_price)
// @return net_result
export method net_result(Order this, float target_price) =>
    entry_price = this.entry_price()
    shares = this.shares()

    gross_result = this.is_long() ? (target_price - entry_price) * shares : (entry_price - target_price) * shares
    commission_cost = this.commission_cost(target_price)

    // Calculate net result
    net_result = gross_result - commission_cost
    net_result

// @method create_take_profit_label
// @param this Order object
// @param simulate Is this a simulated label, default false
// @param prefix Label prefix
// @param size Label size, default small
// @param offset_x Label offset, default 5
// @param bg_color Label background color
// @param text_color Label text color
// @return void
export method create_take_profit_label(
   Order this,
   bool simulate = false,
   string prefix = "✓",
   string size = "small",
   simple int offset_x = 5,
   color bg_color = na,
   color text_color = #003e1f) =>
    var label take_profit_label = na

    if barstate.islast
        entry_price = this.entry_price()
        target_price = this.take_profit_price()
        stop_loss_price = this.stop_loss_price()
        percent_to_target_price = this.percent_to_target(target_price)

        // Calculate shares
        shares = this.shares()

        // Calculate position size
        position_size = this.position_size()

        // Calculate profit
        net_amount = this.net_result(target_price)

        // Calculate risk/reward
        risk_reward_ratio = this.risk_reward()

        // Label variables
        var string type = entry_price < target_price ? "long" : "short"
        var string rr_text = na(risk_reward_ratio) ? "" : "R/R: " + str.tostring(risk_reward_ratio, "##.##")
        var string commission_cost = "\nCommission: " + str.tostring(this.commission_cost(target_price), "##.##") + "$"
        var string stop_loss_text = na(risk_reward_ratio) ? "" : "\nStop loss: " + str.tostring(stop_loss_price)
        var string amount = simulate ? "" : str.format("{0,number,currency}", net_amount) + " - "

        // Draw the label
        take_profit_label := label.new(
           last_bar_index + offset_x, 
           target_price, 
           text=prefix + " " + str.tostring(target_price) 
           + " - " + str.tostring(percent_to_target_price, "##.##") + "% - " 
           + amount
           + rr_text,
           tooltip = "Entry price: " + str.tostring(entry_price)
           + "\nTarget price: " + str.tostring(target_price)
           + "\nEntry amount: " + str.tostring(position_size) + "$"
           + stop_loss_text
           + commission_cost
           + "\nShares: " + str.tostring(shares, "##.####"))
        label.set_style(take_profit_label, style=type == "long" ? label.style_label_lower_left : label.style_label_upper_left)
        label.set_textcolor(take_profit_label, textcolor = text_color)
        label.set_color(take_profit_label, bg_color)
        label.set_size(take_profit_label, labelSize(size))
        label.delete(take_profit_label[1])

// @method create_stop_loss_label
// @param this Order object
// @param simulate Is this a simulated label, default false
// @param prefix Label prefix
// @param size Label size, default small
// @param offset_x Label offset, default 5
// @param bg_color Label background color
// @param text_color Label text color
// @return void
export method create_stop_loss_label(
   Order this, 
   bool simulate = false,
   string prefix = "✘", 
   string size = "small", 
   simple int offset_x = 5,
   color bg_color = na,
   color text_color = #c9032b) =>
    var label stop_loss_label = na

    if barstate.islast
        entry_price = this.entry_price()
        stop_loss_price = this.stop_loss_price()

        // Calculate shares
        shares = this.shares()

        // Calculate position size
        position_size = this.position_size()
        
        // Calculate profit
        percent_to_stop_loss = this.percent_to_target(stop_loss_price)
        stop_amount = this.net_result(stop_loss_price)

        // Draw the label
        if simulate
            stop_loss_label := label.new(last_bar_index + offset_x, stop_loss_price, text=prefix + " " + str.tostring(stop_loss_price) + " - " + str.tostring(percent_to_stop_loss, "##.##") + "%")
        else
            stop_loss_label := label.new(last_bar_index + offset_x, stop_loss_price, text=prefix + " " + str.tostring(stop_loss_price) + " - " + str.tostring(percent_to_stop_loss, "##.##") + "% - " + str.format("{0,number,currency}", stop_amount))

        label.set_style(stop_loss_label, style=entry_price > stop_loss_price ? label.style_label_upper_left : label.style_label_lower_left)
        label.set_textcolor(stop_loss_label, textcolor = text_color)
        label.set_color(stop_loss_label, bg_color)
        label.set_size(stop_loss_label, labelSize(size))
        label.delete(stop_loss_label[1])

// @method create_entry_label
// @param this Order object
// @param simulate Is this a simulated label, default false
// @param prefix Label prefix
// @param size Label size, default small
// @param offset_x Label offset, default 5
// @param bg_color Label background color
// @param text_color Label text color
// @return void
export method create_entry_label(
   Order this, 
   bool simulate = false,
   string prefix = "⌞", 
   string size = "small", 
   simple int offset_x = 5,
   color bg_color = na,
   color text_color = #49416D) =>
    var label entry_label = na

    if barstate.islast
        entry_price = this.entry_price()

        // Calculate shares
        shares = this.shares()

        // Calculate position size
        position_size = this.position_size()

        // Draw the label
        if simulate
            entry_label := label.new(last_bar_index + offset_x, entry_price, text=prefix + " " + str.tostring(entry_price))
        else
            entry_label := label.new(last_bar_index + offset_x, entry_price, text=prefix + " " + str.tostring(entry_price) + " - " + str.format("{0,number,currency}", position_size) + " - " + "Shares: " + str.tostring(shares, "##.####"))
        label.set_style(entry_label, style=label.style_label_lower_left)
        label.set_textcolor(entry_label, textcolor = text_color)
        label.set_color(entry_label, bg_color)
        label.set_size(entry_label, labelSize(size))
        label.delete(entry_label[1])

export method create_line(Order this, float target_price, color line_color, int offset_x = 10, string line_style = line.style_dotted, int line_width = 1, bool draw_entry_line = true) =>
    var line entry_line = na
    var line line = na

    if barstate.islast
        if draw_entry_line
            entry_price = this.entry_price()
            entry_line := line.new(x1=bar_index, x2=last_bar_index + offset_x, y1=entry_price, y2=entry_price, width=line_width, color=line_color, style=line_style)
            line.delete(entry_line[1])
        line := line.new(x1=bar_index, x2=last_bar_index + offset_x, y1=target_price, y2=target_price, width=line_width, color=line_color, style=line_style)
        line.delete(line[1])

// Example
// ------------------------------------------------
entry_order = Order.new()
entry_order.create_entry_label(simulate=true)
entry_order.create_line(target_price=entry_order.entry_price(), line_color=#49416D, draw_entry_line = false, line_style = line.style_solid)

take_profit_order = Order.new(take_profit_price=25000, stop_loss_price=29000)
take_profit_order.create_take_profit_label(size="normal")
take_profit_order.create_line(target_price=take_profit_order.take_profit_price(), line_color=#003e1f)

take_profit_order2 = Order.new(stop_loss_price=25600, risk_reward_ratio=3.00)
take_profit_order2.create_take_profit_label(size="normal")
take_profit_order2.create_line(target_price=take_profit_order2.take_profit_price(), line_color=#003e1f)

stop_loss_order = Order.new(stop_loss_price=29000)
stop_loss_order.create_stop_loss_label()
stop_loss_order.create_line(target_price=stop_loss_order.stop_loss_price(), line_color=#c9032b, draw_entry_line = false)
