What transfers
Full source and its complete commit history — single author, no third-party contributors.
106 markdown files, including a written justification for each of 166 engine constants.
Logo and icon set.
The live publishing channel, transferred with the sale.
30 days of transition support.
What does not transfer: the credentials
A market-data API key and a Telegram bot token live in .NET user secrets, outside the repository. Nothing is unbuilt behind them — they are personal accounts, and you supply your own. Both are free to obtain.
The database, and what it means for the track record
The calibration record is the product's strongest claim — 264 boundary breaches in 4,975 scored EUR/USD readings, 5.3% against a declared 5%, plus 220 in 3,922 for gold. That record lives in the database, not in the code.
Which means there is a choice to make, and it should be made explicitly rather than assumed:
You inherit the evidence the sale was made on — 8,913 scored outcomes, ready to keep accruing from day one.
The engine is identical, but the public track record starts at zero and takes months
to earn back. backfill shortens that by scoring the past in one sweep —
the live portion cannot be recreated.
There is no wrong answer. There is only an answer that has to be
agreed. ApiKeys is empty at handover — any key you mint
afterwards is a live machine credential of yours.
Settings with no safe default on your infrastructure
The full reference lists every configuration section the code reads. These are the ones that will stop you.
Every host must name the same file. See the next section — this is the one that costs an afternoon.
The only way anyone becomes an operator. It ships empty, which is the
safe state: AdminAccess fails closed, and the admin page tells you it is
unconfigured rather than pretending your account lacks permission. Add your own address
and sign in.
The dashboard refuses to start in production without it, by design. Unset, cookie keys are regenerated per process and every restart signs out every subscriber with nothing in the logs to explain it. Point it at a persistent volume.
Live market data. Unset, it falls back to a bundled CSV provider — fine for evaluation, development-only in production terms.
Until set, every ops endpoint answers 503.
Billing is built end to end and activates when all three are set. Until then the dashboard detects that, routes upgrades to manual activation, and says so to the user.
Unset, password-reset emails become log lines — the reset flow silently does nothing.
Only if you want the Telegram channel. Everything else runs without it.
Two rules about where values live
Secrets belong in environment variables or user secrets, never in
appsettings.json. Nothing sensitive is committed in this repository,
and it is worth keeping it that way.
And .NET loads user secrets in the Development environment only for the API and dashboard hosts; the bot host reads them in any environment. On a server, set everything through environment variables rather than relying on the secret store.
The one thing that catches people
All three hosts must point at the same SQLite file
They ship wired together by relative path, which works when they run from their project folders on one machine. On a server, set all three to one absolute path.
Getting this wrong does not error. Each host quietly attaches to its
own quant.db, sign-up works, no reports ever appear, and nothing in the logs
looks wrong.
ConnectionStrings__Quant=Data Source=/data/quant.db
Behaviours that read as bugs if nobody tells you
"Generate report" needs two processes, not one
The dashboard serves the page and queues the work. The process that performs it is a hosted service inside the bot host. Run the dashboard alone and every request is accepted, stored, and never executed.
# both, not one
dotnet run --project QuantMarketEngine/src/QuantMarketEngine.Dashboard --urls http://localhost:5199
dotnet run --project QuantMarketEngine/src/QuantMarketEngine.TelegramBot
And the report does not appear the instant you press the button. The page returns immediately and says the instrument is being analysed. The worker picks the job up and takes five to ten seconds. Then you refresh — there is no live update and the page does not poll.
Someone who presses the button and stares at an unchanged table will conclude it did nothing. That is the single most likely first impression to get wrong.
The bot host is not running.
Today's reading exists; nothing is re-run.
The pre-publication checks raised a critical issue — the reading is held, not published.
That account's plan delays same-session access. It affects the scheduled feed, not a session the account requested itself.
The human approval gate — the precise claim
Every report the engine publishes on its own schedule waits for a person, and a reading its own checks call unfit is never published by any route.
One route is deliberately exempt: a report a subscriber requests from the reports page is generated and published without waiting for an operator. That is an owner decision rather than an oversight — a subscriber who has to wait for someone to notice cannot use the button. Its pre-publication checks still apply, so a stale bar never goes out as today's market.
The scheduled run, the event-driven watcher and the operator's own "Report now" all stop at the gate.
PDF export needs a browser on the host
It prints through headless Chrome or Edge. The Docker images do not include
one, so PDF export is off in a default container until a browser is installed and
PdfExport:BrowserPath is set. The link hides itself, the endpoint answers 503,
and the text and JSON exports are unaffected.
In a container you may also need PdfExport:NoSandbox=true — Chrome's sandbox
needs kernel namespaces a container running as root usually does not have. It is off by
default because the flag makes the feature work everywhere and weakens it everywhere, which
is the operator's decision rather than a default to inherit.
The PDF is English only. Arabic in a PDF needs bidirectional layout, and the rasteriser reverses digits inside a mixed Arabic/Latin run. The Arabic report is delivered as text instead.
The dashboard screens are English; the content is not
Report downloads on the platform are English by design. Arabic is still produced
everywhere else — on disk, on Telegram, on the public site, and from the API's
?lang=ar. Bilingual rendering from one engine is one of the product's buyer
profiles, not something that was removed.
The security policy forbids all JavaScript
script-src 'none' is correct today because the dashboard is static
server-rendered Blazor with no JavaScript at all. Adding
@rendermode InteractiveServer to any component will break the policy
until it is widened. This is stated in the middleware and covered by a test, so you find out
at build time rather than in production.
Known limits, stated plainly
- No customers and no revenue. This is an asset sale.
- The billing integration has never processed a real transaction. Stripe hosted Checkout, the hosted Billing Portal and a signature-verified webhook are all implemented behind a provider interface, and card data never touches the platform. The gap is a live card, not the code.
- Silver runs on the engine but is not scored yet. Its feed carries no intraday range, so several readings are deliberately withheld rather than estimated, and its calibration still reads "sample too small". Gold shares the feed shape and settles at FAIR market quality. This is a data limit, not an engine limit — more history does not move it; a provider with intraday movement does.
- The probability cone is the weakest surface, and the report says so itself. EUR/USD's 20-day lower edge runs 6.3% against 5%; gold's upper edges run worse. The daily VaR — the headline figure — passes on both.
- Three sections ship disabled: fundamental analysis, the event-driven trigger, and parts of the economic calendar. All three are configuration values rather than missing code. The calendar currently carries 14 central-bank and US releases through 17 December 2026, after which it goes quiet until the list is extended.
- 38 of 166 engine constants have no written justification. They are catalogued as such, the count is published rather than hidden, and none of them reaches the client-facing page. The other 128 carry their derivation or their measurement.
Your first hour — how to check all of this yourself
Nothing above asks to be taken on trust. These commands run against the repository you receive.
The track record, read straight from the database
cd QuantMarketEngine/src/QuantMarketEngine.TelegramBot && python -c "
import sqlite3
c=sqlite3.connect('quant.db')
for s, in c.execute('select distinct Symbol from AnalysisOutcomes order by Symbol'):
t=c.execute('select count(*) from AnalysisOutcomes where Symbol=?',(s,)).fetchone()[0]
b=c.execute('select count(*) from AnalysisOutcomes where Symbol=? and VaRBreached=1',(s,)).fetchone()[0]
if t>=200: print(f'{s}: {b}/{t} = {b/t*100:.1f}%')
"
The test suite
cd QuantMarketEngine && dotnet test --nologo -v q
The size of what you are buying
Sum the totals rather than taking the last line — a batched wc will
otherwise report one batch as the whole repository.
git rev-list --count HEAD && git ls-files '*.cs' '*.razor' | xargs wc -l | grep -w total | awk '{s+=$1} END {print s}'
Dependency vulnerabilities
cd QuantMarketEngine && dotnet list package --vulnerable --include-transitive
The engine, with no account anywhere
The bundled CSV provider and the historical bars ship with the repository, so the engine generates real reports offline before you register with any data vendor.
cd QuantMarketEngine/src/QuantMarketEngine.TelegramBot dotnet run -- report # writes 5 files per symbol into reports/ dotnet run -- audit # the calibration cards — the engine grading itself