Daniel Orsi

Finance student · portfolio, risk and data analysis · Toronto

Hey, I’m Daniel.

I studied finance at York University in Toronto, and I’m spending this year on pure math courses before a master’s in quantitative finance. On the side I build projects in portfolio construction, risk analysis and trading research, mostly in Python, SQL and Excel. Some of it worked and a lot of it didn’t. It’s all here. I’m looking for a junior analyst or portfolio management role.

1.20
Sharpe ratio of a low-risk stock portfolio from my paper. The S&P 500 fund got 0.93
83
horse-racing betting angles I tested. The odds had already priced them in
298
tests on the poker bot, which is currently playing online
0
libraries in the neural network I wrote in C++. I did the math by hand

Projects

01
Portfolio construction · research paper · 2026

Building lower-risk stock portfolios with cleaner correlations

Portfolio managers rely on how stocks move together, but most of a correlation matrix is noise. I tested whether filtering that noise out builds a better portfolio, on 421 S&P 500 stocks from 2010 to 2026, and wrote it up as a paper.

1.20
Sharpe ratio of the long-only minimum-variance portfolio (S&P 500 fund: 0.93)
27% → 10%
portfolio volatility out of sample, before and after cleaning
73%
of weeks where the correlations had measurably shifted
≈ 3 bps
trading cost at which the short-term reversal signal stops paying

Same stocks, cleaner covariance

$1$2$42014201620182020202220242026
Growth of $1 · 2026-07-10 (latest)
Min-variance, cleaned covariance$7.14 · Sharpe 1.18
SPY$7.61 · Sharpe 0.93
Daily returns from the paper’s iteration 11, 421 S&P 500 names, monthly rebalance, 5% name cap. Sharpe is full-period, net of 5 bps one-way costs. Universe is current constituents, so absolute returns carry survivorship bias; the comparison within the universe does not.

How it works. Drag the sliders

λ₊ = 1.55λ = 11.8 →λ = 7.0 →λ = 5.7 →01234
q = N/T0.33Planted3Detected above edge3
Simulated in your browser: 60 synthetic stocks over 180 days. Grey bars are eigenvalues of the sample correlation matrix, the blue curve is the Marchenko–Pastur density pure noise would produce, and anything past the red edge is treated as signal. The noise variance is refitted after removing the signal, as in the paper. Shrink T/N or weaken the factors and watch real structure sink into the noise bulk.

The correlation structure never sits still

04820122014201620182020202220242026approx. 5% significanceepisode threshold, z = 5.3
Week ofhover the chartWeeks rejecting “no change”73%
Permutation-calibrated z-score comparing 126-day windows before and after each week. Almost every week clears the usual significance bar, so “breaks” are defined against the drift distribution itself: 14 episodes in fifteen years, marked in red.

Short version: the cleaned matrix helps a lot when you use the whole thing, like building a minimum-variance portfolio. Boil it down to a few features for a volatility forecast and it makes a decent baseline worse.

I also checked whether the Marchenko–Pastur edge is the right number of factors to strip out for a reversal strategy. It’s fine, but not better than just picking a generous fixed number. The reversal effect itself is real (z ≈ 22) and worth about 1.3 basis points a day, which is nothing once you pay to trade it.

Built with
  • Python
  • NumPy
  • SciPy
  • statsmodels
  • pandas
  • scikit-learn
02
Options strategy · risk management · 2026

An options strategy that sells volatility, carefully

This started with me trying to simulate realistic index returns and turned into an options strategy that’s been spitting out a signal every weekday since July. My first version looked great and turned out to be wrong.

0.93
Sharpe ratio in backtest, at +9.0% a year
−0.7%
what the original iron condor earns once priced on the real volatility smile
4 of 4
predictions written down in advance and confirmed on new data
Daily
unattended forward test since 1 July 2026

The forward test so far

Implied volatilityMy HAR forecastRealized, once the window closed
10%15%20%25%30%07-0107-0707-2710-0310-2111-05
Hover a column for that day’s signal.Signal on the right side11 of 13 settled
The live forward-test log, 2026-07-01 to 2026-11-09: 23 signals, 13 with their one-month window closed. A small sample, shown as is. A signal counts as right when realized volatility landed on the side of implied that it bet on.

I went through a bunch of return models along the way: plain GBM, Heston with jumps (calibrated with CMA-ES), a neural SDE, Hawkes processes for crash clustering, and a classifier whose only job was to tell my fake data from the real thing.

The first strategy was an iron condor that backtested at a Sharpe of 1.8. Then I priced it on a realistic volatility smile with real costs and it lost money, so that was that. What’s left is a short-dated put spread with a tail-risk filter. It’s on a paper account until the fills look like the backtest.

