How I integrated an LLM into SahamLens without leaking portfolio data
This post explains the two-stage LLM pipeline behind SahamLens: local summarisation first, remote analysis second. The goal was to add AI-assisted stock research without sending portfolio positions, trade history, or personal investment context to the model API.
May 25, 2026·11 min read·sahamlens, llm, local-first, python
How I integrated an LLM into SahamLens without leaking portfolio data
SahamLens is a local-first trading intelligence platform that I built for Indonesian stock investors. From the beginning, one architectural constraint was non-negotiable: portfolio data, trade history, and personal research notes should remain on the user’s machine by default.
That requirement shaped how I approached LLM integration.
For many AI features, the easiest implementation pattern is straightforward: collect the application context, send it to an external model API, and render the response. It is simple to ship, easy to iterate on, and works well for products where the data is already cloud-native. For SahamLens, that pattern was the wrong fit. The application stores information that I would not want to hand over to a third-party API on every request, including current positions, watchlist context, journal entries, and portfolio-level reasoning.
Instead of treating the model as something that can see the entire workspace, I designed the integration around a strict boundary: the LLM should only receive a derived market summary that is useful for analysis but does not expose the user’s private portfolio state.
The result was a two-stage pipeline: local analysis first, remote LLM interpretation second.
Why the usual LLM integration pattern was a bad fit
The default architecture for LLM-powered products is usually some variation of this flow:
gather all relevant context from the application
send that context to the model API
receive a response
display or store the result
For a writing assistant or general productivity app, that is often acceptable. For a local-first investment workspace, it creates an unnecessary privacy problem. If the request payload contains portfolio holdings, entry prices, transaction history, personal notes, or trade journal content, then the external model provider effectively becomes part of the data boundary of the product.
Even if the provider has reasonable retention policies, that still means the privacy model depends on a third party’s operational guarantees rather than on the application’s architecture. I wanted the opposite: the application should preserve the boundary by construction, so that sensitive user data is excluded before the LLM layer is involved at all.
That decision forced a different integration design.
The boundary I wanted
Before writing the integration code, I defined a simple rule:
The LLM may see market context and machine-generated summaries, but it must not see raw portfolio positions, trade history, journal entries, or any user-specific investment records.
That rule sounds obvious in hindsight, but it matters because it changes the shape of the system. Instead of asking “how do I send enough context to the model?”, the question becomes “what is the smallest useful context I can derive locally that still allows the model to produce a meaningful analysis?”
On this page
That shift led to a pipeline where the local engine is responsible for the heavy lifting of data preparation, while the LLM is responsible for interpretation and synthesis.
The architecture I implemented
The final design uses a two-stage pipeline.
Stage 1: local summarisation and feature extraction
The first stage runs entirely on the user’s machine. SahamLens ingests market data, computes technical indicators, assembles stock-level signals, and produces a structured summary object for each analysis request.
This stage is where the application does the work that should remain private or deterministic:
loading market and watchlist data from the local workspace
computing technical indicators and derived features
scoring or ranking tickers based on internal heuristics
selecting relevant recent headlines or company context
normalising the data into a compact schema for downstream analysis
Most importantly, this is also where the privacy boundary is enforced. The summary object is explicitly designed to exclude anything that is portfolio-specific or user-sensitive. It contains market-facing information, not account-facing information.
A simplified example of the summary payload might look like this:
This payload is useful for analysis, but it does not expose:
current holdings
entry prices
realised or unrealised P/L
trade journal notes
portfolio allocation
personal watchlist rationale
user identity or account metadata
In other words, the model receives a market research brief, not the user’s investment book.
Stage 2: LLM analysis on the derived summary
Once the local summariser produces a safe, structured summary, that summary is passed to the LLM for interpretation. The model’s role is not to compute indicators or access raw portfolio state; its role is to transform the structured market context into a readable analysis brief.
Typical outputs from this stage include:
a short narrative explanation of the current technical setup
a summary of what signals appear to confirm or contradict each other
a description of market context around the stock
a list of risks, caveats, or points that deserve follow-up review
a compact research brief that can be stored locally and revisited later
The response is then stored back in the local workspace rather than becoming the source of truth itself. In SahamLens, the LLM output is one layer of analysis attached to the local research workflow, not a replacement for the underlying data or the user’s own decision process.
Why the split matters
This separation between local computation and remote interpretation turned out to be useful for reasons beyond privacy.
1. It makes the data boundary explicit
The most obvious benefit is that sensitive portfolio data never needs to be serialized into an external prompt. The application decides what crosses the boundary, and that boundary is narrow enough to inspect directly.
2. It reduces prompt noise
If the model receives raw application state, the prompt becomes cluttered with information that may not even help the analysis. By forcing a local summarisation step, the context passed to the LLM becomes smaller, more structured, and more focused on the question being asked.
3. It keeps the deterministic work local
Indicator computation, ranking logic, feature extraction, and summary assembly are deterministic parts of the system. They are better handled by the local analysis engine than by a language model. That keeps the LLM focused on synthesis instead of making it guess at calculations that the application can compute directly.
4. It improves auditability
When the output looks wrong, I can inspect the pipeline in stages:
Was the local market data correct?
Did the summariser produce the right signals?
Did the safe payload omit something important?
Did the model misinterpret the summary?
That is much easier to debug than a single opaque request containing the entire application context.
The actual prompt engineering problem
The technical challenge was not only keeping the data boundary clean. It was also getting the model to produce analysis that was actually useful for Indonesian equities rather than generic “LLM finance commentary.”
The first prompt versions were technically correct but not practically helpful. The output would often restate indicators instead of interpreting them:
“RSI is above 60, which may indicate bullish momentum.”
“MACD shows a positive signal.”
“The stock appears to be in an uptrend based on moving averages.”
That kind of output adds very little if the UI already displays the indicator values. It turns the model into a verbose label generator rather than an analysis layer.
What I actually wanted was a brief that could connect signals into market context. For example:
whether a move looked like broad sector participation or isolated stock strength
whether momentum was supported by turnover or looked fragile
whether recent headlines changed the interpretation of the technical setup
whether the stock was approaching a part of the chart where risk-reward became less attractive
whether the setup looked different in the context of typical IDX liquidity and retail flow behaviour
The prompt therefore had to do more than describe the format of the answer. It had to constrain the model’s frame of reference.
What changed in the prompt design
The best improvement came from changing the prompt from “explain these indicators” to “write analysis in the style of an IDX-focused research brief with explicit market constraints.”
I made three important changes.
1. I added a concrete few-shot example
Instead of only describing the kind of answer I wanted, I included an example of the output style using an IDX stock scenario. That gave the model a reference for tone, structure, and level of specificity.
The example showed that the expected output should:
interpret signals relative to each other rather than one by one
mention risk conditions and invalidation points
refer to liquidity, sector behaviour, or turnover when relevant
This improved output quality more than adding additional prose instructions. The model responded much better to an explicit example of the target analysis than to a long description of how it should behave.
2. I injected IDX-specific context into the system prompt
A lot of finance-oriented prompting implicitly assumes a US market frame: highly liquid large-cap names, heavy institutional coverage, and a different mix of retail participation. That assumption can distort how the model talks about Indonesian equities.
To reduce that mismatch, I added explicit context in the system prompt about the environment the model should assume, including:
that the platform focuses on Indonesian equities listed on the IDX
that liquidity profiles can vary significantly across tickers
that market behaviour may be influenced by retail participation patterns differently from US equities
that the goal is not to map every setup to a US analogy, but to interpret it within the context of the local market
This did not magically make the model an IDX specialist, but it reduced the tendency to default to generic US-market framing.
3. I constrained what “useful analysis” means
I also tightened the instruction set around what the model should and should not do. For example:
do not merely restate indicator values that are already present in the summary
prioritise interpretation, interaction between signals, and risk framing
mention uncertainty when the setup is mixed or weak
avoid inventing catalysts or fundamentals not present in the provided context
avoid giving absolute buy/sell instructions
These constraints were important because the product is not meant to act like an automated signal engine. The AI layer is there to support review and reasoning, not to impersonate an investment advisor.
A concrete lesson from the prompt work
The most useful prompt improvement was not adding more adjectives or more “be analytical” instructions. It was writing one example of the exact output format and reasoning style I wanted.
That example effectively became a specification for the model’s behaviour. It encoded several things at once:
how detailed the answer should be
how much weight to give to technical vs contextual information
how to phrase uncertainty
how to talk about risk without turning the output into a recommendation
what an IDX-oriented analysis brief should feel like in practice
That was more effective than trying to describe all of those requirements separately in abstract terms.
The single best prompt improvement was replacing a vague instruction with one concrete example of the target analysis style. Once the model could pattern-match the structure of the desired output, the response quality improved much faster than it did from adding more generic guidance.
What I would do again
If I were rebuilding this integration from scratch, I would keep the same sequence of decisions:
define the privacy boundary first
decide what the LLM is allowed to see
design a local summariser that produces only that safe context
treat the LLM as an interpreter of derived signals, not as the system that owns the raw data
That order matters. If the integration starts from the API call and only later asks what should have been excluded, the application ends up shaped around the LLM’s appetite for context rather than around the product’s actual privacy and architecture requirements.
Closing thoughts
The most important thing the local-first constraint did was force clarity. It forced me to separate deterministic analysis from language-model interpretation. It forced me to define what data was actually necessary for the model. And it forced the system to preserve privacy not as a policy statement, but as a property of the architecture itself.
In practice, that produced a better design than the simpler “send everything to the model” approach would have. The two-stage pipeline is easier to reason about, easier to test, and easier to extend. More importantly, it keeps SahamLens aligned with the original purpose of the product: a personal investment workspace where the user owns the workflow and the data never needs to leave the machine unless they explicitly choose otherwise.