Home Calibration record Disclaimer Documentation العربية
Metric Risk · QuantMarketEngine

Running the platform

The README covers the engine and its reports. This covers the rest — the subscriber dashboard, the public API, the database they share, and what it takes to put all three on a server.

Everything below was run and checked against this repository, not written from memory.

$6–8 a month to run the whole platform, almost all of it a small VPS

The four hosts

The bot runs on its own. The dashboard and the API are what a customer touches.

TelegramBot console

The engine. Generates reports, scores past readings, publishes to Telegram and to the static site. Also the one-off command runner.

Dashboard port 5199

The Blazor web app: landing page, pricing, docs, sign-up, and the subscriber area — reports, API keys, alerts, billing.

API port 5006

The public REST API that subscribers call with an X-Api-Key.

Migrator console

Applies migrations and seeds the plan catalogue, then exits. For deployments running more than one node.

Prerequisites

.NET 10 SDK. Nothing else — the database is SQLite, created on first run.

Two credentials are yours to supply, and both are deliberately empty in appsettings.json:

AlphaVantage:ApiKey

The market-data provider. Without a key the engine falls back to the bundled CSV provider, so it runs and can be measured offline.

Telegram:BotToken

Only if you want the Telegram channel. Everything else works without it.

Keep them out of the repository. In development:

# from QuantMarketEngine/src/QuantMarketEngine.TelegramBot
dotnet user-secrets set "AlphaVantage:ApiKey" "YOUR_KEY"
dotnet user-secrets set "Telegram:BotToken"   "YOUR_TOKEN"

In production, environment variables: AlphaVantage__ApiKey, Telegram__BotToken.

What you have to sign up for, and what it costs

Two external accounts. Both have a free tier this engine fits inside, and you can evaluate the whole platform without either.

Market data — required for live prices

Who

AlphaVantage by default. Free registration, no card.

What it costs

4 provider calls per symbol per report. Measured, not estimated — the bot prints its own call count at the end of every run.

Free allowance

25 calls a day, which is what the engine's own counter reports against.

So what fits

Three symbols a day is about 12 calls — comfortably inside it. This repository ships configured for three: EUR/USD, gold and silver.

When you'd pay

More symbols, or intraday timeframes. Their paid tiers start around $50/month.

Check their current terms before relying on this

The 25/day figure is what the engine reports against and was true when it was written. An external provider's pricing is theirs to change, not ours. You are not tied to them either — switching providers is a small job, described below.

Telegram — optional

Only if you want the publishing channel. A bot token is free from @BotFather inside Telegram itself, in about a minute. Without it the engine runs completely normally — it writes its reports to disk and to the static site, and simply does not publish to Telegram.

Running with no account at all

CsvFileProvider ships with the repository along with the historical bars, so the engine runs, produces reports and can be measured end to end with no registration anywhere. That is how the calibration studies in this project were produced — every one of them states "zero provider calls". Use it to evaluate the engine before signing up for anything.

The whole monthly bill

Market data — free tier at three symbols$0
Telegram$0
A small VPS$5–7
A domain — about $12–15 a year~$1
TLS certificate — Let's Encrypt$0
Database — SQLite on the same host$0
Total$6–8 / month

Stripe has no monthly fee — it takes a percentage per transaction. Outbound email has free tiers of a few hundred messages a day, far more than password resets need.

Choosing which instruments to cover

EUR/USD, gold and silver are a configuration, not a design. Nothing in the analysis is written for a particular instrument — the layers receive candles and a symbol code, and never ask what the code means.

Where the list lives

In the database, managed from the web interface/admin/symbols in the dashboard. Add an instrument, pause one, resume one; the change takes effect on the next scheduled run with nothing to restart and no file to edit.

Who can reach that page is Admin:Emails in the dashboard's configuration. Leave it empty and nobody has access, including you. That is deliberate: an empty list read as "no restriction" would open the page to every subscriber on a deployment that simply forgot the setting.

Telegram:ActiveSymbols is now only a seed. On a first run against an empty database its contents become the initial rows; after that the table is authoritative and the setting is not consulted again — so switching an instrument off in the browser is not undone by the next restart.

A one-off run can still target a single instrument regardless of the list:

dotnet run -- report GBPUSD 1D

Remember the cost: 4 provider calls per symbol per report. On a 25-a-day free allowance, three symbols is comfortable and six is the ceiling.

Adding one properly

Check your provider carries it, and under what ticker

