Do you mean source code ?
(() => {
βuse strictβ;
// ββ Constants ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
const BUTTON_ID = "zerodha-expand-collapse-btn";
const SELECTORS = {
section : "section.open-positions",
header : "header.data-table-header",
icons : "span.expand-icon",
expanded: "expanded",
};
const COLORS = {
expand : "#2e7d32", // green
collapse: "#c62828", // red
};
const LABELS = {
expand : "Expand All",
collapse: "Collapse All",
};
// ββ Helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
/**
* Applies a plain object of style properties to an element.
* Avoids inline style strings; keeps styling declarative and easy to diff.
*/
function applyStyles(el, styles) {
Object.assign(el.style, styles);
}
/** Creates the button with all attributes set via safe DOM APIs (no innerHTML). */
function createButton() {
const btn = document.createElement("button");
btn.id = BUTTON_ID;
btn.textContent = LABELS.expand; // textContent β XSS-safe
btn.setAttribute("aria-pressed", "false");
btn.setAttribute("title", LABELS.expand);
applyStyles(btn, {
marginLeft : "10px",
padding : "3px 8px",
cursor : "pointer",
borderRadius: "4px",
border : "none",
fontWeight : "500",
fontSize : "12px",
color : "white",
background : COLORS.expand,
transition : "background 0.2s ease",
});
return btn;
}
/** Syncs button label, colour, and ARIA state to the current expanded flag. */
function syncButton(btn, isExpanded) {
const label = isExpanded ? LABELS.collapse : LABELS.expand;
btn.textContent = label;
btn.setAttribute("aria-pressed", String(isExpanded));
btn.setAttribute("title", label);
applyStyles(btn, { background: isExpanded ? COLORS.collapse : COLORS.expand });
}
/**
* Derives the true expanded state from the DOM.
* Returns true only if every icon is currently expanded.
*/
function areAllExpanded() {
const icons = document.querySelectorAll(SELECTORS.icons);
if (!icons.length) return false;
return [...icons].every(icon => icon.classList.contains(SELECTORS.expanded));
}
// ββ Core logic βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function injectButton() {
// Idempotency β bail if already injected (handles SPA re-renders)
if (document.getElementById(BUTTON_ID)) return;
const section = document.querySelector(SELECTORS.section);
if (!section) return;
const header = section.querySelector(SELECTORS.header);
if (!header) return;
const btn = createButton();
btn.addEventListener("click", () => {
try {
const icons = document.querySelectorAll(SELECTORS.icons);
if (!icons.length) return;
// Read actual DOM state β not an internal flag
const currentlyExpanded = areAllExpanded();
icons.forEach(icon => {
const isExpanded = icon.classList.contains(SELECTORS.expanded);
if (!currentlyExpanded && !isExpanded) icon.click();
if ( currentlyExpanded && isExpanded) icon.click();
});
// Disable during settle window β prevents double-click race condition
btn.disabled = true;
// Defer DOM read β Zerodha updates classes asynchronously after icon.click()
setTimeout(() => {
// Guard β button may have been removed if user navigated away
if (!document.getElementById(BUTTON_ID)) return;
syncButton(btn, areAllExpanded());
btn.disabled = false;
}, 50);
} catch (err) {
// Absorb errors so a broken icon never bricks the button
btn.disabled = false;
console.warn("[Zerodha Ext] Error toggling positions:", err);
}
});
header.appendChild(btn);
// Defer injection sync β Zerodha's classes may not have settled on page load
setTimeout(() => {
if (!document.getElementById(BUTTON_ID)) return;
syncButton(btn, areAllExpanded());
}, 50);
// Button is live β stop watching for it
domObserver.disconnect();
// Watch only the header for button removal (SPA navigation away)
// When removed, restart the main observer to wait for it again
removalObserver.observe(header, { childList: true });
}
// ββ Observers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Watches for the button being removed from the DOM (user navigated away)
const removalObserver = new MutationObserver((mutations) => {
const removed = mutations.some(m =>
[...m.removedNodes].some(node => node.id === BUTTON_ID)
);
if (removed) {
removalObserver.disconnect();
// Resume watching for the section to reappear
domObserver.observe(document.body, { childList: true, subtree: true });
}
});
// Watches for the positions section to appear, then injects the button
const domObserver = new MutationObserver(() => {
if (!window.location.pathname.includes("positions")) return;
injectButton();
});
domObserver.observe(document.body, {
childList: true,
subtree : true,
});
// Also attempt immediately in case the DOM is already ready
injectButton();
})();
Yeah⦠You said you can examine every line of source. Right? How do end users do that? Did you put the code in Github?
Weβre excited to announce that position grouping, which is already available on the web, is now live on the Kite app as well.
Position Grouping
- Positions can now be grouped by None, Underlying, or Underlying & Expiry. Tapping the Group option automatically groups positions by their underlying. To switch grouping modes or turn off grouping entirely, tap the settings icon at the top and select the preferred option.

- A red indicator at the top signals when grouping or a filter is active.
- Individual groups can be collapsed or expanded by tapping the group name; itβs useful for focusing on specific positions. Thereβs also a Collapse/Expand All button at the top to manage all groups at once.

- Similar to the web, grouping is remembered until the user manually logs out of the app, and relogging will lead to a reset of the applied grouping. While the filter is reset the next day.
Multi-Selection & Exit
Multi-selection has been significantly reworked. Previously, a long press would open a separate page for selecting positions, making it impossible to track P&L during the process. Now, multi-selection happens directly on the positions page, allowing P&L to be tracked in real time while selecting. Additionally, positions can now be exited group-wise by long-pressing on the group name.

These changes are implemented on the Orderbook, GTT, and Alerts pages as well.

This build also has the Analyze button inside the quick basket, which was available only on the web earlier. The build is currently available in beta and will be rolled out to the live environment in the coming days.

Itβs WIP. Should be available in a few days; we are revamping a few things wrt filters.
@DhanushK @Arockiya_Raja , likewise can you bring grouping of holdings based on the industry, marketcap as well? This data is already available on console with colour blocks but if the same data can be brought directly to the holdings where we can see number of holdings in a particular industry or for a given market cap, it will be even more useful especially for the mobile app users.
On our list of things to do, already picked it up.
Hope to see it soon on the app.
This is now available on the Kite web.
Could you make the selected filter persistent in mobile version ? Iβm using iOS
Whenever I select the filter and close the app and when I open app again it resets to its default state.
Next. Alerts based on P&L.
alerts on whatsapp is possible ?
Following up on the filter request:
- Filtering between CE, PE and FUT on kite. Right now theyβre all clubbed under NFO. Also
- In console, under the P&L section, please add SUBTOTAL along with additional filters. For eg, if I want to look up what I earned in a particular scrip, I have to manually add up all the trades.
Same for kite, sum total should be dynamically changed to subtotal upon filtering. (using search filter)
please introduce a collapse all/expand all button in kite web also just like in kite app.
if i have too many positions beyond a certain number of lines after grouping it wont show all the positions. have to manually collapse positions then only the last positions will start showing.
Also sorting by p&l & day p&l doesnt work when grouping by underlying is done. ideally it should be sort by each grouping instead it still tries to sort by individual line items instead of by groups.



