From 21b1687a0d961da85e8882b170573bed7c0eaaf6 Mon Sep 17 00:00:00 2001 From: Rajesh Mohanan Date: Wed, 24 Jun 2026 13:29:02 +0530 Subject: [PATCH] Add Zerodha broker import (equity, long-only) --- brokers/README.md | 14 +++- src/stores/globals.js | 6 ++ src/utils/addTrades.js | 14 +++- src/utils/brokers.js | 150 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 181 insertions(+), 3 deletions(-) diff --git a/brokers/README.md b/brokers/README.md index e988e296..a4f5c49d 100644 --- a/brokers/README.md +++ b/brokers/README.md @@ -201,4 +201,16 @@ Please note that trades need to be ordered in ascending order 4. Select date range and click Export. - + +# Zerodha +Current scope (v1): +- Only the **Equity (EQ)** segment is supported (no F&O, currency or commodity). +- **Long positions only.** Any sell that would require a short position is rejected with an error rather than imported incorrectly. +- One account per import - if you trade from multiple Zerodha accounts, import each account's tradebook separately. + +1. Login to Kite (Zerodha's trading platform). +2. Go to Console > Reports > Tradebook. +3. Select segment "Equity", choose your date range, and click "Download" to get a CSV file named `tradebook--EQ.csv`. +4. Upload that CSV file as-is in TradeNote's import page. + +**Fees are estimated, not exact.** Zerodha's tradebook export doesn't include brokerage, STT, exchange charges, stamp duty or GST, and it doesn't say whether an order was placed as intraday (MIS) or delivery (CNC) - the actual basis Zerodha uses to bill those charges. TradeNote estimates this by checking whether a matched buy/sell pair happened on the same day, and applies Zerodha's published equity fee schedule accordingly. This is a reasonable approximation for journaling purposes, but it can diverge meaningfully from your real ledger (for example, a position held overnight that was originally placed as an intraday order will be billed by Zerodha at intraday rates, but TradeNote will estimate it at delivery rates). Don't rely on these figures for tax or accounting purposes - cross-check against Zerodha's own P&L/tax P&L reports for that. diff --git a/src/stores/globals.js b/src/stores/globals.js index b2c3f3f8..148b01a8 100644 --- a/src/stores/globals.js +++ b/src/stores/globals.js @@ -3285,6 +3285,12 @@ export const brokers = reactive([{ label: "TopstepX", assetTypes: ["futures"], autoSync: false +}, +{ + value: "zerodha", + label: "Zerodha", + assetTypes: ["stocks"], + autoSync: false } ]) diff --git a/src/utils/addTrades.js b/src/utils/addTrades.js index c28a4d30..aa9b02aa 100644 --- a/src/utils/addTrades.js +++ b/src/utils/addTrades.js @@ -1,5 +1,5 @@ import { filteredTradesTrades, blotter, pAndL, tradeExcursionId, spinnerLoadingPage, currentUser, selectedBroker, tradesData, timeZoneTrade, uploadMfePrices, executions, tradeId, existingImports, trades, gotExistingTradesArray, existingTradesArray, brokerData, selectedTradovateTier, queryLimit, queryLimitExistingTrades, marketCloseTime } from '../stores/globals.js' -import { useBrokerHeldentrader, useBrokerInteractiveBrokers, useBrokerMetaTrader5, useBrokerTdAmeritrade, useBrokerTradeStation, useBrokerTradeZero, useTradovate, useNinjaTrader, useRithmic, useFundTraders, useTastyTrade, useTopstepX } from './brokers.js' +import { useBrokerHeldentrader, useBrokerInteractiveBrokers, useBrokerMetaTrader5, useBrokerTdAmeritrade, useBrokerTradeStation, useBrokerTradeZero, useTradovate, useNinjaTrader, useRithmic, useFundTraders, useTastyTrade, useTopstepX, useBrokerZerodha } from './brokers.js' import { useChartFormat, useDateTimeFormat, useDecimalsArithmetic, useInitParse, useTimeFormat } from './utils.js' /* MODULES */ @@ -135,7 +135,7 @@ export async function useImportTrades(param1, param2, param3, param0) { } - let readAsTextArray = ["tradeZero", "template", "tdAmeritrade", "interactiveBrokers" , "tradovate", "ninjaTrader", "heldentrader", "rithmic", "fundTraders", "tastyTrade", "topstepX"] + let readAsTextArray = ["tradeZero", "template", "tdAmeritrade", "interactiveBrokers" , "tradovate", "ninjaTrader", "heldentrader", "rithmic", "fundTraders", "tastyTrade", "topstepX", "zerodha"] if (readAsTextArray.includes(selectedBroker.value)) { if (param2 == "api") { fileInput = param1 @@ -283,6 +283,16 @@ export async function useImportTrades(param1, param2, param3, param0) { }) } + /**************************** + * ZERODHA + ****************************/ + if (selectedBroker.value == "zerodha") { + console.log(" -> Zerodha") + await useBrokerZerodha(fileInput).catch(error => { + importFileErrorFunction(error) + }) + } + /**************************** * CREATE EXECUTIONS, TRADES diff --git a/src/utils/brokers.js b/src/utils/brokers.js index 2d29a9d2..b458bc16 100644 --- a/src/utils/brokers.js +++ b/src/utils/brokers.js @@ -65,6 +65,156 @@ export async function useBrokerTradeZero(param) { }) } +/**************************** + * ZERODHA + **************************** + * Equity (EQ) only, INR only, long-only (no short-selling support). + * Input is Zerodha Kite's "tradebook" CSV export (one row per execution). + * Fees are not provided by the broker file - they're computed here from + * Zerodha's published equity fee schedule, since brokerage/STT/exchange + * charges/GST are all formulaic, not broker-discretionary. Approximate by + * design (e.g. brokerage cap is applied per matched lot rather than per + * original order) - acceptable for journaling purposes. + ****************************/ +const zerodhaFeeRates = { + gst: 0.18, + sttDelivery: 0.001, //both legs + sttIntradaySell: 0.00025, //sell leg only + exchangeTxn: 0.0000297, + sebi: 0.000001, + stampDuty: 0.00015, //buy leg only + intradayBrokerageRate: 0.0003, + intradayBrokerageCap: 20 +} + +const useZerodhaApplyChunkFees = (sellExec, buyPrice, qty, isIntraday) => { + const buyTurnover = qty * buyPrice + const sellTurnover = qty * sellExec.price + + const brokerageBuy = isIntraday ? Math.min(zerodhaFeeRates.intradayBrokerageCap, zerodhaFeeRates.intradayBrokerageRate * buyTurnover) : 0 + const brokerageSell = isIntraday ? Math.min(zerodhaFeeRates.intradayBrokerageCap, zerodhaFeeRates.intradayBrokerageRate * sellTurnover) : 0 + const brokerage = brokerageBuy + brokerageSell + + const sttBuy = isIntraday ? 0 : zerodhaFeeRates.sttDelivery * buyTurnover + const sttSell = isIntraday ? zerodhaFeeRates.sttIntradaySell * sellTurnover : zerodhaFeeRates.sttDelivery * sellTurnover + const stt = sttBuy + sttSell + + const exchangeTxn = zerodhaFeeRates.exchangeTxn * (buyTurnover + sellTurnover) + const sebi = zerodhaFeeRates.sebi * (buyTurnover + sellTurnover) + const stampDuty = zerodhaFeeRates.stampDuty * buyTurnover + const gst = zerodhaFeeRates.gst * (brokerage + exchangeTxn + sebi) + + //all charges for the matched chunk are attributed to the closing (sell) execution; doesn't affect net totals + sellExec.comm += brokerage + sellExec.sec += stt + exchangeTxn + sebi + stampDuty + gst +} + +export async function useBrokerZerodha(param) { + return new Promise(async (resolve, reject) => { + try { + let tempArray = [] + if (typeof param === "string") { + let papaParse = Papa.parse(param, { header: true, skipEmptyLines: true }) + tempArray = papaParse.data + } else { + tempArray = param + } + + let executionsBySymbol = {} + tempArray.forEach(row => { + if (!row.symbol || !row.trade_type || !row.order_execution_time) return + + let side = row.trade_type.trim().toLowerCase() === "buy" ? "B" : "S" + let execTimeOnly = row.order_execution_time.split('T')[1] || "00:00:00" + + let exec = { + symbol: row.symbol, + side, + qty: parseFloat(row.quantity), + price: parseFloat(row.price), + tradeDate: row.trade_date, //YYYY-MM-DD + execTimeOnly, + sortKey: row.order_execution_time, + comm: 0, + sec: 0 + } + if (!executionsBySymbol[exec.symbol]) executionsBySymbol[exec.symbol] = [] + executionsBySymbol[exec.symbol].push(exec) + }) + + //FIFO match per symbol, long-only, to classify each matched quantity chunk as intraday vs delivery and apply fees accordingly + Object.keys(executionsBySymbol).forEach(symbol => { + let execs = executionsBySymbol[symbol].slice().sort((a, b) => a.sortKey.localeCompare(b.sortKey)) + let openLots = [] //FIFO queue of { remainingQty, price, tradeDate } + + execs.forEach(exec => { + if (exec.side === "B") { + openLots.push({ remainingQty: exec.qty, price: exec.price, tradeDate: exec.tradeDate }) + } else { + let qtyToMatch = exec.qty + while (qtyToMatch > 0.000001) { + if (openLots.length === 0) { + throw new Error("Sell of " + exec.qty + " " + symbol + " on " + exec.tradeDate + " exceeds open long quantity - short-selling is not supported for Zerodha imports") + } + let lot = openLots[0] + let matchedQty = Math.min(lot.remainingQty, qtyToMatch) + let isIntraday = lot.tradeDate === exec.tradeDate + useZerodhaApplyChunkFees(exec, lot.price, matchedQty, isIntraday) + lot.remainingQty -= matchedQty + qtyToMatch -= matchedQty + if (lot.remainingQty <= 0.000001) openLots.shift() + } + } + }) + }) + + //build rows in the shape the rest of the import pipeline expects (Template.csv columns) + Object.keys(executionsBySymbol).forEach(symbol => { + executionsBySymbol[symbol].forEach(exec => { + let qtyNumberSide = exec.side === "B" ? -exec.qty : exec.qty + let grossProceeds = qtyNumberSide * exec.price + let netProceeds = grossProceeds - exec.comm - exec.sec + + let dateArray = exec.tradeDate.split('-') //YYYY-MM-DD + let formattedDate = dateArray[1] + "/" + dateArray[2] + "/" + dateArray[0] //MM/DD/YYYY + + let temp = {} + temp.Account = "Zerodha" + temp["T/D"] = formattedDate + temp["S/D"] = formattedDate + temp.Currency = "INR" + temp.Type = "stock" + temp.Side = exec.side + temp.Symbol = exec.symbol + temp.SymbolOriginal = exec.symbol + temp.Qty = exec.qty.toString() + temp.Price = exec.price.toString() + temp["Exec Time"] = exec.execTimeOnly + temp.Comm = exec.comm.toString() + temp.SEC = exec.sec.toString() + temp.TAF = "0" + temp.NSCC = "0" + temp.Nasdaq = "0" + temp["ECN Remove"] = "0" + temp["ECN Add"] = "0" + temp["Gross Proceeds"] = grossProceeds.toString() + temp["Net Proceeds"] = netProceeds.toString() + temp["Clr Broker"] = "" + temp.Liq = "" + temp.Note = "" + + tradesData.push(temp) + }) + }) + } catch (error) { + reject(error) + return + } + + resolve() + }) +} + /**************************** * METATRADER 5 ****************************/