Your vendor's code may not be GBPUSD. Translate inside the provider; the engine keeps its own naming.

Add it on /admin/symbols

Code and, optionally, a display name. Re-adding one that is paused resumes it — its bars, reports and calibration record come back with it.

Seed its history, once

Press Fetch history on its row — the bot picks the request up within a few seconds and the result appears under Recent work, with the number of provider calls it spent.

The equivalent from a terminal, for every active instrument at once:

dotnet run -- seedhistory   # full daily history, every active symbol
dotnet run -- backfill      # walk-forward scoring — fills the calibration cards

backfill is still command-line only: it is a long sweep over the whole history rather than a request the page should wait on. Without either, the instrument still produces reports — it simply has no track record yet, and says so.

Give it a quoting convention if it needs one

PriceFormatter decides decimal places from the symbol code: anything containing JPY → 3, XAU/GOLD → 2, XAG/SILVER → 3, everything else → 5. That default is right for FX majors and wrong for an index or a crypto pair. It is one small class with one method, and adding a rule is a two-line change.

"Report now" does not publish

It generates a reading and puts it in the review queue, exactly where a scheduled one goes. Nothing reaches Telegram or the site until a human approves it.

This gap is on record

Gold once printed at five decimals — 4021.26444 — because the formatter only knew about JPY. A price a desk cannot read costs more credibility than it seems to.

What a new instrument does not have on day one

A calibration record. The engine will not claim one it has not earned — the calibration page prints "sample too small" until 20 scored outcomes exist for that symbol, and the confidence figure falls back to an absolute reading rather than a rank inside a history it does not yet have. backfill shortens this by scoring the past in one pass; after that the record accrues daily.

One real limit — and it is about data, not the engine

Gold and silver run on a feed that gives one price per day with no intraday range. The consequence is structural and worth understanding before you add similar instruments:

  • Market quality settles at FAIR and stays there.
  • Several readings are deliberately withheld rather than estimated from a range that does not exist.
  • Silver's VaR calibration still reads "sample too small", because the outcomes it can score are fewer.

More history does not fix thisseedhistory will not move it. It is the shape of the data. Fixing it needs a provider that supplies intraday movement, which is a paid tier.

So when you add an instrument, ask what its feed actually gives you. A full OHLC daily bar gets you everything EUR/USD has. A single closing price gets you a working but quieter report — and the engine will tell the reader which one it is rather than paper over it.

Using a different data provider

Nothing ties this engine to AlphaVantage. The analysis layers never see a vendor — they ask for candles through an interface, and the vendor sits behind it. Two implementations already exist, so the pattern is not theoretical: AlphaVantageProvider (HTTP) and CsvFileProvider (files on disk).

What a provider has to supply

Two interfaces, in Domain/Interfaces, deliberately small:

public interface IMarketDataProvider
{
    Task<IReadOnlyList<MarketSnapshot>> FetchAsync(
        string symbol, string timeframe, CancellationToken ct = default);
}

public interface IHistoricalMarketDataProvider
{
    Task<IReadOnlyList<MarketSnapshot>> FetchHistoryAsync(
        string symbol, string timeframe, CancellationToken ct = default);
}

They are separate on purpose: the first fetches only the recent window the live analysis needs, the second returns as much history as the source will give for the walk-forward backtest. One class may implement both, which is what both existing providers do.

And a MarketSnapshot is what you would expect — nothing vendor-shaped in it:

Symbol · Timeframe · Timestamp

Which bar this is.

Open · High · Low · Close

The OHLC.

Volume

Nullable — FX feeds often have none, and the engine does not require it.

DataQualityScore

Your own 0–100 assessment of the bar.

The three steps

Write the class

Put it in Infrastructure/DataProviders beside the other two, and return snapshots sorted oldest-first.

Register it

In ServiceCollectionExtensions.cs, where the two existing providers are registered, point both interfaces at your class:

services.AddScoped<IMarketDataProvider, MyVendorProvider>();
services.AddScoped<IHistoricalMarketDataProvider, MyVendorProvider>();

Map the symbols

Your vendor's ticker for EUR/USD may not be EURUSD. Translate inside your provider — the rest of the engine keeps using its own symbol names.

That is the whole change. No analysis layer, no renderer, no report and no test touches a vendor name.

The pre-publication price check is deliberately not your vendor