Built with
  • Python
  • PyTorch
  • arch
  • statsmodels
  • CMA-ES
  • ib_insync
  • systemd
03
Trading system · market structure · 2026

An arbitrage system that worked, but couldn’t pay for its servers

Six days in August building a system that spots price gaps between Solana exchanges and trades them. It worked and the trades it won made money, just not enough to cover the infrastructure. Working out why it lost most races was the interesting part.

≈ 400 µs
from seeing a swap to sending the trade
0.000 bps
pricing error against on-chain simulation, on all six venues
8 of 2,755
trades won on the main night of live running
3 regions
servers in New York, Frankfurt and Tokyo

How it works. Click a stage, or press play

One swap, start to finish
Ingest

Raw block fragments (shreds) arrive from the leader and are reassembled into transactions, before the block is finished.

A slower confirmed stream runs alongside as the source of truth.

Each stage is its own Rust crate with one job, and each stamps its own latency so a delay can be traced to a stage. From seeing a swap to sending the trade took about 400 microseconds.

Why it lost: one block, traced

Swap that opens the gapOther botsMy transactions
PRICE GAP BETWEEN THE TWO POOLS03084128176200POSITION IN THE BLOCK →
Transaction 0 · Nothing to trade yet

The two pools agree on the price.

The first 200 transactions of one block I traced by hand. Positions are from the chain; the height of the gap line is only a sketch. Anyone who has to see the swap and then react arrives after the gap is gone.

What I tried, and where it landed

FIRST IN BLOCKLAST
Mine, first measurement · median 838 of 1,519
8 wins in 2,755 sends that night

About 23 successful swaps had already moved the pool by the time my transaction ran.

Where the transaction ran inside its block, read from the chain after each test. Earlier is better. Some runs were recorded as medians and some as means, so the dots are a guide rather than a strict ranking.

The trades were reaching the right block. They were just running late inside it. I tested the possible causes one at a time: paying more, a different route, skipping crowded pools, a lighter transaction. Only the last one moved anything, and not enough.

The bots that win are backrunning. Their trade sits right behind the swap that opens the gap, in the same block, which takes validator-level access I didn’t have. So it was a structural gap, not a code gap. The wins were real money, but too few to beat the cost of the servers, so I stopped there. Losing a race was cheap, at least: a failed trade cost a fee and never the principal. The code is private.

Built with
  • Rust
  • Tokio
  • QUIC
  • Solana programs
  • Latitude
  • TeraSwitch
04
Decision-making under uncertainty · 2026

A poker bot

No-limit hold’em, multiple players. The bot keeps a running guess at what cards everyone is holding, updates it every time someone acts, and searches ahead from there. It’s playing online right now.

25.7k
lines of Python across 142 files, strict mypy throughout
298
tests: unit, property-based, integration and statistical
33
numbered experiments in the log, failures included
+98 bb/100
live over 1,321 hands. Could still be luck at that sample size

Try it: narrowing an opponent's range

AA
AKs
AQs
AJs
ATs
A9s
A8s
A7s
A6s
A5s
A4s
A3s
A2s
AKo
KK
KQs
KJs
KTs
K9s
K8s
K7s
K6s
K5s
K4s
K3s
K2s
AQo
KQo
QQ
QJs
QTs
Q9s
Q8s
Q7s
Q6s
Q5s
Q4s
Q3s
Q2s
AJo
KJo
QJo
JJ
JTs
J9s
J8s
J7s
J6s
J5s
J4s
J3s
J2s
ATo
KTo
QTo
JTo
TT
T9s
T8s
T7s
T6s
T5s
T4s
T3s
T2s
A9o
K9o
Q9o
J9o
T9o
99
98s
97s
96s
95s
94s
93s
92s
A8o
K8o
Q8o
J8o
T8o
98o
88
87s
86s
85s
84s
83s
82s
A7o
K7o
Q7o
J7o
T7o
97o
87o
77
76s
75s
74s
73s
72s
A6o
K6o
Q6o
J6o
T6o
96o
86o
76o
66
65s
64s
63s
62s
A5o
K5o
Q5o
J5o
T5o
95o
85o
75o
65o
55
54s
53s
52s
A4o
K4o
Q4o
J4o
T4o
94o
84o
74o
64o
54o
44
43s
42s
A3o
K3o
Q3o
J3o
T3o
93o
83o
73o
63o
53o
43o
33
32s
A2o
K2o
Q2o
J2o
T2o
92o
82o
72o
62o
52o
42o
32o
22
Hover a hand for its share of the range.
The opponent…
Observed
nothing yet
Effective range
100% of combos
Most likely
AKo AQo KQo AJo KJo
A simplified illustration of Bayesian range updating: each observed action reweights every starting hand by how likely that hand was to take it. The real bot does this over all 1,326 exact hole-card combinations for every opponent jointly, with a weighted particle filter and a fitted action-likelihood model in place of the toy curves used here.

I got the solver code working on tiny poker games first (Kuhn and Leduc), where you can check the answer exactly, before pointing it at the real game. The hold’em search samples opponent hands from the belief and uses a fitted model of how people actually play.

Poker is noisy enough that it’s hard to tell if a change helped, so the benchmark replays every deck from every seat. There’s also a replay viewer where you can watch what the bot thought everyone had. I also built a home-game version where people play against it from their phones.

Built with
  • Python
  • NumPy
  • SciPy
  • PyTorch
  • eval7
  • pytest
  • hypothesis
  • websockets
05
Data analysis · statistical modelling · 2026

Trying to beat the horses

I pulled 706 race days of Woodbine results out of chart PDFs, built a database, and tested every betting angle I could find in the research. My model kept landing on the same horses the expert handicappers picked, before their picks were posted. The pari-mutuel odds had already priced that in.

51,221
starters across 6,322 races and 706 race days
0 of 83
betting ideas that beat the track's cut, over 243 statistical tests
58% → 73%
of the market's skill matched by my model after feature work
0.83
my estimate of a pricing adjustment for second place, new for a North American track

Try it: pricing an exacta

Win odds (to 1)

Track take implied by these odds: 18.2%

Exacta probability, first ↓ then second →
#1#2#3#4#5#6
#1
11.8
9.4
7.1
5.3
3.6
#2
9.1
4.6
3.5
2.6
1.8
#3
6.6
4.2
2.5
1.9
1.3
#4
4.5
2.8
2.3
1.3
0.9
#5
3.0
1.9
1.5
1.2
0.6
#6
1.8
1.2
0.9
0.7
0.5
Hover a cell for the fair price of that exacta.
Plain Harville assumes the race for second is a rerun of the race for first without the winner, which overrates favourites underneath. Raising the remaining probabilities to a fitted exponent corrects it. I estimated 0.83, 0.61 and 0.54 for second, third and fourth at Woodbine, close to the published Hong Kong values.

Most of the work was the boring part: parsing PDFs, 22 validation checks, and making sure nothing in the model could peek at the future. I also ran the tests on fake races with a planted edge first, to make sure they’d find one if it existed.

They would have caught anything that moved a horse’s win chance by about 5%. The track takes 15–26% of every bet, so anything smaller than that wasn’t going to pay anyway.

It didn’t lose badly. The model was reading the races well, and consistently matched the experts’ selections ahead of time. But in a pari-mutuel pool everyone’s money sets the price, so by post time the odds had shrunk or erased whatever edge was there.

Built with
  • Python
  • SQLite
  • pdfplumber
  • pydantic
  • LightGBM
  • PyTorch
  • pytest
06
Machine learning · 2026

A neural network (transformer) in C++ with no libraries

I wanted to actually understand attention, so I wrote one with no framework: matrices, softmax, embeddings, self-attention, an MLP and Adam, all in plain C++, with the gradients worked out by hand.

0
external libraries, and no automatic differentiation
16
hand-derived backward steps, from loss to embeddings
1,390
lines of C++17
10.8M
parameters in the PyTorch GPT built afterwards to compare

Step through the backward pass

Embeddings
·
Q · K · V
·
Scores
·
Softmax
·
Residual
·
Hidden 1
·
Hidden 2
·
Logits
·
Loss
01

dL/dlogits. Softmax and cross-entropy differentiate together into (probabilities − target).

1 / 16
The backward pass of the C++ model, in the order the code performs it. There is no autograd: each of these sixteen gradients is derived and written out by hand.

It’s small on purpose: one attention head, one block, trained a character at a time on Shakespeare. The hard part was the math, mostly the softmax Jacobian and keeping track of the four different paths the gradient takes back to the input.

I did a from-scratch MNIST network in C++ before this, and afterwards rebuilt the same model in PyTorch to check my math, then scaled it up to a six-layer GPT.

Built with
  • C++17
  • Make
  • PyTorch

Project index

Everything else

All of it.

  • About 40,000 lines of Rust across 39 crates. Quotes matched on-chain simulation to 0.000 bps and the trade left about 400 microseconds after the swap was seen. 8 wins in 2,755 sends on the main night. The wins made money, but not enough to cover infrastructure: the bots that win most were backrunning from inside the same block with validator-level access.

    RustTokioQUICSolanaRead the case study ↑
  • 142 Python files, about 25,700 lines, 298 tests under strict mypy. Kuhn and Leduc solvers verify the CFR and IS-MCTS machinery against exploitability bounds before it touches the full game. 33 numbered experiments, negatives included.

    PythonNumPySciPyPyTorchpytesthypothesisRead the case study ↑
  • 421 S&P 500 names, 2010–2026. Every claim is judged against a simulated estimation-noise null. The cleaned minimum-variance book reaches Sharpe 1.20 against 0.93 for SPY; the reversal anomaly it isolates is real and unharvestable after costs.

    PythonNumPySciPystatsmodelspandasRead the case study ↑
  • Heston with jumps calibrated by CMA-ES, Hawkes crash clustering, a HAR forecast against implied volatility, a tail-risk gate and Kelly sizing by block bootstrap. A systemd timer has run the signal every weekday since July 2026.

    PythonPyTorcharchstatsmodelsib_insyncsystemdRead the case study ↑
  • A positional PDF chart parser, 22 validation checks, strict no-look-ahead history, a Plackett–Luce attention model and exact joint-outcome Kelly staking. The model matched the expert handicappers' picks before they were posted, but none of the 83 beat the track's cut: the pari-mutuel odds had already absorbed the edge.

    PythonSQLiteLightGBMPyTorchpydanticpdfplumberRead the case study ↑
  • 1,390 lines of C++17. Preceded by a from-scratch MNIST network, and followed by PyTorch twins up to a 10.8M-parameter GPT to check the hand-written maths against autograd.

    C++17MakePyTorchRead the case study ↑
  • Best fit was GARCH(2,2) with skewed-t errors (QLIKE −2.43). VaR forecasts are backtested with Kupiec and Christoffersen coverage tests, with probability-integral-transform diagnostics.

    PythonarchstatsmodelsSciPy
  • Lagged correlation under five dependence measures, Granger tests, permutation validation and time-split backtests. The final audit rejected 269 strategies and graded the remaining 30 as probably overfitted; the recommended portfolio is empty.

    Pythonstatsmodelsscikit-learnhmmlearnpandas
  • An async websocket collector for live books and trades, 30-second features (microprice, spread, imbalance), cost and horizon sweeps, and an event-step MCTS policy.

    Pythonasynciowebsocketspandas
  • 4,994 trading days. Daily changes correlate at +0.29 overall but −0.35 on FOMC days; rolling windows and a hidden-Markov regime model show how unstable the sign is.

    PythonSciPyscikit-learnhmmlearn
  • The leak-proof rerun left 3 of 61 configurations profitable. A cleaner follow-up on AAPL added naive baselines, and nothing beat just guessing the majority class. Good lesson, learned early.

    Pythonscikit-learnXGBoostLightGBMPyTorchStreamlit
  • Websocket collectors, a trades-to-candles builder, a signal loop, and an order API with a documented request collection.

    PythonFastAPIwebsockets

About

I finished my finance coursework at York University in Toronto. I haven’t graduated yet because I’m taking a year of pure math first, with the plan of doing a master’s in mathematical, quantitative or computational finance. I’m also studying for the CSC, CFA and FRM exams.

Everything on this page is stuff I did because I was curious. Usually it starts with a question like “can you actually beat the track?” or “how does attention really work?” and I keep going until I have an answer.

The answer is often no. My first trading model had an amazing backtest that turned out to be a bug, and since then I check my own results pretty hard before I believe them.

In progress
Canadian Securities Course (CSC) · studying for the exam
In progress
Chartered Financial Analyst (CFA) · studying for the exam
In progress
Financial Risk Manager (FRM) · studying for the exam
2026 – 2027
Pure mathematics courses · York University
2021 – 2024
Finance · York University

Finance and portfolio analysis

  • Portfolio construction and asset allocation
  • Financial analysis and financial modelling
  • Equities, fixed income and options
  • Backtesting and performance measurement (Sharpe ratio, drawdown)
  • Market and macroeconomic research

Risk and statistics

  • Risk management and Value-at-Risk (VaR)
  • Volatility forecasting (GARCH, HAR)
  • Correlation and covariance analysis
  • Regression, time-series analysis and hypothesis testing
  • Checking results out of sample before trusting them

Tools

  • Microsoft Excel
  • Python: pandas, NumPy, statsmodels, scikit-learn
  • SQL and SQLite
  • Data cleaning, reporting and data visualisation
  • Machine learning with PyTorch and LightGBM

Programming

  • Python, C++, Rust, SQL, TypeScript
  • Go for orchestration layers
  • APIs and real-time data feeds
  • Automated testing
  • Linux, Git and Docker

Contact

Say hi.

Happy to talk about any of this, or about junior analyst, portfolio management, risk or trading roles.