Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion brokers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,4 +201,16 @@ Please note that trades need to be ordered in ascending order
4. Select date range and click Export.

<img src="https://f003.backblazeb2.com/file/7ak-public/tradenote/topstepx3.png" width="250">


# 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-<client-id>-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.
6 changes: 6 additions & 0 deletions src/stores/globals.js
Original file line number Diff line number Diff line change
Expand Up @@ -3285,6 +3285,12 @@ export const brokers = reactive([{
label: "TopstepX",
assetTypes: ["futures"],
autoSync: false
},
{
value: "zerodha",
label: "Zerodha",
assetTypes: ["stocks"],
autoSync: false
}
])

Expand Down
14 changes: 12 additions & 2 deletions src/utils/addTrades.js
Original file line number Diff line number Diff line change
@@ -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 */
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
150 changes: 150 additions & 0 deletions src/utils/brokers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
****************************/
Expand Down