Before a report publishes, the gate cross-checks the session's close against the ECB reference rate — free, key-less, and registered outside the provider branch on purpose, so that the feed cannot certify itself. Leave it as it is: if you swap your main provider, that second opinion keeps working and keeps being independent.

A rate-limiting handler is wired for AlphaVantage specifically

If your vendor has a different limit, write the equivalent for it — or drop it if they have none. The engine counts its own calls either way, and prints the total at the end of every run.

If you have your own data

CsvFileProvider already reads OHLC from files on disk. If you have history in CSV — from a broker export, a terminal, or a vendor you already pay for — you may not need a new class at all: point the CSV data directory at your files and the engine runs on them. That is exactly how every calibration study in this project was produced.

The one thing that catches people

All three hosts must point at the same SQLite file

They do not by default. Each would otherwise create its own quant.db beside itself, and you would get a dashboard where sign-up works, no reports ever appear, and nothing in the logs looks wrong.

This repository already wires them together by relative path:

TelegramBot

Data Source=quant.db

API

Data Source=../QuantMarketEngine.TelegramBot/quant.db

Dashboard

Data Source=../QuantMarketEngine.TelegramBot/quant.db

That works when the three run from their project folders on one machine, which is the development layout. On a server, set all three to one absolute path — a volume the containers share, or a path on the host:

ConnectionStrings__Quant=Data Source=/data/quant.db

If you move to PostgreSQL, the Npgsql provider is already referenced.

Running it locally

Three terminals, or run whichever you need.

The engine — generates today's report for every active symbol:

# from QuantMarketEngine/src/QuantMarketEngine.TelegramBot
dotnet run -- report

The dashboard — the site a subscriber signs into:

dotnet run --project QuantMarketEngine/src/QuantMarketEngine.Dashboard --urls http://localhost:5199

The API — what an X-Api-Key calls:

dotnet run --project QuantMarketEngine/src/QuantMarketEngine.API --urls http://localhost:5006

Open http://localhost:5199, create an account, and you land on the free Observer plan.

Migrations and seeding

On a single node you do nothing. Database:MigrateOnStartup defaults to true, so the dashboard and the API migrate and seed on boot.

On more than one node, turn that off — otherwise every instance races Migrate() against the same database at start-up. Set Database__MigrateOnStartup=false on the nodes and run the migrator once as a deploy step:

dotnet run --project QuantMarketEngine/src/QuantMarketEngine.Migrator

It is idempotent and safe to re-run.

Seeding creates the three plans the pricing page renders from — Observer $0, Analyst $49/mo, Desk $79/mo — with their entitlements. Change a price in SaasPlanSeeder and it reconciles on the next boot; the pricing page and the entitlement checks both read the same catalogue, so they cannot disagree.

Deploying

Four Dockerfiles are in the repository.

Dockerfile.bot

The engine. No health check.

Dockerfile.dashboard

The web app. Health check :8081/health/live

Dockerfile.api

The public API. Health check :8080/health/live

Dockerfile.migrator

Migrations and seed. No health check.

Both web hosts expose /health/live (is the process up) and /health/ready (can it reach the database) — point your orchestrator's liveness and readiness probes at those respectively.

A minimal deployment is: run the migrator once, then the dashboard, the API and the bot, all three with ConnectionStrings__Quant pointing at one shared volume.

Put TLS in front of it

Neither web host terminates TLS itself. Use a reverse proxy.

What is honestly incomplete

Nothing is missing from the billing path — but it has never met a live card

Stripe hosted Checkout, the hosted Billing Portal and a signature-verified webhook covering activation, status changes and cancellation are all implemented, behind a provider interface so another PSP can be swapped in. Card data never touches the platform.

It activates the moment Stripe__SecretKey, Stripe__WebhookSecret and the price ids are set. Until they are, the dashboard detects that, routes upgrades to manual activation, and says so to the user rather than failing at the payment step. What you are inheriting is a built integration with no transaction history, not an unbuilt one.

The plans and their enforcement are separate from that and fully exercised: an Observer account really is served the delayed report, and the institutional endpoint really does return 403 Missing entitlement: reports.institutional.

Outbound email

IEmailSender has a no-op implementation for development and an SMTP one that activates when Email:SmtpHost is set. Until you set it, password-reset emails become log lines — which means the reset flow silently does nothing.

Where to read next

COMMANDS.md

Every engine command.

configuration.md

Every configuration key the code reads.

web-publishing-setup.md

The static site publisher.

system-explained/numbers/

Why each engine constant has the value it has.