Show PNL on Position Line on Chart

i would agree. An options to put it in ticks, or points would be my preference.

Thank you for this, is there any way that the PNL data box can be moved off of the price axis and closer to the current candle?

1 Like

Or just the space needed to avoid that working orders covers the PNL

Or number 2, by inverting the order on how to display PNL, first the numbers, then the currency

this is why it is unusable when scalping – I am not fast enough to move or place orders on the DOM and when you have price PNL, tp or sl at the same location on the price chart axis, it’s easy to lose money because it’s almost impossible to select the right one and or what if you move the wrong one?

What are you talking about? " to select the right one" what does that mean?
The Pnl is UNDER the order price on Y axis. So you can’t go wrong with it

I found this addition extremely helpful for scalping

Anyway if the currency is located after the pnl should be better
You have more visibility of the Pnl

try on a mobile web, then get back to me. I can do it on tradeview not on tradeovate mobile web. i have to trade on the train ride to work. To be honest, i got tired of fighting the chart trading system on tradeovate. it is not very trader friendly. That’s why tradingview is needed. Others probably tradeovate no problem, for me – its still a baby charting package that severely needs to grow up to what us traders can use and most of all, count on.

1 Like

Would it possible to switch PnL to show points/ticks instead of dollar amount? Same in the upper panel. Showing dollar amount effects my psychology in managing trades. Thank you.

4 Likes

Hi Alexander,

as many users have noticed and reported here, the last trade price is lagging a bit the price movement so the price line is not well synchronized.

But…

There is a study called “NOM COLORED PRICE LINE” that fit perfectly the price movement (in the candlestick) with ZERO lag.

Would you please consider the logic of this study and replace the current “Last trade price”?
This is very important

Again it is very accurate, no lag never even in a wild fast market

Thanks

Here’s a brief video of comparision

I am not seeing this option on my charts? Does using the beta access trader application mean I cannot use it to trade live with or in market replay?

1 Like

I know that this was added about a month ago but it would be nice if this was also added to the SL Line as-well.

It would be very helpful to see how much profit or loss you’re locking in. I understand that if you’re doing a bracket you can figure it out rather quickly but if you add a buy or sell by clicking on a spot on the chart its not as easy, especially as quickly as things move when trading the NQ.

1 Like

I don’t see this as an option in chart settings as of 10/17/2022?

1 Like

What happened to this feature?

I trade on their desktop app and it still works for me, but I have to trade off a chart on the main Tradovate window. If I open a chart up on a separate window and trade off of that, it doesn’t display the PNL on the position line. Maybe that’s the problem you’re having too?

Any updates on topic: displayng points/ticks on chart instead of dollars i know its on doms but thats just not it… i would love this feature and as i read others people aswell ! thanks for any reply

2 Likes

Please have option to display PnL in ticks

1 Like

Please implement this Tradovate. I want to see my PnL in TICKS and POINTS on the chart and even by the information bar that shows my position entry and size.

So this frustrated me. I use the web version of Tradovate (also NinjaTrader Web, since they are basically the same now), and I love the floating PnL feature in NinjaTrader Desktop. The version that Tradovate “added” awhile back just didn’t cut it for me, on a super widescreen I had to turn my head left and right just to focus on PnL.

So I built this little hack that reads your Open PnL in real time and displays it in a popup panel attached to your mouse. It updates instantly and even shows your points/ticks.

What it looks like:

  • Shows Symbol + Side (Short/Long) on the left.
  • Big, color-coded PnL on the right (green = profit, red = loss).
  • A smaller line under PnL showing (+XX.X pts / +YYY ticks).
  • F = toggle follow mouse
  • V = toggle show/hide
  • ▲/▼ buttons (when pinned) let you resize the font dynamically.

If anyone else wants it, here’s how:

