Skip to content
vp.Ven Petrov
WorkWritingAboutAt a glance
← Projects & notes
Financial review labProject story · 5 min read

Exploring a financial report with Python and FastAPI

I started with one income change, then opened up the same 54-page report: products, regions, cash flow, core adjustments and a dataset whose figures lead back to the source.

Ven PetrovInteractive prototypeOpen project

I wanted a small finance example where I could check every number and make the working visible. I used Codex to build this independent prototype around one question: what contributed to the change in quarterly operating income?

I chose public Novartis figures with comparable quarters in the same statement, so the task could stay focused on that reconciliation. It compares reported Q4 2025 with Q4 2024. This is new demonstration work using public reports, not work I performed for Novartis.

Explore the full report, or start with the original quarterly bridge.

Start with a comparison that can be checked

The source is a manually reviewed extract of the company's financial report. Each financial line carries an identifier, its two period values, the unit, its sign and a report reference. Automatic PDF extraction is outside this version.

The Python pipeline checks the input before calculating: both quarters must be present, the reporting basis and units must agree, and the expected financial lines must be unique and complete. I kept expense values negative as shown in the income statement.

That sign convention matters. Other expense moved from minus $659 million to minus $447 million. The expense became smaller, so its contribution to operating income was positive $212 million.

Reconcile before writing

Reported operating income moved from $3,530 million to $3,616 million, an increase of $86 million. The pipeline calculates seven changes: sales, other revenues, cost of goods sold, selling and administration, research and development, other income and other expense. I left the gross-profit subtotal out of the bridge because adding it alongside its component lines would count part of the movement twice. The seven signed contributions sum to the same $86 million, with a zero residual.

The interface shows those movements as a bridge. A reader can highlight income increases or reductions, select a financial line, inspect its two original values and open the relevant report page. A second view lists the normalized figures together.

The bridge explains the arithmetic. It doesn't establish operational causes. The short brief therefore separates calculated observations from the company's own explanation, which has a separate evidence reference. The comparison also stays within reported IFRS actuals; budget, forecasts, constant-currency measures and core non-IFRS results would require different inputs.

Give the narrative a small, relevant context

This version builds the brief from checked calculations and a bounded set of relevant report summaries. It makes no model requests and has no inference cost. Numbers are produced by financial arithmetic, rather than generated as prose.

During review, the first narrative validator exposed a gap: checking format, numbers and citation identifiers could still accept a numberless sentence that contradicted the facts. A real citation could also be attached to an invented cause.

I narrowed the optional OpenRouter adapter after that review. It is implemented and disabled; its permitted task is to select and order up to three exact approved statements derived from the facts and evidence. Newly written prose is rejected. A live comparison would still need to test whether model selection is useful. No live inference has been run.

Inspect the failure boundaries

The saved evaluation contains 22 deterministic cases. Alongside reconciliation, they check deliberately wrong signs, mismatched units, incomparable periods, missing or duplicated financial lines and incorrect totals. Brief validation checks the exact approved-statement contract, including rejection of unknown citations, invented metrics, unsupported quantities, a numberless contradiction and an invented cause attached to a real citation.

The interface shows each expected and actual result. Rejecting a deliberately bad input is a successful test. These checks exercise this fixture and its explicit acceptance rules; they do not establish general truth detection or benchmark an LLM's financial reasoning.

A sanitized trace records the calculation stages and the hash of the reviewed source extract. The hash identifies the normalized input, not the bytes of the original PDF.

Connect a service to an inspectable interface

The backend uses Python and FastAPI. The interface uses React and TypeScript within this Next.js site. Refreshing the calculation requests the Python service through the site’s API. A saved result keeps the same source figures, chart and checks available when that service is unavailable.

The result is a working prototype with a narrow question and visible working. Broader ingestion, automatic extraction, private financial data, forecasting and production deployment remain outside its scope.

Expand the same report, without losing the source

The first bridge covered one quarterly change. I wanted to see what else the same document could explain. I used Codex and specialist agents to review all 54 pages, then built a curated dataset from selected financial tables.

The wider explorer compares quarterly and full-year figures, product sales, regions and therapeutic areas. It also follows cash from opening to closing balance, separates reported IFRS income from the company's core measure, and keeps the balance sheet tied to its year-end dates. The source map shows which pages supplied structured figures and which were reviewed for context.

A small detail mattered with newer products: some prior-year cells are blank. Turning those blanks into zero would create a growth comparison the report does not support. I kept them unavailable. Currency rates, share counts and earnings per share also retain their own units rather than being forced into USD millions.

The page uses the same separation as the original prototype. Python builds the financial views and checks their identities. React lets the reader choose periods and inspect the result. The refresh control calls the real FastAPI service; browsing the saved result is labeled separately.

See the source, request flow and exact calculation code. That walkthrough also explains what was manually transcribed, what the software calculates and what has not been tested with an LLM.

