Why I Built "BullSheet"
Problems with Existing Stock Screeners
Update: Part 2 , Part 3 and Part 4 is Live
I have been sharing updates about my internal finance engine, BullSheet on my LinkedIn and a lot of people asked me for details on how it works. So, I decided to explain BullSheet publicly here.
Let me introduce myself quickly.
I have a Master’s degree in Computer Science and a Bachelor’s degree in Mathematics. I’ve been working as a backend and infrastructure engineer since around 2008 and am currently based in Berlin. But beyond my day job (or lack thereof recently), I’ve been an active investor. That journey is years of learning, years of trying different things and acknowledging some expensive lessons.
You can reach out to me via:
Email: bullsheet@anar-bayramov.com
Before I built BullSheet, my investment strategy was manual with excel sheets, inefficient, and painfully time-consuming. I had the vision for an automated system for years, but never the time to build the infrastructure. Recently, finding myself unemployed and armed with AI coding assistants, I finally start to build it. And honestly, it turned out much better than I anticipated.
(Disclaimer: This is not investment advice. This is just the logic I use for my own sanity.)
What is “BullSheet”?
First off, the name comes from “Bull Markets” and “Fundamental Sheets” with, obviously, a bit of humor.
“Where is the sign-up link? There isn’t one.
The financial data I use requires a commercial license that I don’t have, so I can’t legally share the tool also it is personalized for my own risk tolerance. I actually pitched BullSheet to YCombinator thinking I could make it a product for retail investors but I got the rejection letter without any details. Who knows maybe they just disliked the humor in the name.
So, BullSheet remains a private, local engine running exclusively for my portfolios.” However, that doesn’t mean I can’t share the logic behind it. Think of this series like one of those engineering blog posts titled ‘How we scaled our backend to 1 billion requests.‘ They don’t give you the source code, but they walk you through the architecture and the lessons learned. That is exactly what I plan to do here. But with more details.
Essentially, BullSheet is:
A comprehensive 14-layer company analysis engine.
A Quantitative Risk Model using probability and statistics.
A Multi-Factor Market Screener.
A Portfolio Risk Manager.
Crucially, here is what it is not:
It is not an algo-trading or day-trading tool.
It does not use AI for the analysis itself. (I used AI to help write the code, but financial data is widely deterministic so calculations are pure math).
It has almost zero technical analysis.
If you expect me to draw some lines on a chart and promise you’ll get rich, you should probably go check out a day trading course. This post and this tool is intended to tell you my opinionated way of fishing, not to hand you a fish.
Why I’m Sharing This?
Honestly, I’m sick of seeing misleading ‘bullshit’ financial advice targeted at retail investors, while the actual professional-grade tools cost a fortune that individuals can’t afford.
I want to share what I know, but I also want to prove a point: Active investing is an extremely complex game. The hard truth is that 99% of you will do much better by simply buying diversified index funds.
For me, this is extreme satisfaction where several passions of mine: mathematics, software engineering and finance crosses.
This is why, I’ve allocated a good budget to test out things, and while I’ve been beating the market for 5–6 years in a row, I humbly don’t deny that luck may be involved. But it works for me. And even if everything I do is “bullshit” , the majority of my net worth is safely parked in diversified ETFs. So I won’t go broke in a month.
Problems with Existing Stock Screeners
Here is a screenshot of BullSheet Screener with redacted data.
One of the most common questions I get is simply: Why build this? Why not use any of the dozens of existing screeners out there?
I will be explaining problems with simple ratios so it will be easier to follow.
Problem 1: The “True/False” Trap
The most fundamental flaw in almost every standard tool is: Boolean Logic.
Most screeners work on a binary system. You tell them what you want for example,
P/E ratio below 15
Revenue growth over 20%
and they give you a list. The data is technically “correct,” but it’s practically too raw.
Imagine you run that filter and end up with 50 companies. Boolean logic treats all 50 of those companies exactly the same. They all passed the test.
Problem 2: How do you know which filtered companies are prior to the other?
Company A might be barely scraping by with a P/E of 14.9, while Company B is a powerhouse trading at a P/E of 8. How do you know which one has better valuation? How do you rank them? Only by P/E ? What if P/E is great but P/B is not?
Company A: P/E 15 and P/B 3
Company B: P/E 15 and P/B 2
Company C: P/E 35 and P/B 1
Since Company A and Company B has same P/E Ratio, you can simply check P/B ratio but Company C has different P/E and P/B. Traditionally speaking Company C has better P/B Ratio than the others but it has higher P/E ratio. It is called Metric Conflict. It can be solved by weighted scoring system. which BullSheet screener essentially doing.
Problem 3: The Baseline Bias
Standard screeners don’t give you a score; they just give you an unranked bucket of companies.
Once you apply strict filters (like excluding high P/E stocks), you create a list so specific that you can no longer compare it to general benchmarks like the S&P 500. You have effectively created a custom micro-market of your own, so you need a custom index to track it.
The Example: A Tiny Tech Market
Imagine a market with just 5 companies. Let’s call this the “Tech Index.”
Company A: P/E 50
Company B: P/E 25
Company C: P/E 30
Company D: P/E 100 (Hype stock)
Company E: P/E 150 (Mega hype stock)
If you look at this market as a whole, the average P/E ratio is 71.
(50 + 25 + 30 + 100 + 150) / 5 = 71
Using this standard benchmark, Company A (P/E 50) looks “cheap.” After all, 50 is significantly lower than the market average of 71. A standard screener would likely flag this as a “Value Buy.”
Applying the Filter
Now, let’s apply a logic filter: No Hype.
“I want to exclude any company with a P/E over 60 because it’s too risky.”
Company D (100) → Excluded
Company E (150) → Excluded
The “BullSheet” Realignment
Now that we have filtered out the noise, we are left with a specific subset of prudent companies. We must recalculate the baseline for this specific universe.
(50 + 25 + 30) / 3 = 35
The Result
Look at Company A again.
Compared to the Standard Market (71), Company A (50) looked Cheap.
Compared to My Custom Index (35), Company A (50) is actually Expensive.
Problem 4: The “Hard Number” Fallacy (Why Context Matters)
Another major issue is “standard investing advices” and the screeners that support this obsession with “Hard Numbers.
You hear it all the time: “Best stocks are with a P/E ratio below 15.”
That sounds like prudent advice, but it ignores several critical things such as: Sector, Market Regime or Interest Rates.
A P/E of 25 might be expensive for a Utility company, but it might be incredibly cheap for a High-Growth Tech company. But more importantly, the “correct” ratio changes based on the market cycle.
In a Bubble Market: Almost every decent company will have a P/E well above 20 or 30. If you strictly filter for “P/E < 15,” you will either find nothing, or you will find “value traps” companies that are cheap because they are dying.
In a Bear Market: Prices crash, and earnings often lag. A P/E of 15 might actually be expensive when the rest of the market is trading at 10.
Standard screeners don’t adjust for this. They are hard coded. Active investors always buying and selling regardless of market cycles. So I needed a system that calculates these averages dynamically. I need to know: “What is the average P/E for Tech stocks RIGHT NOW?” If the average is 35, and I find a solid company trading at 25, that is a relative bargain even if the old textbooks say 25 is “expensive.”
Problem 5: The Linear Trend Illusion (Consistency vs. Volatility)
Finally, there is the problem of how trends are calculated. Let's talk about growth rate as an example.
Most tools use CAGR (Compound Annual Growth Rate).
CAGR is quite useful, but it’s also not fully accurate. It only cares about the starting point and the ending point. It ignores the journey. E.g CAGR 5Y
Let’s say I want to find a stable, compounding company. I want to see revenue that increases consistently year over year:
5 Years ago: 10 Billion
4 Years ago: 11 Billion
3 Years ago: 12 Billion
...and so on.
This shows a predictable, linear trend.
But if you use a standard screener to find “20% Growth over 5 Years,” it might give you a company that did this:
Year 1: 10 Billion
Year 2: 20 Billion (Huge spike due to a one-off event)
Year 3: 8 Billion (Massive crash)
Year 4: 12 Billion (Recovery)
Both companies might show the exact same CAGR over a 5-year period. But one is a steady compounder, and the other is a rollercoaster.
Standard screeners usually can’t tell the difference between “consistent growth” and “volatile noise.”
Building a Scoring System, Not a List
To solve things above, I needed a scoring engine. I needed a way to take my filtered companies and force them to compete against each other.
I wanted a system where a company isn’t just “good” or “bad.” It gets a score based on a weighted mix of criteria. I broke it down into layers 14 of them, to be exact.
The Main Layers of the BullSheet
(Disclaimer: This is not exact filters I’m using. This is just a logic I share for demonstration purposes)
The process is split into two main layer: Hard Filters and Weighted Scoring.
1. The Hard Filters (The Bouncer)
To minimize noise, I immediately exclude the Banking, Minerals, and Healthcare sectors from my analysis. I lack the enough expertise required to value these industries accurately, so I focus exclusively on sectors within my circle of competence. which I encourage everyone to do so.
Besides I add special “sanity checks.” If a company country has a currency risk, low volume (I buy in relatively big chunks, low volume causing higher spreads), market cap (I personally don’t invest in companies below 2B market cap), etc.
2. The Weighted Score
Once I have a clean list, I score them. But not all metrics are created equal. For example:
Cash & Safety: If a company has a great balance sheet, that might contribute 15% to the score.
Technical Analysis: If the price is above the 200-day EMA, it implies a bull trend. I might weight that at 10%.
P.S: I do not believe in TA, except some statistics e.g EMA
Sector Rotation: This is crucial. Even a good company will suffer if its whole sector is tanking. I calculate a “sector penalty” to drag down the score of stocks in failing industries. E.g Tariffs affected certain industries more than others so they get penalized by me too. E.g if Score of Company B is 75/100 while all sector is tanking. I penalize them to lets say 70/100
Sentiment: Earnings surprise, analyst ratings (I generally ignore them but it is a simple sentiment metric that can be included in simple example)
The final result isn’t a “Yes/No.” It’s a specific score (e.g., 85/100). This allows me to look at the top 50 companies and know why they are there.
df["Final_Score"] = (
(df["score_valuation"] * 0.30) +
(df["score_quality"] * 0.20) +
(df["score_technicals"] * 0.15) +
(df["score_sentiment"] * 0.15) +
(df["score_momentum"] * 0.20)
) * 100P.S: I have several weighted scores in BullSheet. Ratios varies by different holding periods. E.g if you will rotate your investments every week. EMA 200D is way too wide or some valuations will be too constant since values announced quarterly. While for an annual holding period EMA 10D is a noise(it is generally a noise any way)
The Code Logic
I’m an engineer, so I think in code. While I won’t share the exact proprietary formula, here is a Python example that most simplistic way how the logic flows.
Notice how I don’t just filter, I calculate a weighted average based on what matters most (Value, Quality, Momentum, etc.).
import numpy as np
class CustomRanker:
def generate_scores(self, df):
# 1. HARD FILTERS (The Bouncer)
# Remove sectors I don't understand or stocks that are dangerous
df = self._apply_sanity_filters(df)
# 2. CALCULATE COMPONENT SCORES
# We don't just look at P/E. We look at Value, Momentum, Quality, and Safety.
df = self._score_valuation(df) # e.g., PEG Ratio, Forward P/E
df = self._score_quality(df) # e.g., ROIC, Margins
df = self._score_technicals(df) # e.g., Price vs 200 EMA
df = self._score_sentiment(df) # e.g., Analyst Ratings, Earnings Surprise
# 3. CALCULATE SECTOR PENALTY
# If the whole sector is crashing, punish the stock score.
df["sector_penalty"] = self._calculate_sector_drag(df)
# 4. THE FINAL WEIGHTED FORMULA
# This is where the magic happens. We decide what matters most.
df["Final_Score"] = (
(df["score_valuation"] * 0.30) +
(df["score_quality"] * 0.20) +
(df["score_technicals"] * 0.15) +
(df["score_sentiment"] * 0.15) +
(df["score_momentum"] * 0.20)
) * 100
# Apply the sector penalty to the final score
df["Final_Score"] = df["Final_Score"] - df["sector_penalty"]
# Clip negative scores to 0
df["Final_Score"] = df["Final_Score"].clip(lower=0)
return df.sort_values(by="Final_Score", ascending=False)
def _apply_sanity_filters(self, df):
# Example: Exclude specific complex industries
exclude_sectors = ["Banking", "Mining", "Utilities"]
df = df[~df["Sector"].isin(exclude_sectors)].copy()
# Example: Must be profitable (EBITDA > 0) and not over-leveraged
df = df[
(df["EBITDA"] > 0) &
(df["Interest_Coverage"] > 3)
].copy()
return df
def _score_quality(self, df):
# Rank by ROIC (Return on Invested Capital)
df["score_quality"] = df.groupby("Sector")["ROIC"].rank(pct=True).fillna(0)
return df
def _calculate_sector_drag(self, df):
# If the sector is down over the last 3 months, apply a penalty.
# We don't want to catch falling knives even if the stock looks cheap.
sector_perf = df["Sector_Perf_3M"].fillna(0)
# If sector is negative, multiply the drag.
# e.g., -5% performance becomes a 10 point penalty.
penalty = np.where(sector_perf < 0, abs(sector_perf) * 2.0, 0)
return penaltySummary
Technically, you could achieve a stripped-down version of this in Excel.
I actually started with a spreadsheet myself. But BullSheet has evolved into more than just a screener. There are complex probabilistic calculations and dynamic baseline adjustments happening in the background that would turn any spreadsheet into a nightmare. As an engineer, writing clean Python is infinitely easier (and safer) than managing a fragile, 50-tab Excel monster.
What’s Next?
By running this engine, I end up with a final list of companies that are balanced across the criteria I actually care about.
However, a high score is just the starting point. I can’t just blindly buy the top 10 ranked stocks. If the algorithm spits out 10 Semiconductor companies at the top, buying them all isn’t investing, it’s betting on a single industry. If that sector takes a hit, my portfolio gets wiped out.
I still need to risk-averse the portfolio.
In the next post, I’ll maybe explain more details or different part of BullSheet. I prefer keeping it casual.
See you soon.



This is very interesting. May I ask where you get the data from?