nasan-risk-score-postion-size-estimator.pine
Copy
// This Pine Script™ code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © federalTacos5392b
//@version=6
indicator("Nasan Risk Score & Postion Size Estimator", overlay = true)
if not timeframe.isdaily
runtime.error("This indicator only works on the daily timeframe")
// Input parameters for user customization
capital =input.float(10000, "Capital")
winRate = input.float(50, "Win Rate (n)", step = 0.01) // Default win rate
profit_Ratio = input.float(2.0, "Profit Ratio", step = 0.01) // Default profit ratio
scalingType = input.string("Aggressive", "Risk Scaling", options = ["Aggressive", "Conservative"]) // Choose scaling type
profitRatio = 1 - (1/(1+profit_Ratio))
startYear = 1990
endYear = 2050
lookbackYears = 5
Max_acceptable_dd = input.float(20.0, "Max Acceptable Draw Down")
// Inputs for customization
var int start_year_ = startYear
// Variables for tracking the current year's high and low values
var float current_high_ = na
var float current_low_ = na
var int current_year_ = year(time)
var float[] max_dd_array = array.new_float()
var int[] years_array_ = array.new_int()
// Detect new year and update arrays
if year(time) != current_year_
if not na(current_high_) and not na(current_low_) and current_year_ >= start_year_
// Calculate Max Drawdown as a percentage: (Year's High - Year's Low) / Year's High * 100
float max_dd = 100 * (current_high_ - current_low_) / current_high_
array.push(max_dd_array, max_dd)
array.push(years_array_, current_year_)
// Reset values for the new year
current_year_ := year(time)
current_high_ := high
current_low_ := low
else
// Update yearly high and low values
current_high_ := math.max(current_high_, high)
current_low_ := math.min(current_low_, low)
// On the final bar, store the current year's data if valid and calculate weighted average
var float weighted_avg_dd = na
if barstate.islast
if not na(current_high_) and not na(current_low_) and current_year_ >= start_year_
float max_dd = 100 * (current_high_ - current_low_) / current_high_
array.push(max_dd_array, max_dd)
array.push(years_array_, current_year_)
int available_years = array.size(years_array_)
if available_years > 0
int last_5_count = math.min(5, available_years)
float sum_weighted_dd = 0.0
float total_weight = 0.0
for i = 0 to last_5_count - 1
int index = available_years - last_5_count + i
float dd_value = array.get(max_dd_array, index)
int weight = i + 1 // Weighting: as provided, "last_5_count - i"
sum_weighted_dd += dd_value * weight
total_weight += weight
weighted_avg_dd := sum_weighted_dd / total_weight
// Plot the weighted average max drawdown as a line on the chart
plot(weighted_avg_dd, title="Weighted Avg Max Drawdown", color=color.red, linewidth=2, style=plot.style_line)
Tol = (100 - (weighted_avg_dd - Max_acceptable_dd)) /100
Tolerance_ = Tol
// Inputs for customization
var int start_year = startYear
// Variables for tracking the current year and ATR percentage
var float current_high = na
var float current_low = na
var int current_year = year(time)
var float[] max_atr_array = array.new_float()
var float[] min_atr_array = array.new_float()
var int[] years_array = array.new_int()
// ATR Calculation
atr_percent = 100 * ta.ema(ta.tr / close, 14)
// Plot the ATR percentage on the chart
plot(atr_percent, color=color.gray, title="ATR% (14 EMA)")
// Detect new year and update arrays
if year(time) != current_year
if not na(current_high) and not na(current_low) and current_year >= start_year
array.push(max_atr_array, current_high) // Store max ATR% for the year
array.push(min_atr_array, current_low) // Store min ATR% for the year
array.push(years_array, current_year) // Store the year
// Reset values for the new year
current_year := year(time)
current_high := atr_percent
current_low := atr_percent
else
// Update yearly high and low ATR% values
current_high := math.max(current_high, atr_percent)
current_low := math.min(current_low, atr_percent)
// Ensure the table is created at the beginning
var table atr_table = table.new(position.top_center, 3, 7, bgcolor=color.white)
// Store the final year's data on the last bar
var float weighted_avg_high = na
var float weighted_avg_low = na
if barstate.islast
if not na(current_high) and not na(current_low) and current_year >= start_year
array.push(max_atr_array, current_high)
array.push(min_atr_array, current_low)
array.push(years_array, current_year)
// Ensure there's enough data for the last five years
int available_years = array.size(years_array)
if available_years > 0
var int[] last_years = array.new_int()
var float[] last_highs = array.new_float()
var float[] last_lows = array.new_float()
int last_5_count = math.min(5, available_years)
float sum_weighted_high = 0
float sum_weighted_low = 0
float total_weight = 0
for i = 0 to last_5_count - 1
int index = available_years - last_5_count + i
float max_value = array.get(max_atr_array, index)
float min_value = array.get(min_atr_array, index)
int weight = i + 1 // Recent years have higher weight
// Weighted Sum Calculation
sum_weighted_high += max_value * weight
sum_weighted_low += min_value * weight
total_weight += weight
array.push(last_years, array.get(years_array, index))
array.push(last_highs, max_value)
array.push(last_lows, min_value)
// Compute Weighted Averages
weighted_avg_high := sum_weighted_high / total_weight
weighted_avg_low := sum_weighted_low / total_weight
// Plot the Weighted Max and Min ATR% outside the table
plot(weighted_avg_high, color=color.red, title="Weighted Max ATR%", linewidth=2, style=plot.style_line)
plot(weighted_avg_low, color=color.green, title="Weighted Min ATR%", linewidth=2, style=plot.style_line)
// Function to calculate annual returns
//------------------------------------------------------
// Annual returns + current-year YTD + weighted averages
//------------------------------------------------------
// Function to calculate annual returns (~12M, completed years)
calculate_annual_returns() =>
var float[] returns = array.new_float()
var int[] years = array.new_int()
int yr = year(time)
if yr >= startYear and yr <= endYear
float annualReturn = (close - close[1]) / close[1] * 100
if not na(annualReturn)
array.push(returns, math.round(annualReturn, 2))
array.push(years, yr)
[returns, years]
//------------------------------------------------------
// Get annual returns (12M bars)
//------------------------------------------------------
[returnsArray, yearsArray] = request.security(syminfo.tickerid, "12M", calculate_annual_returns())
benchmarkTicker = input.symbol("SP:SPX", "Benchmark (SPX)")
[benchmarkReturnsArray, benchmarkYearsArray] = request.security(benchmarkTicker, "12M", calculate_annual_returns())
//------------------------------------------------------
// Detect first close of CURRENT calendar year (YTD only)
//------------------------------------------------------
int evalYear = year(time)
var float firstCloseOfYear = na
var float benchmarkFirstCloseOfYear = na
if year(time) != year(time[1])
if year(time) == evalYear
firstCloseOfYear := close
benchmarkFirstCloseOfYear := request.security(benchmarkTicker, "D", close)
// YTD return (current year only)
float ytdReturn = not na(firstCloseOfYear) ? (close - firstCloseOfYear) / firstCloseOfYear * 100 : na
float benchmarkYTDReturn = not na(benchmarkFirstCloseOfYear) ? (request.security(benchmarkTicker, "D", close) - benchmarkFirstCloseOfYear)/ benchmarkFirstCloseOfYear * 100 : na
//------------------------------------------------------
// Weighted average: completed years + current YTD once
//------------------------------------------------------
calculate_weighted_average_with_ytd(returns, years, float ytdReturn, int lookback) =>
int sz = array.size(returns)
int sy = array.size(years)
// No data yet (early history)
if sz == 0 or sy == 0
na
else
float totalWeightedReturns = 0.0
float totalWeights = 0.0
int evalYearLocal = year(time)
int limit = math.min(sz, sy)
// Completed years only
for i = 0 to limit - 1
int yr = array.get(years, i)
int age = evalYearLocal - yr
if age > 0 and age <= lookback
float weight = lookback - age
totalWeightedReturns += array.get(returns, i) * weight
totalWeights += weight
// Add CURRENT YEAR YTD once (highest weight)
bool lastIsCurrentYear = false
if sy > 0
lastIsCurrentYear := array.get(years, sy - 1) == evalYearLocal
if not na(ytdReturn) and not lastIsCurrentYear
totalWeightedReturns += ytdReturn * lookback
totalWeights += lookback
totalWeights != 0 ? totalWeightedReturns / totalWeights : na
//------------------------------------------------------
// Final weighted averages (compute ONLY on last bar)
//------------------------------------------------------
float weightedAverageMain = na
float weightedAverageBenchmark = na
if barstate.islast
weightedAverageMain := calculate_weighted_average_with_ytd(returnsArray, yearsArray, ytdReturn, 5)
weightedAverageBenchmark := calculate_weighted_average_with_ytd(benchmarkReturnsArray, benchmarkYearsArray, benchmarkYTDReturn, 5)
// Plot results
plot(weightedAverageMain, title="Weighted Average Main", color=color.blue)
plot(weightedAverageBenchmark, title="Weighted Average Benchmark", color=color.red)
// Function to calculate Min Risk with 1% cap when stock return is negative
calculate_min_risk(stockReturn, benchmarkReturn, maxATR) =>
risk = (stockReturn - benchmarkReturn) / maxATR > 1 ? (stockReturn - benchmarkReturn)/maxATR: 1
// Function to calculate Max Risk with 1% cap when stock return is negative
calculate_max_risk(stockReturn, benchmarkReturn, minATR) =>
risk = (stockReturn - benchmarkReturn) / minATR > 1 ? (stockReturn - benchmarkReturn) / minATR : 2
// Function to calculate Nasan Risk Score (Aggressive Scaling)
calculate_aggressive_risk(minRisk, maxRisk, n, m) =>
a = math.max(m, n)
b = math.min(m, n)
math.pow(minRisk,a )+ b * (math.pow(maxRisk,a) - math.pow(minRisk,a))
// Function to calculate Nasan Risk Score (Conservative Scaling)
calculate_conservative_risk(minRisk, maxRisk, n, m) =>
a = math.max(m, n)
b = math.min(m, n)
math.pow(minRisk,b )+ a * (math.pow(maxRisk,b) - math.pow(minRisk,b))
// Calculate Min Risk and Max Risk using the placeholders
minRisk = calculate_min_risk(weightedAverageMain, weightedAverageBenchmark, weighted_avg_high)
maxRisk = calculate_max_risk(weightedAverageMain, weightedAverageBenchmark, weighted_avg_low)
plot(weightedAverageMain, "Weighted Anuual Stock Return")
plot(weightedAverageBenchmark ,"Weighted Anuual Benchmark Return", color=color.white)
// Declare the risk score as float
var float nasanRiskScore = na
// Calculate Nasan Risk Score using the appropriate scaling
if scalingType == "Aggressive"
nasanRiskScore := calculate_aggressive_risk(minRisk, maxRisk, (winRate / 100), profitRatio)
else
nasanRiskScore := calculate_conservative_risk(minRisk, maxRisk, (winRate / 100), profitRatio)
// Round the Nasan Risk Score to one decimal place
nasanRiskScoreRounded = math.round(nasanRiskScore*Tolerance_, 2)
// Calculate Nsasn Ultimate Helth Index
Nor_len = input(252, "Normalization Length")
n = input(21, "Technical Length")
benchmark_ticker = benchmarkTicker
// Stock Raw Score
ema = ta.ema(hlc3, n)
// Volume Ratio Calculation
volume_ratio = ta.sma(volume, n) / ta.sma(volume, 252)
green_volume = math.sum(volume * (close > open ? 1 : 0), n)
red_volume = math.sum(volume * (close < open ? 1 : 0), n)
total_volume = math.sum(volume, n)
green_weight = green_volume / total_volume
red_weight = red_volume / total_volume
temp_weight = ema - ema[n] > 0 ? green_weight : red_weight
// Adjusted Volume Ratio
adjusted_volume_ratio = volume_ratio * temp_weight
// Stock Raw Score
raw_score = ((ema - ema[n]) * adjusted_volume_ratio) / ta.atr(n)
// Benchmark Raw Score
BenchmarkRawScore() =>
benchmark_hlc3 = hlc3
benchmark_close = close
benchmark_open = open
benchmark_volume = volume
benchmark_green_vol = math.sum(benchmark_volume * (benchmark_close > benchmark_open ? 1 : 0), n)
benchmark_red_vol = math.sum(benchmark_volume * (benchmark_close < benchmark_open ? 1 : 0), n)
benchmark_total_vol = math.sum(benchmark_volume, n)
benchmark_green_weight = benchmark_green_vol / benchmark_total_vol
benchmark_red_weight = benchmark_red_vol / benchmark_total_vol
benchmark_ema = ta.ema(hlc3, n)
benchmark_temp_weight = benchmark_ema - benchmark_ema[n] > 0 ? benchmark_green_weight : benchmark_red_weight
benchmark_volume_ratio = ta.sma(benchmark_volume, n) / ta.sma(benchmark_volume, 252)
benchmark_adjusted_volume_ratio = benchmark_volume_ratio * benchmark_temp_weight
benchmark_atr = ta.atr(n)
benchmark_raw_score = ((benchmark_ema - benchmark_ema[n]) * benchmark_adjusted_volume_ratio) / benchmark_atr
[benchmark_hlc3, benchmark_close, benchmark_open, benchmark_volume, benchmark_green_vol, benchmark_red_vol,
benchmark_total_vol, benchmark_green_weight, benchmark_red_weight, benchmark_ema, benchmark_temp_weight,
benchmark_volume_ratio, benchmark_adjusted_volume_ratio, benchmark_atr, benchmark_raw_score]
[benchmark_hlc3, benchmark_close, benchmark_open, benchmark_volume, benchmark_green_vol, benchmark_red_vol,
benchmark_total_vol, benchmark_green_weight, benchmark_red_weight, benchmark_ema, benchmark_temp_weight,
benchmark_volume_ratio, benchmark_adjusted_volume_ratio, benchmark_atr, benchmark_raw_score] = request.security(benchmark_ticker, timeframe.period, BenchmarkRawScore())
// Min-Max Normalization for Stock
min_x = ta.lowest(raw_score, Nor_len)
max_x = ta.highest(raw_score, Nor_len)
normalized_stock = (raw_score - min_x) / (max_x - min_x)
// Min-Max Normalization for Benchmark
min_y = ta.lowest(benchmark_raw_score, Nor_len)
max_y = ta.highest(benchmark_raw_score, Nor_len)
normalized_benchmark = (benchmark_raw_score - min_y) / (max_y - min_y)
// Compute ATR multiplier to scale ATR in postion size calulationw e nh
Baseline = input.float(3, "ATR Baseline Multiple")
NUHI = math.log((1 + normalized_stock) / (1 + normalized_benchmark))
tr_var = ta.stdev(ta.tr,21)
tr_var_stoch =(ta.stoch(tr_var,tr_var,tr_var,252))/100
Q = Baseline + tr_var_stoch
mul= Q - math.exp(ta.ema(NUHI,21))
plot(mul, color=color.purple)
// Calulate Position Size
PositionSize = int((capital * (nasanRiskScoreRounded / 100)) / (mul*ta.atr(21)))
// User-defined input to control the display of risk metrics
showRiskMetrics = input.bool(false, title="Show Postion Size even if Stock Return < Benchmark Return")
// Determine if risk metrics should be shown
showMetrics = showRiskMetrics or (weightedAverageMain >= weightedAverageBenchmark)
// Determine the table cell color based on the Nasan Risk Score
cellColor = nasanRiskScoreRounded > 2.0 ? color.red : nasanRiskScoreRounded < 2.0 ? color.green : color.white
// === Apply color rule ===
// User input
cpf = input.float(5.0, "Capital Prudence Factor", tooltip = "If you can't afford 2X–5X position size, don't buy it. Recommended range: 2–5")
// Final CPF selection
total_exposure = PositionSize * close * cpf
bool isValid = total_exposure < capital
color posColor = isValid ? color.green : color.red
adj= isValid? math.round(PositionSize - math.sqrt(PositionSize)) : math.round(PositionSize*capital/total_exposure)
// Function to get the cell color based on specific conditions
getMetricColor(value, threshold, aboveColor, belowColor) =>
value > threshold ? aboveColor : belowColor
// Create a single table to display all risk-related metrics
var table risk_table = table.new(position.top_right, 2, 10, border_width=1, border_color=color.blue, bgcolor=color.white)
if barstate.islast
// Header Row
table.cell(risk_table, 0, 0, "Metric", text_color=color.black, bgcolor=color.gray)
table.cell(risk_table, 1, 0, "Value", text_color=color.black, bgcolor=color.gray)
// Nasan Risk Score
if showMetrics and not na(nasanRiskScoreRounded)
table.cell(risk_table, 0, 1, "Nasan Risk Score", text_color=color.black)
table.cell(risk_table, 1, 1, str.tostring(nasanRiskScoreRounded), text_color=color.white, bgcolor=cellColor)
// Position Size
if showMetrics
table.cell(risk_table, 0, 2, "Position Size/Adj size", text_color=color.black)
table.cell(risk_table, 1, 2, str.tostring(PositionSize) + "/" + str.tostring(adj), text_color=posColor, bgcolor=color.white)
// Weighted Avg Max Drawdown
maxDrawdownColor = getMetricColor(weighted_avg_dd, 30, color.red, color.green)
table.cell(risk_table, 0, 3, "5 Year Weighted Avg Annual Max Drawdown", text_color=color.black)
table.cell(risk_table, 1, 3, str.tostring(weighted_avg_dd, "#.##"), text_color=color.white, bgcolor=maxDrawdownColor)
// Weighted Max ATR%
maxATRColor = getMetricColor(weighted_avg_high, 5, color.red, color.green)
table.cell(risk_table, 0, 4, "5 Year Weighted Avg Annual Max ATR%", text_color=color.black)
table.cell(risk_table, 1, 4, str.tostring(weighted_avg_high, "#.##"), text_color=color.white, bgcolor=maxATRColor)
// Weighted Min ATR%
minATRColor = getMetricColor(weighted_avg_low, 2, color.red, color.green)
table.cell(risk_table, 0, 5, "5 Year Weighted Avg Annual Min ATR%", text_color=color.black)
table.cell(risk_table, 1, 5, str.tostring(weighted_avg_low, "#.##"), text_color=color.white, bgcolor=minATRColor)
// Weighted Avg Stock Return vs Benchmark Return
stockVsBenchmarkColor = weightedAverageMain > weightedAverageBenchmark ? color.green : color.red
table.cell(risk_table, 0, 6, "5 Year Weighted Avg Annual Stock Return", text_color=color.black)
table.cell(risk_table, 1, 6, str.tostring(weightedAverageMain, "#.##"), text_color=color.white, bgcolor=stockVsBenchmarkColor)
// Weighted Avg Benchmark Return
benchmarkReturnColor = weightedAverageBenchmark < 10 ? color.red : color.green
table.cell(risk_table, 0, 7, "5 Year Weighted Avg Annual Benchmark Return", text_color=color.black)
table.cell(risk_table, 1, 7, str.tostring(weightedAverageBenchmark, "#.##"), text_color=color.white, bgcolor=benchmarkReturnColor)
// ATR Multiple Used
table.cell(risk_table, 0, 8, "ATR Multiple Used", text_color=color.black)
table.cell(risk_table, 1, 8, str.tostring(mul, "#.##"), text_color=color.purple)
// --- Seasonality Factor Calculation ---
seasonalityFactor = input.float(1.0, "sesonality factor") // Default neutral
// --- Seasonality-adjusted Position Size ---
seasonalityPositionSize = int(PositionSize * seasonalityFactor)
seasonalityAdj = int(adj * seasonalityFactor)
plot(adj, "PS")
// --- Add new row in risk_table ---
if barstate.islast
table.cell(risk_table, 0, 9, "Seasonality Adj Position Size", text_color=color.black)
table.cell(risk_table, 1, 9, str.tostring(seasonalityPositionSize) + "/" + str.tostring(seasonalityAdj), text_color=color.blue)
Show all 445 lines