Project sources & checks
Public source report ↗

Novartis Q4 and full-year 2025 report. The consolidated income statement is on page 17; operating-performance commentary is on page 5. Data period: Q4 2025 versus Q4 2024, reported IFRS, USD million.

Interactive calculation ↗

Inspect the seven contributions, original figures, page references, validation cases and sanitized calculation trace.

Saved result ↗

The saved review contains the normalized source facts, zero-residual bridge, deterministic brief and 22 evaluation case results. It records zero model calls.

On this page
Start with a comparison that can be checkedReconcile before writingGive the narrative a small, relevant contextInspect the failure boundariesConnect a service to an inspectable interfaceExpand the same report, without losing the sourceSources & checks
PythonFastAPIReactTypeScriptFinancial analysis

Connected to this

All writing
Product-description comparison bench · Experiment

Comparing one model call with separate vision and writing steps

Transfutur ERP · Experience

ERP for transport and logistics

Ven PetrovSoftware engineer · Barcelona
GitHub ↗LinkedIn ↗Email ↗

Find a project or a note

17 results

Transfutur ERP · ExperienceERP for transport and logisticsAs the sole engineer, I built an ERP from zero for transport and logistics. It supported roughly €5–6 million in annual invoicing across two offices, and I kept it running for about a year.Financial review lab · Project storyExploring a financial report with Python and FastAPII started with one income change, then opened up the same 54-page report: products, regions, cash flow, core adjustments and a dataset whose figures lead back to the source.Crude-oil narrative tracker · Project storyCrude-oil research from global news and market dataI wanted help following global events that could matter for crude oil. The prototype combines news research with calculated market and position values, while keeping recent assessments separate from the longer numeric history.MNQ time-series research · ExperimentTime-series prediction experimentsAn exploration of high-resolution market data: choosing what to acquire, checking its limits, comparing time intervals and testing whether a prediction survived later data and the cost of acting on it.NQ/MNQ backtest engine · Technical noteA missing closing bar left the simulator’s short position openThe strategy reset for a new day while a shared counter still counted yesterday’s short. A small replay shows why closing the position has to happen before clearing its state.Small-cap edge research · Project storyTurning small-cap trading ideas into repeatable researchI asked Claude Code for a fresh research system that could test named price patterns, compare later periods and carry its methodology into new sessions. One candidate shows how features, entry timing and costs fit together.Gadgeon Europe applications · ExperienceConnected-device dashboards and mobile apps at Gadgeon EuropeI worked on Ionic and Angular applications that brought several APIs into dashboards for energy monitoring and connected-device control, with releases and maintenance across multiple client versions.ctxforge · Project storyctxforge: discovering what to build before codingA context discovery framework that asks an LLM to act like a senior engineering interviewer: understand the idea, uncover requirements and agree the scope before an agent starts building.Sociedades Inactivas enquiries · Technical noteSaving an enquiry before trying to send its emailOn the Sociedades Inactivas catalogue, a visitor can ask about a company or send a general enquiry. The form separates saving that request from delivering its notification, including when the visitor retries.Astro local-services website · Project storyA local-services site assembled from city, service and FAQ recordsThe Astro site describes flooring and other local services across different cities. Shared templates check availability, read the matching content and select one FAQ set for both visitors and structured data.3D Spaces website migration · Project storyMoving 3D Spaces to Next.js while its email stayed on the old serverI rebuilt the virtual-tour website with AI coding collaborators, preserving its content and URLs. The cutover also had to protect the mailbox, enquiry flow and existing analytics history.Capture CRM · Project storyConnecting classified listings to contacts in a small CRMA Chrome extension and Next.js dashboard bring classified listings, platform profiles and contact history together. Saving a listing again can add the person who was missing from an earlier import.Product-description comparison bench · ExperimentComparing one model call with separate vision and writing stepsA small product-description tool lets me inspect the visual analysis before the copy is written, change the model used for each step and compare request measurements.Between Us · Project storyBetween Us: why I kept the first take and widened the shotI compared dialogue takes, asked for wider shots and more physical acting, then changed the workflow when its studio limited the scenes I wanted to make.Restaurant-service voice agent · ExperimentDesigning a restaurant-service call around the real offerI worked through a Spanish outbound agent prompt, corrected a free visit to the actual paid service and compared how two drafts handled transfers and bookings.Vapi and Telnyx voice integration · Technical noteThe Vapi agent could speak, but it could not hear me clearlyMy Telnyx number connected to the assistant, but the caller audio was distorted. Three codec tests recovered the connection without fixing that original problem.Warloop: Echoes of Solmara · Project storyWarloop: turning generated character art into a playable prototypeGood-looking character images became tiny figures, a backwards walk and misplaced bosses in the game. I worked through the gap between generated art and playable behavior.
esc to close · Tab to move through results