Last active
December 18, 2020 05:47
-
-
Save ace3/f9b6963c47866a2eff1b00704f341c92 to your computer and use it in GitHub Desktop.
TamperMonkey
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name TradingView SQN Calculator | |
// @namespace https://ignizbot.com/ | |
// @version 0.6 | |
// @description try to take over the world! | |
// @author Ignasius Wahyudi | |
// @match https://www.tradingview.com/chart/*/ | |
// @require https://unpkg.com/[email protected]/dist/math.min.js | |
// @require https://cdnjs.cloudflare.com/ajax/libs/date-fns/1.30.1/date_fns.min.js | |
// @require https://cdnjs.cloudflare.com/ajax/libs/sweetalert/2.1.2/sweetalert.min.js | |
// @require https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.26.0/moment.min.js | |
// @grant none | |
// ==/UserScript== | |
// declaring functions | |
function exists(keys) { | |
let key, parent = window; | |
for (let i = 0; i < keys.length; i++) { | |
key = keys[i]; | |
if (!parent.hasOwnProperty(key)) { | |
return; | |
} | |
parent = parent[key]; | |
} | |
return parent; | |
} | |
function output(overview, performance, trades, inputs, properties) { | |
let ret = []; | |
ret.push(inputs.headings.join("\t")); | |
ret.push(inputs.items.join("\t")); | |
ret.push(""); | |
ret.push(properties.headings.join("\t")); | |
ret.push(properties.items.join("\t")); | |
ret.push(""); | |
ret.push(overview.headings.join("\t")); | |
ret.push(overview.items.join("\t")); | |
ret.push(""); | |
performance.items = performance.items.map(tab); | |
ret.push(performance.headings.join("\t")); | |
ret = ret.concat(performance.items); | |
ret.push(""); | |
trades.items = trades.items.map(tab); | |
ret.push(trades.headings.join("\t")); | |
ret = ret.concat(trades.items); | |
return ret.join("\n"); | |
} | |
function tab(array) { | |
return array.join("\t"); | |
} | |
function type(k) { | |
const a = {"le":"Entry Long","lx":"Exit Long","se":"Entry Short","sx":"Exit Short"}; | |
return a.hasOwnProperty(k) ? a[k] : undefined; | |
} | |
function tz(n) { | |
return n.replace(/([0-9]+(\.[0-9]+[1-9])?)(\.?0+$)/, "$1") | |
} | |
function mv(a, k) { | |
if (typeof k === "function") { | |
return a.push(k()); | |
} | |
if (typeof k === "string") { | |
const reg = /^(\$)?(.+?)(?:\.(\d+))?(%)?$/; | |
const res = reg.exec(k); | |
if (!a.hasOwnProperty(res[2])) { | |
return; | |
} | |
res[2] = a[res[2]]; | |
res[3] = +res[3] || 0; | |
if (res[2] === null) { | |
return "N/A"; | |
} | |
if (res[4] === "%") { | |
// res[2] *= 100; | |
res[3] += 2; | |
} | |
if (k === "profitFactor" && res[2] < 1) { | |
res[2] *= -1; | |
} | |
res[1] = ""; // Drop $ | |
res[4] = ""; // Drop % | |
return res[1] + tz(res[2].toFixed(res[3])) + res[4]; | |
} | |
throw new TypeError("Unsupported type: " + typeof k); | |
} | |
function dt(timestamp) { | |
const D = new Date(timestamp); | |
const d = [D.getFullYear(), p(D.getMonth() + 1), p(D.getDate())].join("-"); | |
const t = [p(D.getHours()), p(D.getMinutes()), p(D.getSeconds())].join(":"); | |
return d + " " + t; | |
} | |
function p(x) { | |
return (x.length === 1 || x < 10) ? "0" + x : x; | |
} | |
function clipboard(format, data) { | |
const tmp = document.oncopy; | |
document.oncopy = function clipboard_oncopy(e) { | |
e.clipboardData.setData(format, data); | |
e.preventDefault(); | |
}; | |
document.execCommand("copy", false, null); | |
alert("Copied to Clipboard"); | |
document.oncopy = tmp; | |
} | |
function sqn_calculate(){ | |
let i, k, m, s, v; | |
const rw = exists("TradingView.bottomWidgetBar._widgets.backtesting._reportWidgetsSet.reportWidget".split(".")) | |
if (!rw) { | |
console.info("Still loading..."); | |
return; | |
} | |
const data = rw._data; | |
const cd = rw._report._currencyDecimals || 2 | |
const dd = rw._report._defaultDecimals || 3 | |
const pd = rw._report._percentDecimals || 2 | |
const sd = rw._report._seriesDecimals || 2 | |
let overview = { | |
headings: [], | |
items: [] | |
}; | |
m = [ | |
["Net Profit", "$netProfit." + sd], | |
["Total Closed Trades", "totalTrades"], | |
["Percent Profitable", "percentProfitable." + pd + "%"], | |
["Profit Factor", "profitFactor." + dd], | |
["Max Drawdown", null], | |
["Avg Trade", "$avgTrade." + sd], | |
["Avg # Bars in Trades", "avgBarsInTrade"] | |
]; | |
for (i = 0; i < m.length; i++) { | |
v = m[i]; | |
overview.headings.push(v[0]); | |
if (v[0] === "Max Drawdown") { | |
overview.items.push(mv(data.performance, "$maxStrategyDrawDown." + sd)); | |
} else { | |
overview.items.push(mv(data.performance.all, v[1])); | |
} | |
} | |
const p = data.performance; | |
let performance = { | |
headings: ["", "All", "Long", "Short"], | |
items: [] | |
}; | |
m = [ | |
["Net Profit", "$netProfit." + sd], | |
["Gross Profit", "$grossProfit." + sd], | |
["Gross Loss", "$grossLoss." + sd], | |
["Max Drawdown", null], | |
["Buy & Hold Return", null], | |
["Sharpe Ratio", null], | |
["Profit Factor", "profitFactor." + dd], | |
["Max Contracts Held", "maxContractsHeld"], | |
["Open PL", null], | |
["Commission Paid", null], | |
["Total Closed Trades", "totalTrades"], | |
["Total Open Trades", "totalOpenTrades"], | |
["Number Winning Trades", "numberOfWiningTrades"], | |
["Number Losing Trades", "numberOfLosingTrades"], | |
["Percent Profitable", "percentProfitable." + pd + "%"], | |
["Avg Trade", "$avgTrade." + sd], | |
["Avg Win Trade", "$avgWinTrade." + sd], | |
["Avg Loss Trade", "$avgLosTrade." + sd], | |
["Ratio Avg Win / Avg Loss", "ratioAvgWinAvgLoss." + dd], | |
["Largest Winning Trade", "$largestWinTrade." + sd], | |
["Largest Losing Trade", "$largestLosTrade." + sd], | |
["Avg # Bars in Trades", "avgBarsInTrade"], | |
["Avg # Bars in Winning Trades", "avgBarsInWinTrade"], | |
["Avg # Bars in Losing Trade", "avgBarsInLossTrade"] | |
]; | |
for (i = 0; i < m.length; i++) { | |
v = m[i]; | |
switch (v[0]) { | |
case "Buy & Hold Return": | |
performance.items.push([v[0], mv(p, "$buyHoldReturn." + cd), mv(p, "buyHoldReturnPercent." + pd + "%")]); | |
break; | |
case "Commission Paid": | |
performance.items.push([v[0], mv(p.all, "$commissionPaid." + cd)]); | |
break; | |
case "Max Drawdown": | |
performance.items.push([v[0], mv(p, "$maxStrategyDrawDown." + cd)]); | |
break; | |
case "Open PL": | |
performance.items.push([v[0], mv(p, "$openPL." + cd), mv(p, "openPLPercent." + pd + "%")]); | |
break; | |
case "Sharpe Ratio": | |
performance.items.push([v[0], mv(p, "sharpeRatio.3")]); | |
break; | |
default: | |
performance.items.push([v[0], mv(p.all, v[1]), mv(p.long, v[1]), mv(p.short, v[1])]); | |
} | |
} | |
const t = data.trades; | |
let trades = { | |
headings: "Trade #,Type,Signal,Date/Time,Price,Contracts,Profit,Profit %,Cum. Profit,Cum. Profit %,Run-up,Run-up %,Drawdown,Drawdown %".split(","), | |
items: [] | |
}; | |
for (i = 0; i < t.length; i++) { | |
v = t[i]; | |
const vd = "$v." + sd | |
const pp = "p." + pd + "%" | |
trades.items.push([i + 1, type(v.e.tp), v.e.c, dt(v.e.tm), mv(v.e, "$p." + sd), , , , , , , , , ]); | |
trades.items.push(v.x.c.length | |
? [, type(v.x.tp), v.x.c, dt(v.x.tm), mv(v.x, "$p." + sd), v.q, mv(v.tp, vd), mv(v.tp, pp), mv(v.cp, vd), mv(v.cp, pp), mv(v.rn, vd), mv(v.rn, pp), mv(v.dd, vd), mv(v.dd, pp)] | |
: [, type(v.x.tp), "Open", , , , , , , , , , , ] | |
); | |
} | |
m = [ | |
["Initial Capital", "initial_capital"], | |
["Base Currency", "currency"], | |
["Allow Up To Orders", "pyramiding"], | |
["Order Size", "default_qty_value"], | |
["Order Type", "default_qty_type"], | |
["Recalculate After Order filled", "calc_on_order_fills"], | |
["Recalculate On Every Tick", "calc_on_every_tick"], | |
["Verify Price For Limit Orders", "backtest_fill_limits_assumption"], | |
["Slippage", "slippage"], | |
["Commission", "commission_value"], | |
["Commission Type", "commission_type"], | |
]; | |
const mqty = { | |
"fixed": "Contracts", | |
"cash_per_order": data.currency || "USD", | |
"percent_of_equity": "% of equity" | |
}; | |
let inputs = { | |
headings: [], | |
items: [] | |
}; | |
let properties = { | |
headings: [], | |
items: [] | |
}; | |
// inputs/properties: window.initData.content.charts[#].panes[#].sources[#.type=StudyStrategy].metaInfo.inputs | |
// isHidden = false | |
// .name => .defval | |
const chart = 0; | |
const pane = 0; | |
const sources = window.initData.content.charts[chart].panes[pane].sources; | |
let props = {}; | |
sources.forEach((source) => { | |
if (source.type === "StudyStrategy") { | |
const values = rw._strategy._properties.inputs || {}; | |
source.metaInfo.inputs.forEach((input) => { | |
if (input.isHidden) { | |
return false; | |
} | |
const value = values.hasOwnProperty(input.id) ? values[input.id]._value : input.defval; | |
if (input.hasOwnProperty("groupId")) { | |
if (input.groupId === "strategy_props") { | |
props[input.internalID] = value; | |
} | |
} else { | |
inputs.headings.push(input.name); | |
inputs.items.push(value); | |
} | |
}); | |
} | |
}); | |
for (i = 0; i < m.length; i++) { | |
v = m[i]; | |
s = v[0]; | |
k = v[1]; | |
v = props[k]; | |
if (k === "currency" && v === "NONE") { | |
v = "Default"; | |
} | |
else if (k === "default_qty_type") { | |
v = mqty[v]; | |
} | |
if (k === "pyramiding") { | |
properties.headings.push("Pyramiding"); | |
properties.items.push(v ? "true" : "false"); | |
} | |
properties.headings.push(s); | |
properties.items.push(v); | |
} | |
const result = {overview, performance, trades, inputs, properties}; | |
// Change This | |
let maxDrawDown = 25 | |
let isKellyOverride = false | |
const kellyOverride = 25 | |
const rawPercentagePL = [] | |
const runUpShort = [] | |
const runUpLong = [] | |
const datas = trades.items | |
let firstTrade = new Date(datas[0][3]) | |
let lastLog = datas[datas.length-1]; | |
if(!lastLog[0]){ | |
lastLog = datas[datas.length-2]; | |
} | |
let lastTrade = new Date(lastLog[3]) | |
for (let i = 0; i < datas.length; i++) { | |
if (!datas[i][0] && datas[i][3] !== null) { | |
if(datas[i][7]){ | |
rawPercentagePL.push(datas[i][7]) | |
} | |
if (datas[i][1] === 'Exit Short') { | |
runUpShort.push(parseFloat(datas[i][11])) | |
} else if (datas[i][1] === 'Exit Long') { | |
runUpLong.push(parseFloat(datas[i][11])) | |
} | |
} | |
} | |
let avgUpPercentageShort = 0 | |
for (const short of runUpShort) { | |
avgUpPercentageShort = avgUpPercentageShort + parseFloat(short) | |
} | |
avgUpPercentageShort = avgUpPercentageShort / runUpShort.length | |
let avgUpPercentageLong = 0 | |
for (const long of runUpLong) { | |
avgUpPercentageLong = avgUpPercentageLong + parseFloat(long) | |
} | |
avgUpPercentageLong = avgUpPercentageLong / runUpLong.length | |
let dateDiff = dateFns.differenceInMonths(firstTrade, lastTrade) | |
dateDiff = Math.abs(dateDiff) | |
// #2 Sorted the Profit and Loss | |
const sortedPercentageProfit = [] | |
const sortedPercentageLoss = [] | |
for (const rawPercentage of rawPercentagePL) { | |
if (parseFloat(rawPercentage) < 0) { | |
sortedPercentageLoss.push(parseFloat(rawPercentage)) | |
} else { | |
sortedPercentageProfit.push(parseFloat(rawPercentage)) | |
} | |
} | |
sortedPercentageLoss.sort(function (a, b) { | |
return b - a | |
}) | |
sortedPercentageProfit.sort(function (a, b) { | |
return a - b | |
}) | |
// #3 Get Average Loss Percentage | |
const avgLossPercentage = Math.abs(math.mean(sortedPercentageLoss)) | |
// #4 Multi R Sorted | |
const sortedMultiRProfit = [] | |
const sortedMultiRLoss = [] | |
for (const sortedLoss of sortedPercentageLoss) { | |
const calculateRLoss = parseFloat(sortedLoss / avgLossPercentage) | |
sortedMultiRLoss.push(parseFloat(calculateRLoss)) | |
} | |
sortedMultiRLoss.sort(function (a, b) { | |
return b - a | |
}) | |
for (const sortedProfit of sortedPercentageProfit) { | |
const calculateRProfit = parseFloat(sortedProfit / avgLossPercentage) | |
sortedMultiRProfit.push(parseFloat(calculateRProfit)) | |
} | |
sortedMultiRProfit.sort(function (a, b) { | |
return a - b | |
}) | |
const sortedMultiRCombined = sortedMultiRLoss.concat(sortedMultiRProfit) | |
const numberOfTrades = sortedMultiRCombined.length | |
console.log({ numberOfTrades }) | |
const avgNumberOfTrades = Math.floor((12 / dateDiff) * numberOfTrades) | |
// #5 Count Kelly | |
const avgProfitR = parseFloat(math.mean(sortedMultiRProfit)) | |
const avgLossR = parseFloat(math.mean(sortedMultiRLoss)) | |
const avgTotalR = parseFloat(math.mean(sortedMultiRCombined)) | |
console.log({avgProfitR,avgLossR}); | |
const rKelly = parseFloat(Math.abs(avgProfitR / avgLossR)) | |
const sumProfitR = parseFloat(math.sum(sortedMultiRProfit)) | |
const sumLossR = parseFloat(math.sum(sortedMultiRLoss)) | |
const sumMultipleR = parseFloat(math.sum(sortedMultiRCombined)) | |
const stdev = parseFloat(math.std(sortedMultiRCombined)) | |
let sqn = parseFloat( | |
(avgTotalR / stdev) * | |
Math.sqrt( | |
sortedMultiRCombined.length > 100 ? 100 : sortedMultiRCombined.length | |
) | |
) | |
sqn = parseFloat(sqn) | |
let sqnWording = '' | |
if (parseFloat(sqn) < 1.0) { | |
sqnWording = 'Probably very hard to Trade' | |
} else if (parseFloat(sqn) > 1.01 && parseFloat(sqn) <= 2.0) { | |
sqnWording = | |
'Average System' | |
} else if (parseFloat(sqn) > 2.01 && parseFloat(sqn) <= 3.0) { | |
sqnWording = 'Good System' | |
} else if (parseFloat(sqn) > 3.01 && parseFloat(sqn) <= 5.0) { | |
sqnWording = 'Excellent System' | |
} else if (parseFloat(sqn) > 5.01 && parseFloat(sqn) <= 7.0) { | |
sqnWording = 'Superb System' | |
} else if (parseFloat(sqn) > 7.01) { | |
sqnWording = 'Holy Grail System' | |
} else { | |
sqnWording = '' | |
} | |
const goodStdev = parseFloat(avgTotalR / stdev) | |
const percentWinRate = parseFloat(result.performance.items[14][1]) | |
console.log({ percentWinRate }) | |
const oneMinusWinRate = parseFloat(1 - percentWinRate) | |
const winMinusLossRate = parseFloat(percentWinRate - oneMinusWinRate) | |
let kelly80 = parseFloat( | |
(percentWinRate - oneMinusWinRate / rKelly) * (80 / 100) | |
) //(G21-(G22/G8))*80% | |
if (isKellyOverride) { | |
kelly80 = parseFloat(kellyOverride) | |
} | |
console.log({ kelly80 }) | |
const amountRisk = parseFloat(kelly80 * avgLossPercentage) | |
const coeficient = parseFloat((stdev / avgTotalR / 100) * 100) | |
// #6 Risk of Ruin | |
const avgWin = parseFloat(amountRisk * rKelly) | |
const avgLoss = parseFloat(amountRisk) | |
const probWin = parseFloat(percentWinRate) | |
const probLoss = parseFloat(oneMinusWinRate) | |
const maxRisk = parseFloat(maxDrawDown / 100) | |
console.log({ probWin, avgWin, probLoss, avgLoss }) | |
let z = parseFloat(probWin * avgWin - probLoss * avgLoss) | |
let a = parseFloat( | |
math.pow( | |
probWin * math.pow(avgWin, 2) + probLoss * math.pow(avgLoss, 2), | |
1 / 2 | |
) | |
) | |
let pr = parseFloat(0.5 * (1 + z / a)) | |
let riskOfRuin = parseFloat( | |
parseFloat(math.pow((1 - pr) / pr, maxRisk / a) * 100) | |
) | |
const expectedReturnPerYear = avgTotalR * avgNumberOfTrades * amountRisk | |
const message = ` | |
SQN: ${sqn.toFixed(2)} | |
Good STDEV: ${goodStdev.toFixed(2)} | |
RoR: ${riskOfRuin.toFixed(2)}% | |
Best Kelly: ${kelly80.toFixed(2)} | |
` | |
/* const inlineSqn = `SQN: ${sqn.toFixed(2)} | Good STDEV: ${goodStdev.toFixed(2)} | RoR: ${riskOfRuin.toFixed(2)}% | Best Kelly: ${kelly80.toFixed(2)}`; | |
let lil; | |
let ele = document.querySelector("div.backtesting-select-wrapper"); | |
if(ele){ | |
ele = ele.querySelector("ul.report-tabs"); | |
if(ele){ | |
lil = ele.querySelector("li.text-sqn"); | |
lil.innerHTML = inlineSqn; | |
} | |
} */ | |
swal(sqnWording.replace(/\n/g, '\n\n'),message) | |
} | |
function sqn_init() { | |
let element, li | |
let elementSQN | |
element = document.querySelector('div.backtesting-select-wrapper') | |
if (element) { | |
element = element.querySelector('ul.report-tabs') | |
if (element) { | |
li = element.querySelector('li.ignizbot-sqn') | |
if (!li) { | |
li = document.createElement('li') | |
li.addEventListener('click', sqn_calculate) | |
li.classList.add('ignizbot-sqn') | |
li.innerHTML = 'SQN' | |
element.appendChild(li) | |
} | |
} | |
/* elementSQN = element.querySelector('li.text-sqn') | |
if (!elementSQN) { | |
li = document.createElement('li') | |
li.classList.add('text-sqn') | |
li.innerHTML = '' | |
element.appendChild(li) | |
} */ | |
} | |
setTimeout(sqn_init, 1000) | |
} | |
(function() { | |
sqn_init(); | |
// setInterval(sqn_calculate,1000); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment