Skip to content

Instantly share code, notes, and snippets.

@iCodeForBananas
Last active March 14, 2025 09:55
Show Gist options
  • Select an option

  • Save iCodeForBananas/59555eff470dae1223720446a1fb8d18 to your computer and use it in GitHub Desktop.

Select an option

Save iCodeForBananas/59555eff470dae1223720446a1fb8d18 to your computer and use it in GitHub Desktop.
Lets you trade off of replay mode so that you can keep track of practice sessions. Use one pane to trade from, and keep your mouse off the chart because I have to grab the close price from the HTML that's related to where the mouse hovers..
// ==UserScript==
// @name Practice Trading
// @version 12
// @description Lets you trade off of replay mode so that you can keep track of practice sessions.
// @author iCodeForBananas
// @match https://www.tradingview.com/chart/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=tradingview.com
// @grant none
// ==/UserScript==
(function ($) {
"use strict";
class PracticeTrading {
tradeHistory = [];
activeTrade = null;
startingNetValue = 0;
init() {
$("body").append(`
<style>
#practice-trading-toolbar {
background: #131722;
top: 0;
right: 0;
width: 280px;
bottom: 0;
text-align: center;
border-radius: 0;
box-shadow: none;
}
.practice-trading-table {
}
.practice-trading-table td,
.practice-trading-table th {
padding: 7px;
font-weight: 400;
text-align: center;
width: 25%;
}
.practice-trading-table th {
color: #787b86;
}
.practice-trading-button {
cursor: pointer;
align-items: center;
border-radius: 4px;
border-style: solid;
border-width: 1px;
box-sizing: border-box;
display: flex;
font-size: 15px;
justify-content: center;
outline: 0;
padding: 15px 7px;
position: relative;
width: 100%;
}
.practice-trading-button-blue {
background-color: #2962ff;
border-color: #2962ff;
color: #e3effd;
}
.practice-trading-button-yellow {
background-color: #ffeb3b;
border-color: #ffeb3b;
color: #e3effd;
}
.practice-trading-button-default {
background-color: #131722;
border-color: #50535e;
color: #b2b5be;
}
.practice-trading-button-red {
background-color: #f23645;
border-color: #f23645;
color: #ffebec;
}
.practice-trading-button-small {
background-color: #131722;
border-color: #50535e;
color: #b2b5be;
padding: 7px;
font-size: 13px;
}
.practice-trading-button-disabled {
background-color: #1e222d !important;
border-color: #50535e !important;
color: #50535e !important;
}
</style>
<div class="tv-floating-toolbar" id="practice-trading-toolbar">
<table class="practice-trading-table">
<tr style="border-bottom: 1px solid #2a2e39">
<td colspan="4">
<button id="practice-trading-random-date-action" class="practice-trading-button-default practice-trading-button" style="border: 0" title="Auto opens go to date and starts the replay function so you can just click and go">Go to Date and Start Replay</button>
</td>
</tr>
<tr>
<th title="Win rate (win count / all trades)">%</th>
<th title="Profit factor (gross profit / gross losses)">PF</td>
<th title="Profit ratio (average winner / average loser)">PR</th>
<th title="Number of trades">#</th>
</tr>
<tr style="border-bottom: 1px solid #2a2e39">
<td id="practice-trading-win-rate">--</td>
<td id="practice-trading-profit-factor">--</td>
<td id="practice-trading-profit-ratio">--</td>
<td id="practice-trading-trade-count">--</td>
</tr>
<tr>
<th title="Average winner" colspan="2">Avg Win</td>
<th title="Average loser" colspan="2">Avg Loss</th>
</tr>
<tr style="border-bottom: 1px solid #2a2e39">
<td id="practice-trading-avg-win" colspan="2">--</td>
<td id="practice-trading-avg-loss" colspan="2">--</td>
</tr>
<tr style="border-bottom: 1px solid #2a2e39" title="The net equity of your paper account starting from 0">
<th colspan="2">Net</th>
<td id="practice-trading-net-value" colspan="4">--</td>
</tr>
<tr style="border-bottom: 1px solid #2a2e39">
<th colspan="2" title="Number of stock to buy per trade">Qty</th>
<td id="practice-trading-net-value" colspan="2">
<input type="number" min="1" id="practice-trading-quantity" value="1" style="width: 100%; border: none; color: black; border-radius: 3px; text-align: center;" />
</td>
</tr>
<tr style="border-bottom: 1px solid #2a2e39">
<th colspan="2" title="Amount of profit before the trade will be closed">Take Profit</th>
<td id="practice-trading-net-value" colspan="2">
<input type="number" min="0" id="practice-trading-take-profit" value="0" style="width: 100%; border: none; color: black; border-radius: 3px; text-align: center;" />
</td>
</tr>
<tr style="border-bottom: 1px solid #2a2e39">
<th colspan="2" title="Amount of loss before the trade will be closed">Stop Loss</th>
<td id="practice-trading-net-value" colspan="2">
<input type="number" max="0" id="practice-trading-stop-loss" value="0" style="width: 100%; border: none; color: black; border-radius: 3px; text-align: center;" />
</td>
</tr>
<tr style="border-bottom: 1px solid #2a2e39;">
<td colspan="2" style="padding: 0">
<button id="practice-trading-buy-action" class="practice-trading-button-blue practice-trading-button" style="border-radius: 0;">Buy</button>
</td>
<td colspan="2" style="padding: 0">
<button id="practice-trading-sell-action" class="practice-trading-button-red practice-trading-button" style="border-radius: 0;">Sell</button>
</td>
</tr>
<tr style="border-top: 1px solid #2a2e39">
<th>
Side
</th>
<th>
Entry
</th>
<th>
Exit
</th>
<th>
Profit
</th>
</tr>
<tr id="practice-trading-active-trade-body" style="border-bottom: 1px solid #2a2e39" title="Current active trade, if no values present no trade is on.">
<td id="practice-trading-active-trade-side">--</td>
<td id="practice-trading-active-trade-price">--</td>
<td>--</td>
<td id="practice-trading-active-trade-profit">--</td>
</tr>
</table>
<button style="border-radius: 0; position: absolute; bottom: 0; border-left: 0; border-right: 0; border-bottom: 0; width: 100%;"id="practice-trading-reset-action" class="practice-trading-button-default practice-trading-button" style="border: 0">Reset History</button>
</div>
`);
try {
this.tradeHistory = window.localStorage.getItem("tradeHistory")
? JSON.parse(window.localStorage.getItem("tradeHistory"))
: [];
} catch (e) {
this.tradeHistory = [];
}
this.renderTradeHistory();
this.calculateStats();
$("#practice-trading-buy-action").on("click", this.handleBuy.bind(this));
$("#practice-trading-sell-action").on("click", this.handleSell.bind(this));
$("#practice-trading-reset-action").on("click", this.resetTradeHistory.bind(this));
$("#practice-trading-random-date-action").on("click", this.goToRandomDate.bind(this));
$(".js-rootresizer__contents.layout-with-border-radius").css("width", "calc(100% - 286px)");
window.dispatchEvent(new Event("resize"));
this.hideActiveTrade();
if (window.location.href.includes("random")) {
this.goToRandomDate();
}
this.monitorActiveTradeProfit();
}
monitorActiveTradeProfit() {
setInterval(() => {
if (this.activeTrade) {
const lastProfit = this.activeTrade.profit ? this.activeTrade.profit : 0;
if (this.activeTrade && this.activeTrade.direction === -1) {
this.activeTrade.profit = this.activeTrade.entryPrice - this.getPrice();
}
if (this.activeTrade && this.activeTrade.direction === 1) {
this.activeTrade.profit = this.getPrice() - this.activeTrade.entryPrice;
}
if (lastProfit !== this.activeTrade.profit) {
console.log(" this.activeTrade", this.activeTrade);
this.activeTrade.barCount = isNaN(this.activeTrade.barCount) ? 1 : this.activeTrade.barCount + 1;
console.log(" this.activeTrade", this.activeTrade);
console.log(this.activeTrade.barCount);
}
const stopLossAmount = parseFloat(document.querySelector("#practice-trading-stop-loss").value);
const takeProfitAmount = parseFloat(document.querySelector("#practice-trading-take-profit").value);
if (stopLossAmount !== 0 && this.activeTrade && this.activeTrade.profit <= stopLossAmount) {
this.activeTrade.direction === -1
? this.handleBuy(null, stopLossAmount)
: this.handleSell(null, stopLossAmount);
}
if (takeProfitAmount !== 0 && this.activeTrade && this.activeTrade.profit >= takeProfitAmount) {
this.activeTrade.direction === 1
? this.handleSell(null, takeProfitAmount)
: this.handleBuy(null, takeProfitAmount);
}
}
let tradeProfit = this.activeTrade ? (this.activeTrade.profit * this.activeTrade.quantity).toFixed(2) : 0;
$("#practice-trading-active-trade-profit").html(`
${
this.activeTrade && this.activeTrade.profit < 0
? "<span style='color: #f23645'>" + tradeProfit + "</span>"
: ""
}
${
this.activeTrade && this.activeTrade.profit > 0
? "<span style='color: #089981'>+" + tradeProfit + "</span>"
: ""
}
${
this.activeTrade && this.activeTrade.profit === 0
? "<span style='color: #787b86'>" + tradeProfit + "</span>"
: ""
}
${!this.activeTrade ? "--" : ""}
`);
}, 100);
}
waitForElm(selector, cb) {
console.log(selector, cb);
let intervalHandle;
intervalHandle = setInterval(() => {
if (document.querySelector(selector)) {
cb.call(this);
console.log(document.querySelector(selector));
window.clearInterval(intervalHandle);
}
}, 1000);
}
goToRandomDate() {
setTimeout(() => {
$("[data-name='go-to-date']").click();
setTimeout(() => {
$("[data-name='go-to-date-dialog'] input:first-child").first().focus();
setTimeout(() => {
let intervalHandle;
intervalHandle = setInterval(() => {
// $("[data-name='go-to-date-dialog'] input:first-child").first().val(randomDateString);
if (!$("[data-name='go-to-date-dialog'] input:first-child").length) {
$("#header-toolbar-replay").click();
$("#tv-replay-toolbar__button").click();
clearInterval(intervalHandle);
}
}, 200);
}, 500);
}, 500);
}, 1000);
}
hideActiveTrade() {
$("#practice-trading-active-trade-side").html("--");
$("#practice-trading-active-trade-price").text("--");
}
getXPath(STR_XPATH) {
var xresult = document.evaluate(STR_XPATH, document, null, XPathResult.ANY_TYPE, null);
var xnodes = [];
var xres;
while ((xres = xresult.iterateNext())) {
xnodes.push(xres);
}
return xnodes;
}
getPrice() {
return parseFloat(
document.querySelector(
"[data-name='legend-series-item'] > div:last-child > div > div:nth-child(5) > div:last-child"
).innerText
).toFixed(2);
// const secondPaneXPath =
// "/html/body/div[2]/div[1]/div[3]/div[1]/div/table/tr[1]/td[2]/div/div[1]/div[1]/div[1]/div[2]/div/div[5]/div[2]";
// try {
// const nodes = this.getXPath(secondPaneXPath);
// return parseFloat(nodes[0].innerText).toFixed(2);
// } catch (e) {
// console.error("Unable to get price from last element in OHLC status bar.");
// }
}
getBidPrice() {
return this.getPrice();
}
getAskPrice() {
return this.getPrice();
}
calculateStats() {
const wins = this.tradeHistory.filter((trade) => {
return (
(trade.direction < 0 && trade.entryPrice > trade.exitPrice) ||
(trade.direction > 0 && trade.exitPrice > trade.entryPrice)
);
}).length;
const winRateDecimal = parseInt((wins / this.tradeHistory.length) * 100);
const winRate = this.tradeHistory.length > 0 && wins > 0 ? String(winRateDecimal) + "%" : "--";
let grossWinAmount = 0;
let grossLossAmount = 0;
this.tradeHistory.forEach((trade) => {
let tradeProfit = trade.profit;
if (tradeProfit < 0) {
grossLossAmount += tradeProfit;
}
if (tradeProfit > 0) {
grossWinAmount += tradeProfit;
}
});
grossLossAmount = grossLossAmount === 0 ? 0 : grossLossAmount;
const profitFactor =
this.tradeHistory.length > 0 && grossLossAmount !== 0
? Math.abs(parseFloat(grossWinAmount / grossLossAmount).toFixed(2))
: "--";
const avgLoss =
this.tradeHistory.length - wins > 0 && this.tradeHistory.length - wins > 0
? grossLossAmount / (this.tradeHistory.length - wins)
: 0;
const avgWin = this.tradeHistory.length > 0 && wins > 0 ? grossWinAmount / wins : 0;
console.log("avgLoss", avgLoss);
console.log("avgWin", avgWin);
console.log("wins", wins);
console.log("losers", this.tradeHistory.length - wins);
const profitRatio =
this.tradeHistory.length > 0 && avgLoss !== 0
? Math.abs(parseFloat(avgWin) / parseFloat(avgLoss)).toFixed(2)
: "--";
console.log("grossWinAmount", grossWinAmount);
console.log("grossLossAmount", grossLossAmount);
const tradeCount = this.tradeHistory.length > 0 ? this.tradeHistory.length : "--";
const netValue = this.startingNetValue + grossLossAmount + grossWinAmount;
console.log("netValue", netValue);
$("#practice-trading-avg-win").text(avgWin.toFixed(2));
$("#practice-trading-avg-loss").text(avgLoss.toFixed(2));
$("#practice-trading-trade-count").text(tradeCount);
$("#practice-trading-win-rate").text(winRate);
$("#practice-trading-profit-factor").text(profitFactor);
$("#practice-trading-profit-ratio").text(profitRatio);
$("#practice-trading-net-value").text(netValue.toFixed(2));
}
saveTradeHistory() {
try {
window.localStorage.setItem("tradeHistory", JSON.stringify(this.tradeHistory));
} catch (e) {
window.localStorage.setItem("tradeHistory", JSON.stringify([]));
}
}
resetTradeHistory() {
window.localStorage.setItem("tradeHistory", "[]");
this.tradeHistory = [];
this.renderTradeHistory();
this.calculateStats();
}
// buy the ask
handleBuy(event, profitOverride) {
const price = this.getAskPrice();
if (!price) {
console.error("Unable to get ask price.");
return;
}
if (this.activeTrade && this.activeTrade.direction < 0) {
this.activeTrade.exitPrice = price;
this.calculateProfit(profitOverride);
this.tradeHistory.push(this.activeTrade);
this.activeTrade = null;
this.calculateStats();
this.hideActiveTrade();
$("#practice-trading-sell-action").removeClass("practice-trading-button-disabled");
this.renderTradeHistory();
this.saveTradeHistory();
return;
}
if (!this.activeTrade) {
this.activeTrade = {
symbol: document.querySelector("#header-toolbar-symbol-search").innerText,
id: Date.now(),
barCount: 0,
profit: 0,
direction: 1,
quantity: document.querySelector("#practice-trading-quantity").value,
entryPrice: price,
exitPrice: null,
};
$("#practice-trading-active-trade-side").html(
`<span style='color: #2962ff'>Long (${this.activeTrade.quantity})</span>`
);
$("#practice-trading-active-trade-price").text(this.getAskPrice());
$("#practice-trading-buy-action").addClass("practice-trading-button-disabled");
}
}
// sell the bid
handleSell(event, profitOverride) {
const price = this.getBidPrice();
if (!price) {
console.error("Unable to get bid price.");
return;
}
if (this.activeTrade && this.activeTrade.direction > 0) {
this.activeTrade.exitPrice = price;
this.calculateProfit(profitOverride);
this.tradeHistory.push(this.activeTrade);
this.activeTrade = null;
this.calculateStats();
this.hideActiveTrade();
$("#practice-trading-buy-action").removeClass("practice-trading-button-disabled");
this.renderTradeHistory();
this.saveTradeHistory();
return;
}
if (!this.activeTrade) {
this.activeTrade = {
symbol: document.querySelector("#header-toolbar-symbol-search").innerText,
id: Date.now(),
barCount: 0,
quantity: document.querySelector("#practice-trading-quantity").value,
profit: 0,
direction: -1,
entryPrice: price,
exitPrice: null,
};
$("#practice-trading-active-trade-side").html(
`<span style='color: #f23645'>Short (${this.activeTrade.quantity})</span>`
);
$("#practice-trading-active-trade-price").text(this.getAskPrice());
$("#practice-trading-sell-action").addClass("practice-trading-button-disabled");
}
}
calculateProfit(profitOverride) {
console.log(profitOverride);
if (this.activeTrade.direction === -1 && !profitOverride) {
this.activeTrade.profit =
(this.activeTrade.entryPrice - this.activeTrade.exitPrice) * this.activeTrade.quantity;
}
if (this.activeTrade.direction === -1 && profitOverride) {
this.activeTrade.profit = profitOverride * this.activeTrade.quantity;
}
if (this.activeTrade.direction === 1 && !profitOverride) {
this.activeTrade.profit =
(this.activeTrade.exitPrice - this.activeTrade.entryPrice) * this.activeTrade.quantity;
}
if (this.activeTrade.direction === 1 && profitOverride) {
this.activeTrade.profit = profitOverride * this.activeTrade.quantity;
}
}
renderTradeHistory() {
$(".practice-trading-trade-history-row").remove();
let tradeHTML = ``;
this.tradeHistory.sort((a, b) => a.id - b.id);
console.log(this.tradeHistory);
tradeHTML += this.tradeHistory.reverse().map((trade) => {
let tradeProfit = trade.profit;
return `
<tr class="practice-trading-trade-history-row" style="border-bottom: 1px solid #2a2e39">
<td>
${
trade.direction === 1
? `<span style='color: #2962ff'>Long (${trade.quantity})</span>`
: `<span style='color: #f23645'>Short (${trade.quantity})</span>`
}
</td>
<td>
${trade.entryPrice ? parseFloat(trade.entryPrice).toFixed(2) : "--"}
</td>
<td>
${trade.exitPrice ? parseFloat(trade.exitPrice).toFixed(2) : "--"}
</td>
<td>
${trade.profit < 0 ? "<span style='color: #f23645'>" + tradeProfit.toFixed(2) + "</span>" : ""}
${trade.profit > 0 ? "<span style='color: #089981'>+" + tradeProfit.toFixed(2) + "</span>" : ""}
${trade.profit === 0 ? "<span style='color: #787b86'>" + tradeProfit.toFixed(2) + "</span>" : ""}
</td>
</tr>`;
});
$(".practice-trading-table").append(tradeHTML);
}
}
window.PracticeTradingInstance = new PracticeTrading();
window.PracticeTradingInstance.init();
})(jQuery);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment