<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Khushal Jain</title>
    <description>The latest articles on DEV Community by Khushal Jain (@khushaljain023).</description>
    <link>https://dev.to/khushaljain023</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2342024%2Ff7324eca-2f9d-4809-a40a-431f853fa96c.png</url>
      <title>DEV Community: Khushal Jain</title>
      <link>https://dev.to/khushaljain023</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/khushaljain023"/>
    <language>en</language>
    <item>
      <title>There is no official grammar for Pine Script, so I wrote a small one</title>
      <dc:creator>Khushal Jain</dc:creator>
      <pubDate>Mon, 26 Oct 2026 07:17:55 +0000</pubDate>
      <link>https://dev.to/khushaljain023/there-is-no-official-grammar-for-pine-script-so-i-wrote-a-small-one-3eh3</link>
      <guid>https://dev.to/khushaljain023/there-is-no-official-grammar-for-pine-script-so-i-wrote-a-small-one-3eh3</guid>
      <description>&lt;p&gt;If you have ever tried to lint, format, or diff TradingView Pine Script programmatically, you have hit the same&lt;br&gt;
wall: &lt;strong&gt;there is no official grammar.&lt;/strong&gt; TradingView does not publish or maintain a tree-sitter grammar or an&lt;br&gt;
ANTLR grammar for Pine Script. Every parser you can find is third-party, and third-party grammars for a&lt;br&gt;
language that changes with each Pine version go stale quietly - they just stop matching real-world code.&lt;/p&gt;

&lt;p&gt;I could not find anything to put in a CI pipeline for my indicators, so I wrote&lt;br&gt;
&lt;a href="https://github.com/85599/pinesprout" rel="noopener noreferrer"&gt;PineSprout&lt;/a&gt; (&lt;code&gt;pip install pinesprout&lt;/code&gt;, Python 3.12+, Linux/macOS/Windows,&lt;br&gt;
MIT). This post is about the parser decision and the linter rules, which are the two parts worth stealing.&lt;/p&gt;
&lt;h2&gt;
  
  
  The whole toolkit in one CLI
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pinesprout generate &lt;span class="s2"&gt;"&amp;lt;prompt&amp;gt;"&lt;/span&gt;     &lt;span class="c"&gt;# Claude-powered, from natural language&lt;/span&gt;
pinesprout template new ema-cross-indicator &lt;span class="nt"&gt;--title&lt;/span&gt; &lt;span class="s2"&gt;"My EMA Cross"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; my_indicator.pine
pinesprout lint my_indicator.pine
pinesprout format my_indicator.pine &lt;span class="nt"&gt;--write&lt;/span&gt;
pinesprout analyze my_indicator.pine
pinesprout optimize my_indicator.pine
pinesprout explain my_indicator.pine
pinesprout upgrade legacy_strategy.pine &lt;span class="nt"&gt;--to&lt;/span&gt; 6 &lt;span class="nt"&gt;--write&lt;/span&gt;
pinesprout docs my_indicator.pine &lt;span class="nt"&gt;--format&lt;/span&gt; markdown &lt;span class="nt"&gt;-o&lt;/span&gt; DOCS.md
pinesprout readme my_indicator.pine &lt;span class="nt"&gt;-o&lt;/span&gt; README.md
pinesprout report my_indicator.pine
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Every command supports &lt;code&gt;--json&lt;/code&gt;. That is not a nicety - it is the reason the tool is usable outside a terminal.&lt;br&gt;
Machine-readable output means the linter can gate a PR, the formatter can run in a pre-commit hook, and the&lt;br&gt;
upgrader can be scripted across a folder of old v4 scripts.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why a hand-rolled lexer instead of tree-sitter
&lt;/h2&gt;

&lt;p&gt;Two options existed: depend on an unofficial grammar, or write a small one. I wrote the small one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pinesprout/core/
├── lexer.py        # regex-based Pine Script tokenizer
├── parser.py       # lightweight AST builder
├── ast_nodes.py    # typed AST node definitions
├── formatter.py    # deterministic code formatter
├── linter.py       # static analysis rule engine
├── analyzer.py     # structural / complexity analysis
├── optimizer.py    # refactor suggestions
├── upgrader.py     # v4 -&amp;gt; v5 -&amp;gt; v6 migrations
└── version_rules.py# deprecated symbols &amp;amp; migration rules
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The deliberate constraint: the output shape &lt;strong&gt;mirrors a tree-sitter concrete syntax tree&lt;/strong&gt; - typed nodes,&lt;br&gt;
line/column spans, a &lt;code&gt;children&lt;/code&gt; list. Nothing downstream (formatter, linter, analyzer, optimizer, upgrader)&lt;br&gt;
knows or cares how the tree got built.&lt;/p&gt;

&lt;p&gt;That buys two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It handles real-world Pine's edge cases without waiting on a third party's release cycle.&lt;/li&gt;
&lt;li&gt;If TradingView ever ships an official grammar, it drops in behind &lt;code&gt;parser.py&lt;/code&gt; and &lt;strong&gt;no consumer changes.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A regex tokenizer is not "proper" compiler design, and I am fine with that. It is ~two files, dependency-free,&lt;br&gt;
well tested, and it removes the only thing in the project I could not control.&lt;/p&gt;
&lt;h2&gt;
  
  
  The lint rule that pays for itself: repaint risk
&lt;/h2&gt;

&lt;p&gt;Pine's most expensive class of bug is not a syntax error, it is an indicator that looks brilliant historically&lt;br&gt;
and lies in real time. Repainting. So the linter has a dedicated category (&lt;code&gt;repaint-risk&lt;/code&gt;) driven by an actual&lt;br&gt;
list in &lt;code&gt;version_rules.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;REPAINT_PRONE_FUNCTIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;request.security&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;security&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ta.valuewhen&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ta.barssince&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input.source&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;request.security()&lt;/code&gt; on a higher timeframe without a lookahead guard, &lt;code&gt;ta.valuewhen()&lt;/code&gt; reading a value that has&lt;br&gt;
not settled on the real-time bar - when one of these shows up in a plot path, the linter flags the line and&lt;br&gt;
explains that the script can repaint historical values on lower timeframes, with a separate note for the case&lt;br&gt;
where the repainting is intentional. There is a matching &lt;code&gt;PERFORMANCE_SENSITIVE_FUNCTIONS&lt;/code&gt; set (&lt;code&gt;ta.sma&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;ta.ema&lt;/code&gt;, &lt;code&gt;ta.rsi&lt;/code&gt;, &lt;code&gt;ta.atr&lt;/code&gt;, &lt;code&gt;ta.stoch&lt;/code&gt;, &lt;code&gt;ta.macd&lt;/code&gt;, &lt;code&gt;ta.highest&lt;/code&gt;, &lt;code&gt;ta.lowest&lt;/code&gt;) because calling a rolling&lt;br&gt;
series inside a loop is the second most common way to make a script crawl.&lt;/p&gt;

&lt;p&gt;Alongside those, the rule set covers unused variables, deprecated syntax, and general performance issues - the&lt;br&gt;
four things that actually make an indicator script unmaintainable. Because the rules are a rule &lt;em&gt;engine&lt;/em&gt;, not a&lt;br&gt;
pile of regexes, you can add your house style without touching the core:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .pinesprout/plugins/my_rules.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pinesprout.plugins.base&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PineSproutPlugin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LintRule&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pinesprout.core.linter&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;LintIssue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Severity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LintCategory&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NoMagicColorsRule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LintRule&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no-magic-colors&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LintIssue&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;issues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;color=color.red&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LintIssue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Severity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INFO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;LintCategory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;STYLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Avoid hard-coded colors; expose as an input.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;issues&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyPlugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PineSproutPlugin&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my-org-rules&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.0.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lint_rules&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;NoMagicColorsRule&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;

&lt;span class="n"&gt;PLUGIN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MyPlugin&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plugins load from a local &lt;code&gt;.pinesprout/plugins/&lt;/code&gt; folder or through a &lt;code&gt;pinesprout.plugins&lt;/code&gt; entry point, so a team&lt;br&gt;
can version its own rule pack as a package.&lt;/p&gt;
&lt;h2&gt;
  
  
  Upgrading v4 → v5 → v6
&lt;/h2&gt;

&lt;p&gt;Pine's version migrations are mostly mechanical renames, and they live as data rather than as if-statements in&lt;br&gt;
the upgrader:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# `study()` was renamed to `indicator()` in Pine v5.
# `security()` moved to the `request.*` namespace in v5.   -&amp;gt; request.security(
# `strategy.performance` -&amp;gt; strategy.closedtrades.* accessors introduced in v6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A regex rewrite table plus typed &lt;code&gt;DeprecatedSymbol&lt;/code&gt; entries is honestly enough for a language whose breaking&lt;br&gt;
changes are namespace moves. &lt;code&gt;upgrade --to 6 --write&lt;/code&gt; applies them, and it is the command I reach for most,&lt;br&gt;
because a v4 script you still believe in is worth more than a rewrite you will never start.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Studio, if you prefer a browser
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;streamlit_app.py&lt;/code&gt; wraps the CLI: describe a strategy in plain English (AI-generated), pick a template, or paste&lt;br&gt;
your own script, then lint / format / analyze / optimize / explain / upgrade / document it with one-click&lt;br&gt;
downloads.&lt;/p&gt;

&lt;p&gt;The bit that is genuinely fun is the Template Builder: &lt;strong&gt;25 indicators&lt;/strong&gt; (trend, momentum, volatility, volume) ×&lt;br&gt;
&lt;strong&gt;4 signal patterns&lt;/strong&gt; × &lt;strong&gt;3 risk-management styles&lt;/strong&gt;, with every length, threshold and stop-loss % adjustable -&lt;br&gt;
thousands of distinct scripts, all of them lint-clean because they come out of the same formatter the CLI uses.&lt;br&gt;
There is also a live scrolling ticker (NIFTY 50, SENSEX, BANK NIFTY, commodities via yfinance) and an NSE-style&lt;br&gt;
index watch plus a TradingView global heatmap for the market-browsing side.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;".[dev]"&lt;/span&gt;
streamlit run streamlit_app.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Guardrails worth stating
&lt;/h2&gt;

&lt;p&gt;PineSprout only ever operates on Pine Script source &lt;strong&gt;you provide&lt;/strong&gt;. It does not fetch, decompile, or bypass&lt;br&gt;
protection on TradingView's invite-only or protected scripts. Anyone building tooling in this space should say&lt;br&gt;
that out loud in the README rather than let people assume - it is the difference between a useful tool and a&lt;br&gt;
ToS incident.&lt;/p&gt;

&lt;p&gt;Dev loop is &lt;code&gt;pytest&lt;/code&gt;, &lt;code&gt;ruff check .&lt;/code&gt;, &lt;code&gt;mypy pinesprout&lt;/code&gt;, and CI runs on every push.&lt;/p&gt;

&lt;p&gt;What's the buggiest pattern in your Pine scripts - repainting on historical bars, or &lt;code&gt;security()&lt;/code&gt; look-ahead?&lt;br&gt;
And if you maintain a TradingView repo, do you run any lint/format gate on &lt;code&gt;.pine&lt;/code&gt; files today? The toolkit is&lt;br&gt;
&lt;a href="https://github.com/85599/pinesprout" rel="noopener noreferrer"&gt;85599/pinesprout&lt;/a&gt; on PyPI and GitHub, MIT - the parser is the part I would&lt;br&gt;
most like someone to disagree with.&lt;/p&gt;

</description>
      <category>python</category>
      <category>opensource</category>
      <category>tradingview</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Windows has a free OCR engine. I pointed it at my broker's position grid.</title>
      <dc:creator>Khushal Jain</dc:creator>
      <pubDate>Mon, 26 Oct 2026 07:02:16 +0000</pubDate>
      <link>https://dev.to/khushaljain023/windows-has-a-free-ocr-engine-i-pointed-it-at-my-brokers-position-grid-2ih1</link>
      <guid>https://dev.to/khushaljain023/windows-has-a-free-ocr-engine-i-pointed-it-at-my-brokers-position-grid-2ih1</guid>
      <description>&lt;p&gt;Every options trader I know keeps their book in an Excel sheet they update by hand. Strikes, quantities,&lt;br&gt;
premiums, and a row of "what if NIFTY is here at expiry" columns that they retype every time the market moves.&lt;br&gt;
The sheet is the honest part of the process - it is the only place where the size of the risk is written down.&lt;br&gt;
And it is also the part nobody wants to maintain.&lt;/p&gt;

&lt;p&gt;So I built a tool around one idea: &lt;strong&gt;the screenshot your broker already shows you is the input format.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Drop it in, get back a workbook with live premiums, live P&amp;amp;L, and a scenario grid that recalculates itself.&lt;br&gt;
No broker login, no API key, and the screenshot never leaves the machine.&lt;/p&gt;

&lt;p&gt;The repo is &lt;a href="https://github.com/85599/nifty-desk" rel="noopener noreferrer"&gt;nifty-desk&lt;/a&gt; (MIT, Python, Windows). This post is about the&lt;br&gt;
two parts that were actually hard: reading the screenshot, and getting the numbers to stay live.&lt;/p&gt;
&lt;h2&gt;
  
  
  The trick nobody tells you about Windows OCR
&lt;/h2&gt;

&lt;p&gt;Windows ships an OCR engine - &lt;code&gt;Windows.Media.Ocr&lt;/code&gt;, the same one the Photos app uses. It is free, it runs&lt;br&gt;
locally, it has recognizers per language, and it needs no install. The catch is that it is a WinRT API, so&lt;br&gt;
the practical way to reach it from Python is to shell out to PowerShell.&lt;/p&gt;

&lt;p&gt;Here is the real call from &lt;code&gt;ocr/winocr.ps1&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Add-Type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-AssemblyName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;System.Runtime.WindowsRuntime&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Add-Type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-AssemblyName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;System.Drawing&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Windows.Media.Ocr.OcrEngine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;Windows.Foundation.UniversalApiContract&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;ContentType&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;WindowsRuntime&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Out-Null&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Windows.Graphics.Imaging.BitmapDecoder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;Windows.Foundation.UniversalApiContract&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;ContentType&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;WindowsRuntime&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Out-Null&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="nv"&gt;$engine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$null&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$langs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@([&lt;/span&gt;&lt;span class="n"&gt;Windows.Media.Ocr.OcrEngine&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;::AvailableRecognizerLanguages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$pick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$langs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Where-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LanguageTag&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-eq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$LanguageTag&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-First&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;$null&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-eq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$pick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$langs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Count&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-gt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$pick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$langs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;$null&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-ne&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$pick&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$engine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Windows.Media.Ocr.OcrEngine&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;TryCreateFromLanguage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$pick&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;AsTask&lt;/code&gt; + a small &lt;code&gt;Await&lt;/code&gt; helper turns the WinRT async calls into something a script can wait on - that is&lt;br&gt;
the only way to drive WinRT from Windows PowerShell without writing a compiled app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Upscale before you recognise.&lt;/strong&gt; This was the single biggest accuracy win. The ODIN terminal draws its&lt;br&gt;
position grid in a roughly 9px font. At 1:1 the engine silently drops characters, and a dropped character in&lt;br&gt;
a strike price is not a typo - it is a different trade. So the image goes through a high-quality bicubic&lt;br&gt;
resize at 2x first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$Scale&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-ne&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nv"&gt;$src&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;System.Drawing.Image&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;FromFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nv"&gt;$dst&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;New-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;System.Drawing.Bitmap&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="nv"&gt;$src&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Width&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$Scale&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="nv"&gt;$src&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Height&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$Scale&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nv"&gt;$g&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;System.Drawing.Graphics&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;FromImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$dst&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nv"&gt;$g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InterpolationMode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;System.Drawing.Drawing2D.InterpolationMode&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;HighQualityBicubic&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nv"&gt;$g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DrawImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$dst&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$dst&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Twenty-two lines of PowerShell replaced what would otherwise be a cloud OCR bill and a privacy problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parsing: read the header, never the column position
&lt;/h2&gt;

&lt;p&gt;A grid is not a text file. My first instinct was to split each row on whitespace and index into it, which&lt;br&gt;
works exactly once, on my screen, on my broker layout.&lt;/p&gt;

&lt;p&gt;The version that ships reads the &lt;strong&gt;header row&lt;/strong&gt; and only uses the columns that header actually spells out.&lt;br&gt;
If a broker hides three columns, Net Qty, Market Price and MTM still land in the right cells, and nothing is&lt;br&gt;
guessed at. Two other things earned their complexity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CE/PE recovery.&lt;/strong&gt; When OCR loses the option type on a leg, recover it from the sibling leg of the same
strike, or from the live NSE price for that instrument. Do not fail the whole row for one dropped glyph.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lot size auto-detection.&lt;/strong&gt; Quantities in the grid are in lots; the workbook needs contracts. The tool
infers the lot size from the quantity values instead of asking you to type it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then there is the sign convention, which is the part that quietly ruins P&amp;amp;L sheets:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The terminal shows a short as a &lt;em&gt;negative&lt;/em&gt; Net Qty. The workbook writes a SELL as a &lt;strong&gt;positive&lt;/strong&gt; Qty with a&lt;br&gt;
&lt;code&gt;Side&lt;/code&gt; column, and the scenario grid reads sell-positive / buy-negative so the formulas come out right.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That convention is stated in the README and printed in the sheet, because the alternative is a book that looks&lt;br&gt;
correct and is wrong by twice the position.&lt;/p&gt;
&lt;h2&gt;
  
  
  Keeping it honest: live prices, not a snapshot
&lt;/h2&gt;

&lt;p&gt;A parsed screenshot goes stale in about four minutes. So each workbook pulls premiums and spot from the public&lt;br&gt;
NSE option chain - no key, no subscription - and refreshes every 5 minutes.&lt;/p&gt;

&lt;p&gt;The output is one block per expiry, laid out like the sheet a person would have made by hand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a full &lt;code&gt;STRIKE / QTY / PREMIUM&lt;/code&gt; ladder at a 50-point gap, &lt;strong&gt;including the zero rows&lt;/strong&gt;, so you can type an
unstated position straight into the sheet&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CP IF &amp;lt;spot&amp;gt;&lt;/code&gt; scenario columns at a 100-point gap, each one a live &lt;code&gt;IF&lt;/code&gt; formula - edit a quantity or a rate
and the whole grid recalculates&lt;/li&gt;
&lt;li&gt;CE section, PE section, and a &lt;code&gt;NIFTY&lt;/code&gt; futures line that is always present even when the screenshot has no
future, so you can add one yourself&lt;/li&gt;
&lt;li&gt;a grand &lt;code&gt;TOTAL&lt;/code&gt;, with each expiry printing on its own page&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is real generated output, not a mockup:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxj88ttwc1ri7t1kgr70e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxj88ttwc1ri7t1kgr70e.png" alt="generated workbook" width="800" height="514"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The line I care about most is the &lt;code&gt;CP IF&lt;/code&gt; grid. It answers &lt;em&gt;"what happens to my book if the market sits exactly&lt;br&gt;
where it is - or moves 500 points against me"&lt;/em&gt; before the market answers it for you.&lt;/p&gt;
&lt;h2&gt;
  
  
  Shipping a .exe to people who do not have Python
&lt;/h2&gt;

&lt;p&gt;The traders who wanted this are the least likely to run &lt;code&gt;pip install&lt;/code&gt;. So there is a Windows binary built with&lt;br&gt;
PyInstaller (&lt;code&gt;build_exe.bat&lt;/code&gt; in the repo does it in one command if you would rather build it yourself).&lt;/p&gt;

&lt;p&gt;Two things I got pushed on, and handled deliberately:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Windows protected your PC."&lt;/strong&gt; That is SmartScreen, because the exe is unsigned - code-signing certificates&lt;br&gt;
cost money. The README says exactly that, tells you to build from source if you prefer, and the whole source is&lt;br&gt;
right there. I would rather explain the warning than pay for a certificate I cannot honestly make mean more&lt;br&gt;
than it does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"It read 0 rows."&lt;/strong&gt; Silent failure is the worst outcome for a parsing tool, so the activity log now names the&lt;br&gt;
cause:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;"none of them formed the ODIN position grid"&lt;/em&gt; - header row was cropped out of the screenshot&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;"does not follow the standard ODIN column layout"&lt;/em&gt; - columns rearranged or hidden in that terminal&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;"no text at all"&lt;/em&gt; - the English OCR recognizer is not installed (Settings → Time &amp;amp; Language → Language)&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;"every row was filtered out"&lt;/em&gt; - grid parsed fine, untick &lt;em&gt;NIFTY rows only&lt;/em&gt; or &lt;em&gt;Hide fully closed&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each message maps to an action. Debugging became a checkbox instead of a support conversation.&lt;/p&gt;
&lt;h2&gt;
  
  
  Run it
&lt;/h2&gt;

&lt;p&gt;No-Python path: grab &lt;code&gt;NiftyDesk-win64-v1.0.4.zip&lt;/code&gt; from the&lt;br&gt;
&lt;a href="https://github.com/85599/nifty-desk/releases/latest" rel="noopener noreferrer"&gt;latest release&lt;/a&gt;, unzip, double-click &lt;code&gt;NiftyDesk.exe&lt;/code&gt;.&lt;br&gt;
Workbooks land in an &lt;code&gt;output&lt;/code&gt; folder next to it.&lt;/p&gt;

&lt;p&gt;From source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
python server.py     &lt;span class="c"&gt;# http://127.0.0.1:8765/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Privacy note, stated in the code's shape rather than a policy page: the only network request the tool makes is&lt;br&gt;
the public NSE quote call. Positions and screenshots stay local. And it is a risk-visibility and bookkeeping&lt;br&gt;
tool - it places no orders, promises no returns, and is not investment advice.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would do differently
&lt;/h2&gt;

&lt;p&gt;Wire the same pipeline to the other popular terminals' layouts (right now it is built around ODIN's grid), and&lt;br&gt;
push the scenario grid to options other than NIFTY, since the parsing is symbol-agnostic but the defaults are not.&lt;/p&gt;

&lt;p&gt;Which broker's position grid do you fight with every week - and what does your Excel sheet refuse to tell you&lt;br&gt;
about your own risk? If you want the workbook itself, the repo is&lt;br&gt;
&lt;a href="https://github.com/85599/nifty-desk" rel="noopener noreferrer"&gt;85599/nifty-desk&lt;/a&gt;. It is MIT, so fork it, break it, or hand me a&lt;br&gt;
screenshot layout it chokes on.&lt;/p&gt;

</description>
      <category>python</category>
      <category>opensource</category>
      <category>fintech</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>No API key, no data vendor: pulling live NSE + BSE option chains in Python</title>
      <dc:creator>Khushal Jain</dc:creator>
      <pubDate>Mon, 26 Oct 2026 07:00:55 +0000</pubDate>
      <link>https://dev.to/khushaljain023/no-api-key-no-data-vendor-pulling-live-nse-bse-option-chains-in-python-4fe0</link>
      <guid>https://dev.to/khushaljain023/no-api-key-no-data-vendor-pulling-live-nse-bse-option-chains-in-python-4fe0</guid>
      <description>&lt;p&gt;Retail option analytics in India has a data problem. Licensed feeds cost real money, brokers gate their API&lt;br&gt;
behind accounts, and the free tools that exist hand you a wall of numbers and stop there. Meanwhile the&lt;br&gt;
exchanges publish option chains on their own public websites - which anyone can read, and which nobody will&lt;br&gt;
support you when it changes.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://github.com/85599/Strike-and-Hedge" rel="noopener noreferrer"&gt;Strike &amp;amp; Hedge&lt;/a&gt; on exactly those public endpoints: live&lt;br&gt;
NSE &lt;strong&gt;and&lt;/strong&gt; BSE chains, Black-Scholes Greeks solved locally, PCR, max pain, OI walls, expected move, and a&lt;br&gt;
rule-based trade plan. No API key. MIT. 45 offline tests.&lt;/p&gt;

&lt;p&gt;This post is the field notes - the parts that are not in any tutorial because they only show up when a&lt;br&gt;
government exchange quietly retires an endpoint on a Tuesday.&lt;/p&gt;
&lt;h2&gt;
  
  
  The anti-bot wall comes first
&lt;/h2&gt;

&lt;p&gt;Both exchanges sit behind Akamai. Plain &lt;code&gt;requests&lt;/code&gt; gets 403s, and no amount of header cosplay fixes it,&lt;br&gt;
because the TLS handshake itself is the fingerprint. What works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# copilot/nse.py, copilot/bse.py
# curl_cffi impersonates a real Chrome TLS/JA3 profile; warm cookies from the homepage first
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;curl_cffi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;   &lt;span class="c1"&gt;# falls back to `requests` if curl_cffi is unavailable
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the fallback. &lt;code&gt;curl_cffi&lt;/code&gt; is a compiled dependency and it will not install on some machine you care&lt;br&gt;
about; the tool should degrade to "this source did not load" rather than "this tool does not start".&lt;/p&gt;
&lt;h2&gt;
  
  
  Endpoints are a moving target, so write down what moved
&lt;/h2&gt;

&lt;p&gt;The specific things that bit me, as of October 2026:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;What is actually there&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;NSE chains&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;option-chain-v3&lt;/code&gt; + &lt;code&gt;option-chain-contract-info&lt;/code&gt;. The old &lt;code&gt;option-chain-indices&lt;/code&gt; endpoint is &lt;strong&gt;retired&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BSE (SENSEX) chain&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;DerivOptionChain&lt;/code&gt;, which wants the expiry formatted as &lt;code&gt;15 Oct 2026&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BSE expiry list&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;not published.&lt;/strong&gt; Upcoming Thursdays get probed until one answers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NSE futures quotes&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;gone.&lt;/strong&gt; &lt;code&gt;quote-derivative&lt;/code&gt;, &lt;code&gt;quote-v2&lt;/code&gt; and &lt;code&gt;last-price-v5&lt;/code&gt; all 404&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FINNIFTY daily history&lt;/td&gt;
&lt;td&gt;niftyindices.com - Yahoo delisted &lt;code&gt;^CNXFINANCE&lt;/code&gt;, and NSE's own chart API returns empty for every index&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FINNIFTY intraday&lt;/td&gt;
&lt;td&gt;no public source exists&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two of those are lies rather than gaps, and they are the dangerous kind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;BSE reports &lt;code&gt;Absolute_Change_OI&lt;/code&gt; as 0 intraday.&lt;/strong&gt; Not "flat" - &lt;em&gt;not published yet&lt;/em&gt;. Rendered as a real
number, it tells you writers are not active when the exchange simply has not said.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BSE sends no IV.&lt;/strong&gt; So IV gets solved from the premium. And when a deep-ITM quote will not solve, the row
shows &lt;code&gt;—&lt;/code&gt;. A solver forced to always answer will happily hand you a 500% IV, and the Greeks downstream
will believe it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  When a number cannot be had, derive it and label it
&lt;/h2&gt;

&lt;p&gt;With NSE's futures quote endpoints retired, a futures card has no honest price. The tool derives the futures&lt;br&gt;
&lt;em&gt;level&lt;/em&gt; from put-call parity on the ATM strike - from &lt;code&gt;copilot/chain.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;synthetic_futures&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.065&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Futures level implied by put-call parity on the ATM strike: F = K·e^(-rT) + CE - PE.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;ltp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;put-call parity on ATM strike&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then three things matter more than the formula:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the card is &lt;strong&gt;labelled as derived&lt;/strong&gt;, and says it can be tens of points off your broker's price&lt;/li&gt;
&lt;li&gt;the parity basis is &lt;strong&gt;never scored as a signal&lt;/strong&gt; - last-traded option quotes put that much noise into it&lt;/li&gt;
&lt;li&gt;there is &lt;strong&gt;no futures OI or buildup anywhere in the score&lt;/strong&gt;, because that data does not exist&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Same principle for the FINNIFTY intraday panel: it falls back to recent daily bars &lt;strong&gt;and says that it did.&lt;/strong&gt;&lt;br&gt;
Silent fallback is where dashboards go to die. A tool that narrates its own blind spots gets used; one that&lt;br&gt;
hides them gets believed, and then gets abandoned.&lt;/p&gt;

&lt;p&gt;And at the close: signals come from a chain snapshot, not tick data, so near 15:30 IST the settled expiry is&lt;br&gt;
dropped. Otherwise you get a ₹0.05 premium with nonsense Greeks, printed as if it were actionable.&lt;/p&gt;
&lt;h2&gt;
  
  
  Split the deterministic engine from the LLM
&lt;/h2&gt;

&lt;p&gt;The pipeline is deliberately two halves:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; NSE option-chain-v3 ─┐
 BSE DerivOptionChain ─┤→ chain.py    normalise + Black-Scholes Greeks / IV
 niftyindices.com ─────┤      ↓
 Yahoo quotes/VIX ─────┘  analytics.py   PCR · max pain · OI walls · expected move
                               ↓
                          signals.py     bias + entry / stop / target / exit / lots
                               ↓
                    dataflows.py → agents.py → graph.py    (optional AI desk)
                               ↓
                          reporting.py   markdown report + decision log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;signals.py&lt;/code&gt; needs no key at all and produces &lt;code&gt;BULLISH&lt;/code&gt; / &lt;code&gt;BEARISH&lt;/code&gt; / &lt;code&gt;RANGE&lt;/code&gt; / &lt;code&gt;AVOID&lt;/code&gt; with a score and&lt;br&gt;
written reasons, plus a concrete plan: instrument, entry, stop, target, R:R, lots, max loss in ₹, exit rule.&lt;br&gt;
Stops and targets come from ATR and OI walls &lt;strong&gt;on the underlying&lt;/strong&gt;, converted to premium through delta - so&lt;br&gt;
they react to real price geometry instead of a fixed percentage. Never naked selling: bullish → BUY CE beyond&lt;br&gt;
ATM (plus a futures long on the derived level), range → defined-risk credit spreads, sell near the OI wall,&lt;br&gt;
hedge two strikes further out.&lt;/p&gt;

&lt;p&gt;The risk model has one behaviour I am stubborn about:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Type in capital and risk % - if &lt;strong&gt;one lot&lt;/strong&gt; already risks more than your capital × risk %, the tool refuses&lt;br&gt;
to round up. It tells you the position does not fit, instead of quietly showing "1 lot".&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The AI desk (chain / volatility / flow / news analysts → bull vs bear debate → risk → portfolio manager) sits&lt;br&gt;
&lt;em&gt;on top of the same numbers&lt;/em&gt;, across 8 backends: Anthropic, OpenAI, Gemini, DeepSeek, Groq, OpenRouter,&lt;br&gt;
&lt;strong&gt;Ollama (fully local, no key)&lt;/strong&gt;, or any OpenAI-compatible endpoint. Two guards make it behave:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Grounding check.&lt;/strong&gt; The LLM is given the rule engine's numbers. If its quoted entry is far from the live
premium - the classic mistake of stating a strike as a premium - the rule-engine level is shown instead,
with a warning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decision log.&lt;/strong&gt; Every AI call is scored later against the underlying's actual move with a hit/miss flag,
and those lessons feed into later runs. An agent loop with no outcome tracking is a random number generator
with good grammar.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is the engineering ask buried in that design: keep the rule engine separate. If you only want chains,&lt;br&gt;
Greeks and signals, &lt;strong&gt;delete &lt;code&gt;agents.py&lt;/code&gt; and &lt;code&gt;graph.py&lt;/code&gt; entirely&lt;/strong&gt; and the rest still works.&lt;/p&gt;
&lt;h2&gt;
  
  
  Make failure debuggable, and make tests offline
&lt;/h2&gt;

&lt;p&gt;When a chain will not load, the answer is one request away:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:8000/api/diag/NIFTY     &lt;span class="c"&gt;# what each price source actually returned&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Endpoints change and rate limits bite without notice, so a debug endpoint that shows raw per-source responses&lt;br&gt;
turns "it's broken" into "NSE is 403ing and BSE is fine".&lt;/p&gt;

&lt;p&gt;The test suite is offline on purpose - 45 tests, no network, no keys, so CI stays green whether or not an&lt;br&gt;
exchange is having a day:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; pytest tests &lt;span class="nt"&gt;-q&lt;/span&gt;
python tests/ui_harness.py    &lt;span class="c"&gt;# full UI on synthetic data → http://127.0.0.1:8765&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That synthetic harness is the other half of the trick: it lets a reviewer see the entire UI without a broker&lt;br&gt;
account, a key, or market hours. No frontend build step either - &lt;code&gt;web/&lt;/code&gt; is plain HTML/CSS/JS served by FastAPI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/85599/Strike-and-Hedge.git
&lt;span class="nb"&gt;cd &lt;/span&gt;Strike-and-Hedge &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
uvicorn server:app &lt;span class="nt"&gt;--port&lt;/span&gt; 8000     &lt;span class="c"&gt;# open http://localhost:8000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The part you should read before you scrape
&lt;/h2&gt;

&lt;p&gt;This is a &lt;strong&gt;scraper, not a licensed feed.&lt;/strong&gt; It reads public endpoints at request time and caches briefly; it is&lt;br&gt;
not a redistribution right, and exchanges can change terms whenever they like. Lot sizes in the repo are&lt;br&gt;
fallbacks - &lt;strong&gt;SEBI revises them&lt;/strong&gt;, so verify with your broker. And publishing buy/sell calls publicly can&lt;br&gt;
require SEBI Research Analyst registration, which is worth knowing before you share output anywhere. It is an&lt;br&gt;
educational research tool, not investment advice, and not issued by a registered research analyst.&lt;/p&gt;

&lt;p&gt;Which Indian exchange endpoint has wasted the most of your afternoon - and is anyone tracking a working&lt;br&gt;
futures-quote source now that NSE's are 404ing? If you want the whole pipeline to read or break:&lt;br&gt;
&lt;a href="https://github.com/85599/Strike-and-Hedge" rel="noopener noreferrer"&gt;85599/Strike-and-Hedge&lt;/a&gt;. PRs for endpoint changes, lot sizes and&lt;br&gt;
strike steps are the ones I merge fastest.&lt;/p&gt;

</description>
      <category>python</category>
      <category>fintech</category>
      <category>opensource</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