Setup Instructions

  1. Install Tampermonkey (free browser extension - https://www.tampermonkey.net/)
  • Chrome: Tampermonkey – Chrome Web Store
  • Edge: Tampermonkey – Edge Add-ons
  • Firefox: Tampermonkey – Mozilla Add-ons
  1. Add the script
  • After installing Tampermonkey, click its icon → DashboardCreate a new script.
  • Delete the template code, then paste in the script (see below).
  • Press Ctrl+S (or Cmd+S) to save. Make sure the script is enabled.
  1. Open Tradovate Web
  • Go to https://trader.tradovate.com/.
  • Make sure your Positions panel is open and has the “Open PnL” column visible.
  • Refresh the page. You should now see the floating panel!
  1. Edit tick sizes/values if needed
  • I included the most popular contracts (MNQ, ES, MES, YM, RTY, CL, GC, etc.).
  • If you trade something not in the list, open the script in Tampermonkey and add it to the DEFAULT_TICK_SPECS section in the code.

Notes

  • This runs entirely in your browser. It just reads the DOM of your Positions table; nothing is sent anywhere.
  • I’ve only tested on Windows and Mac but only in the latest chrome version.
  • If you resize your Positions panel or change contracts, the popup updates automatically.
  • If you pin the panel with F, you can drag your mouse freely and the panel stays put.
  • To turn it off completely, just toggle the script off in Tampermonkey.

Disclaimer: Shared as-is for personal use, use at your own risk; not financial advice, and I’m not liable for any losses or issues. :slightly_smiling_face:

The Script:

// ==UserScript==
// @name         Tradovate Positions -> Minimal Big PnL (instant, follow, dark, v-toggle, ticks/points)
// @namespace    tv-positions-openpl-follow-min-instant-v
// @match        https://*.tradovate.com/*
// @match        https://trader.tradovate.com/*
// @run-at       document-idle
// @allFrames    true
// @grant        none
// ==/UserScript==

// ---------- Created By: Matthew Lebo: matthewlebo@gmail.com----------
(() => {
  'use strict';

  // ---------- UI host ----------
  const host = document.createElement('div');
  host.style.position = 'fixed';
  host.style.left = '0px';
  host.style.top = '0px';
  host.style.transform = 'translate3d(12px, 12px, 0)';
  host.style.zIndex = '2147483647';
  document.documentElement.appendChild(host);
  const root = host.attachShadow({mode:'open'});

  root.innerHTML = `
    <style>
      :host { all: initial; }
      .wrap { --scale: 1.0; }
      .card {
        font: 14px/1.28 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
        color:#fff; background:#2a2a2a;
        border:1px solid #3f3f3f; border-radius:12px;
        padding:4px 10px 8px; /* tighter top padding */
        box-shadow:0 10px 30px rgba(0,0,0,.35);
        width:auto; min-width:220px; max-width:560px; user-select:none;
      }
      .rows { max-height:420px; overflow:auto; }

      .row {
        display:flex; align-items:flex-start; justify-content:space-between;
        gap:16px; padding:2px 0 6px; /* reduced top spacing */
        border-top:1px solid rgba(255,255,255,0.06);
      }
      .row:first-child { border-top:none; }

      /* Left: symbol + dir (dir right edge = symbol right edge) */
      .symBlock {
        display:inline-block;   /* shrink-to-fit */
        min-width: 0;
        max-width: 380px;
        vertical-align:top;
      }
      .sym {
        color:#ffffff; font-weight:800;
        font-size: calc(24px * var(--scale));
        line-height: 1.05;
        margin: 0; /* no extra margin above */
        display:block;
        overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
      }
      .dir {
        color:#bdbdbd;
        font-weight:700;
        font-size: calc(12px * var(--scale));
        letter-spacing: 0.6px;
        text-transform: uppercase;
        margin-top: 2px;
        text-align: right;          /* right-aligned under symbol */
        width: 100%;
        overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
      }

      /* Right: PnL + tiny meta + inline controls (controls show only when pinned) */
      .rightGroup { display:flex; align-items:center; gap:8px; }
      .pnlWrap { display:flex; flex-direction:column; align-items:flex-end; }
      .pnl {
        font-weight:900; font-size: calc(32px * var(--scale));
        text-align:right; white-space:nowrap;
      }
      .meta {
        font-size: calc(13px * var(--scale));
        font-weight: 700;
        color:#e6e6e6;
        margin-top: 2px;
        text-align:right;
        white-space:nowrap;
      }
      .pos { color:#00e676; }  /* bright green */
      .neg { color:#ff5252; }  /* bright red   */
      .neu { color:#e0e0e0; }

      .controlsInline { display:none; gap:6px; }
      .btn {
        border:0; background:#3a3a3a; color:#ddd; cursor:pointer;
        border-radius:8px; padding:2px 8px; font-size:14px;
      }
      .btn:hover { background:#444; color:#fff; }
    </style>

    <div class="wrap">
      <div class="card">
        <div class="rows content">Looking for Positions 222…</div>
      </div>
    </div>
  `;

  const wrap = root.querySelector('.wrap');
  const content = root.querySelector('.content');

  // ---------- Tick spec map ----------
  // EDIT TO YOUR NEEDS
  const DEFAULT_TICK_SPECS = {
    // Equity indices
    ES:  { tickSize: 0.25,  tickValue: 12.5 },
    MES: { tickSize: 0.25,  tickValue: 1.25 },
    NQ:  { tickSize: 0.25,  tickValue: 5.0 },
    MNQ: { tickSize: 0.25,  tickValue: 0.5 },
    YM:  { tickSize: 1.0,   tickValue: 5.0 },
    MYM: { tickSize: 1.0,   tickValue: 0.5 },
    RTY: { tickSize: 0.1,   tickValue: 5.0 },
    M2K: { tickSize: 0.1,   tickValue: 0.5 },

    // Energies
    CL:  { tickSize: 0.01,  tickValue: 10.0 },
    NG:  { tickSize: 0.001, tickValue: 10.0 },

    // Metals
    GC:  { tickSize: 0.1,   tickValue: 10.0 },
    MGC: { tickSize: 0.1,   tickValue: 1.0 },
    SI:  { tickSize: 0.005, tickValue: 25.0 },
    SIL: { tickSize: 0.005, tickValue: 2.5 },
  };
  function getUserTickSpecs() {
    try {
      return JSON.parse(localStorage.getItem('tvTickSpecs') || '{}') || {};
    } catch { return {}; }
  }
    function getRoot(sym) {
        const s = (sym || '').toUpperCase().replace(/\s+/g, '');
        // Build a list of known roots from user overrides + defaults
        const user = getUserTickSpecs();
        const keys = Object.keys(user).concat(Object.keys(DEFAULT_TICK_SPECS));
        // pick the longest key that is a prefix of the symbol (e.g., MNQ matches MNQU5)
        let best = '';
        for (const k of keys) {
            if (s.startsWith(k) && k.length > best.length) best = k;
        }
        // fall back to leading letters if nothing matches
        if (!best) best = (s.match(/^[A-Z]+/) || [''])[0];
        return best;
    }
  function getSpec(sym) {
    const root = getRoot(sym);
    const user = getUserTickSpecs();
    return user[root] || DEFAULT_TICK_SPECS[root] || null;
  }
  function fmtSigned(n, decimals) {
    const s = (n >= 0 ? '+' : '');
    const v = isFinite(n) ? (decimals != null ? n.toFixed(decimals) : String(n)) : '0';
    return s + v;
  }

  // ---------- Follow / Pin + Show/Hide ----------
  const state = { follow: true, offsetX: 16, offsetY: 16, scale: 1.0, hidden: false };

  let controlsEl = null;
  function createControls() {
    const box = document.createElement('div');
    box.className = 'controlsInline';
    const dec = document.createElement('button'); dec.className = 'btn'; dec.textContent = '▼';
    const inc = document.createElement('button'); inc.className = 'btn'; inc.textContent = '▲';
    dec.title = 'Smaller (when pinned)'; inc.title = 'Bigger (when pinned)';
    dec.addEventListener('click', () => {
      if (state.follow || state.hidden) return;
      state.scale = Math.max(0.6, +(state.scale - 0.1).toFixed(2));
      wrap.style.setProperty('--scale', state.scale);
      for (const r of rowRefs) syncDirWidth(r.uiSym, r.uiDir);
    });
    inc.addEventListener('click', () => {
      if (state.follow || state.hidden) return;
      state.scale = Math.min(2.0, +(state.scale + 0.1).toFixed(2));
      wrap.style.setProperty('--scale', state.scale);
      for (const r of rowRefs) syncDirWidth(r.uiSym, r.uiDir);
    });
    box.appendChild(dec); box.appendChild(inc);
    return box;
  }
  function updateControlsVisibility() {
    if (!controlsEl) return;
    controlsEl.style.display = (!state.follow && !state.hidden) ? 'flex' : 'none';
  }
  function applyPointerMode() {
    host.style.pointerEvents = state.follow ? 'none' : 'auto';
    updateControlsVisibility();
  }
  function applyVisibility() {
    host.style.display = state.hidden ? 'none' : '';
    applyPointerMode();
  }
  applyPointerMode();

  window.addEventListener('keydown', (e) => {
    const k = e.key.toLowerCase();
    if (k === 'f') { state.follow = !state.follow; applyPointerMode(); }
    if (k === 'v') { state.hidden = !state.hidden; applyVisibility(); }
  });

  // ---------- Cursor follow (edge flip) ----------
  let mx = 12, my = 12, needsPos = true;
  window.addEventListener('mousemove', (e) => { mx = e.clientX; my = e.clientY; needsPos = true; }, { passive:true });

  function placeAtCursor() {
    if (!state.hidden && state.follow && needsPos) {
      needsPos = false;
      const card = root.querySelector('.card');
      const rect = card.getBoundingClientRect();
      const pad = 8;
      let x = mx + state.offsetX, y = my + state.offsetY;
      if (x + rect.width + pad > innerWidth) x = mx - rect.width - state.offsetX;
      if (x < pad) x = pad;
      if (y + rect.height + pad > innerHeight) y = my - rect.height - state.offsetY;
      if (y < pad) y = pad;
      host.style.transform = `translate3d(${x|0}px, ${y|0}px, 0)`;
    }
    requestAnimationFrame(placeAtCursor);
  }
  requestAnimationFrame(placeAtCursor);

  // ---------- FixedDataTable helpers ----------
  function findGrid() {
    const container = document.querySelector('.module.positions.data-table');
    if (!container) return null;
    return container.querySelector('.public_fixedDataTable_main[role="grid"]') ||
           container.querySelector('[role="grid"]');
  }

  function getColMap(grid) {
    const hdrEls = [...grid.querySelectorAll('.fixedDataTableCellLayout_main[role="columnheader"]')];
    const headers = hdrEls
      .map(el => ({ el, left: parseFloat((el.style.left||'0').replace('px','')) || el.getBoundingClientRect().left }))
      .sort((a,b) => a.left - b.left)
      .map(x => ((x.el.querySelector('.public_fixedDataTableCell_cellContent, span') || x.el).textContent || '').trim().toLowerCase());
    const find = re => headers.findIndex(h => re.test(h));
    return {
      symbol:   find(/symbol/),
      netPos:   find(/net\s*pos/),
      netPrice: find(/net\s*price/),
      openPL:   find(/open.*p\/?l/)
    };
  }

  let rowRefs = []; // { symNode, dirNode, pnlNode, qtyText, entryText, uiSym, uiDir, uiPnl, uiMeta, rightGroup, moSym, moDir, moPnl }

  function classForPnlText(txt) {
    if (!txt) return 'neu';
    const isParen = /\(.*\)/.test(txt);
    const num = parseFloat(txt.replace(/[^\d.\-]/g, ''));
    const val = isParen ? -Math.abs(num) : num;
    if (!isFinite(val) || val === 0) return 'neu';
    return val > 0 ? 'pos' : 'neg';
  }

  // Keep dir's right edge aligned with symbol's right edge
  function syncDirWidth(uiSym, uiDir) {
    const w = Math.round(uiSym.getBoundingClientRect().width); // visible width
    uiDir.style.width = w + 'px';
  }

  function computeMeta(sym, dirTxt, qtyText, entryText, pnlText) {
    const spec = getSpec(sym);
    if (!spec) return ''; // unknown spec: show nothing
    const qty = Math.abs(parseFloat((qtyText || '0').replace(/,/g,''))) || 0;
    if (!qty) return '';

    // Parse PnL signed number
    const raw = parseFloat((pnlText || '0').replace(/[^\d.\-]/g,''));
    const pnlSigned = /\(.*\)/.test(pnlText || '') ? -Math.abs(raw) : raw;

    const ticks = pnlSigned / (spec.tickValue * qty);
    const points = ticks * spec.tickSize;

    const ticksRound = Math.round(ticks);
    const ptsStr = fmtSigned(points, 2);
    const ticksStr = fmtSigned(ticksRound, 0);
    return `(${ptsStr} pts / ${ticksStr} ticks)`;
  }

  function buildRowRefs(grid, colIdx) {
    // Clean old
    for (const r of rowRefs) { r.moSym?.disconnect(); r.moDir?.disconnect(); r.moPnl?.disconnect(); }
    rowRefs = [];
    content.innerHTML = '';

    const bodyRows = [...grid.querySelectorAll('.public_fixedDataTable_bodyRow[role="row"], .fixedDataTableRowLayout_main.public_fixedDataTable_bodyRow[role="row"]')];

    for (const row of bodyRows) {
      const cellEls = [...row.querySelectorAll('.fixedDataTableCellLayout_main[role="gridcell"]')]
        .map(el => ({ el, left: parseFloat((el.style.left||'0').replace('px','')) || el.getBoundingClientRect().left }))
        .sort((a,b) => a.left - b.left)
        .map(x => x.el);

      const symCell = cellEls[colIdx.symbol];
      const pnlCell = cellEls[colIdx.openPL];
      const qtyCell = cellEls[colIdx.netPos];
      const entryCell = cellEls[colIdx.netPrice];
      if (!symCell || !pnlCell) continue;

      const symNode = row.querySelector('.symbol-name-cell .column-flow > div:first-child') ||
                      symCell.querySelector('.public_fixedDataTableCell_cellContent') || symCell;
      const dirNode = row.querySelector('.symbol-name-cell .column-flow > div:nth-child(2)') || null;
      const pnlNode = pnlCell.querySelector('.public_fixedDataTableCell_cellContent') || pnlCell;

      const qtyText = (qtyCell?.textContent || '').trim();
      const entryText = (entryCell?.textContent || '').trim();

      // UI row layout
      const uiRow = document.createElement('div'); uiRow.className = 'row';
      const uiLeft = document.createElement('div'); uiLeft.className = 'symBlock';
      const uiSym  = document.createElement('div'); uiSym.className = 'sym';
      const uiDir  = document.createElement('div'); uiDir.className = 'dir';
      uiLeft.appendChild(uiSym);
      uiLeft.appendChild(uiDir);
      uiRow.appendChild(uiLeft);

      const rightGroup = document.createElement('div'); rightGroup.className = 'rightGroup';
      const pnlWrap = document.createElement('div'); pnlWrap.className = 'pnlWrap';
      const uiPnl  = document.createElement('div'); uiPnl.className = 'pnl neu';
      const uiMeta = document.createElement('div'); uiMeta.className = 'meta';
      pnlWrap.appendChild(uiPnl);
      pnlWrap.appendChild(uiMeta);
      rightGroup.appendChild(pnlWrap);
      uiRow.appendChild(rightGroup);
      content.appendChild(uiRow);

      // Initial fill
      const symTxt  = (symNode.textContent || '').trim();
      const dirTxt  = (dirNode?.textContent || '').trim();
      const pnlTxt  = (pnlNode.textContent || '').trim();
      uiSym.textContent = symTxt;
      uiDir.textContent = dirTxt;
      syncDirWidth(uiSym, uiDir);
      uiPnl.textContent = pnlTxt;
      uiPnl.className = `pnl ${classForPnlText(pnlTxt)}`;
      uiMeta.textContent = computeMeta(symTxt, dirTxt, qtyText, entryText, pnlTxt);

      // Observers for instant updates
      const updateSym = () => {
        uiSym.textContent = (symNode.textContent || '').trim();
        syncDirWidth(uiSym, uiDir);
        // symbol change may affect spec; recompute meta
        uiMeta.textContent = computeMeta(uiSym.textContent, uiDir.textContent, qtyText, entryText, uiPnl.textContent);
      };
      const updateDir = () => {
        uiDir.textContent = (dirNode?.textContent || '').trim();
        uiMeta.textContent = computeMeta(uiSym.textContent, uiDir.textContent, qtyText, entryText, uiPnl.textContent);
      };
      const updatePnl = () => {
        const t = (pnlNode.textContent || '').trim();
        uiPnl.textContent = t;
        uiPnl.className = `pnl ${classForPnlText(t)}`;
        uiMeta.textContent = computeMeta(uiSym.textContent, uiDir.textContent, qtyText, entryText, t);
      };

      const moSym = new MutationObserver(updateSym);
      moSym.observe(symNode, { characterData: true, subtree: true, childList: true });

      let moDir = null;
      if (dirNode) {
        moDir = new MutationObserver(updateDir);
        moDir.observe(dirNode, { characterData: true, subtree: true, childList: true });
      }

      const moPnl = new MutationObserver(updatePnl);
      moPnl.observe(pnlNode, { characterData: true, subtree: true, childList: true });

      rowRefs.push({ symNode, dirNode, pnlNode, qtyText, entryText, uiSym, uiDir, uiPnl, uiMeta, rightGroup, moSym, moDir, moPnl });
    }

    // Place the ▲/▼ controls to the right of the FIRST row's PnL
    const first = rowRefs[0];
    if (first) {
      if (!controlsEl) controlsEl = createControls();
      if (controlsEl.parentElement) controlsEl.parentElement.removeChild(controlsEl);
      first.rightGroup.appendChild(controlsEl);
      updateControlsVisibility();
    }
  }

  // Rebuild mapping whenever rows mount/unmount/virtualize
  let moGrid;
  function wireGridObserver(grid, colIdx) {
    moGrid?.disconnect();
    const target = grid.querySelector('.fixedDataTableLayout_rowsContainer') || grid;
    moGrid = new MutationObserver(() => buildRowRefs(grid, colIdx));
    moGrid.observe(target, { childList: true, subtree: true });
  }

  function refresh() {
    const grid = findGrid();
    if (!grid) { content.textContent = 'Looking for Positions…'; return; }
    const colIdx = getColMap(grid);
    if (colIdx.openPL === -1 || colIdx.symbol === -1) {
      content.textContent = 'Could not locate Symbol or Open P/L columns.';
      return;
    }
    buildRowRefs(grid, colIdx);
    wireGridObserver(grid, colIdx);
  }

  const boot = setInterval(() => {
    if (!document.documentElement.contains(host)) return clearInterval(boot);
    refresh();
  }, 500);

  // rAF skeleton (we update position on mousemove)
  function idle() { requestAnimationFrame(idle); } requestAnimationFrame(idle);

  // Update position on mouse move (unchanged)
  window.addEventListener('mousemove', (e) => {
    if (state.hidden || !state.follow) return;
    const card = root.querySelector('.card');
    const rect = card.getBoundingClientRect();
    const pad = 8;
    let x = e.clientX + state.offsetX, y = e.clientY + state.offsetY;
    if (x + rect.width + pad > innerWidth) x = e.clientX - rect.width - state.offsetX;
    if (x < pad) x = pad;
    if (y + rect.height + pad > innerHeight) y = e.clientY - rect.height - state.offsetY;
    if (y < pad) y = pad;
    host.style.transform = `translate3d(${x|0}px, ${y|0}px, 0)`;
  }, { passive:true });

  // Resync dir widths on resize/zoom
  window.addEventListener('resize', () => {
    for (const r of rowRefs) syncDirWidth(r.uiSym, r.uiDir);
  });

})();