Skip to content

Instantly share code, notes, and snippets.

@ace3
Last active December 18, 2020 05:47
Show Gist options
  • Save ace3/f9b6963c47866a2eff1b00704f341c92 to your computer and use it in GitHub Desktop.
Save ace3/f9b6963c47866a2eff1b00704f341c92 to your computer and use it in GitHub Desktop.
TamperMonkey
// ==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