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

Configuration & Secrets Reference

Every setting binds from appsettings.json, user secrets (development) or environment variables (production, in Section__Key form). Secrets never go in appsettings.

This page lists every configuration section the code reads. If a section is not here, it is not read — and the reverse is checked: the names below are the SectionName constants declared on the options classes.

Sections
19
Hosts
Bot · API · Dashboard · Migrator
Source
docs/configuration.md
Revised
2026-08-31

Which environment you are running in

Read this before anything else on the page. Two behaviours depend on the environment and neither one announces itself.

dotnet run falls back to Production when a project has no launch profile

Each project's Properties/launchSettings.json picks the environment. With no such file, .NET chooses Production — and then:

  • User secrets load in Development only for the API and Dashboard hosts. Under Production they are simply absent: a setting you know you stored is missing, with no error. (The bot host calls AddUserSecrets explicitly, so it reads them in any environment.)
  • The Dashboard refuses to start in Production without DataProtection:KeysDirectory.

The Dashboard and API ship a launch profile setting ASPNETCORE_ENVIRONMENT=Development, so a local run behaves. On a server, set every value through environment variables and do not rely on the secret store.

Secrets

production: environment variables only

None of these has a safe default on someone else's infrastructure.

Saas__AdminApiKey Secret host API

Enables the ops surface (/api/v1/admin and the legacy api/* routes). Unset, every ops endpoint answers 503. Compared in constant time.

AlphaVantage__ApiKey Secret host all

Live market data. Unset, the engine falls back to the bundled CSV provider — which is for development only, but does mean the engine runs and can be measured with no account anywhere.

Telegram__BotToken Secret host bot

Telegram publishing and review.

ConnectionStrings__Quant host all

SQLite path. Docker default Data Source=/data/quant.db on a shared volume.

Stripe__SecretKey Secret host API + Dashboard

Unset, billing is disabled everywhere and the dashboard falls back to manual upgrades — and says so to the user rather than failing silently.

Stripe__WebhookSecret Secret host API

Signing secret for POST /api/v1/billing/webhook. The endpoint answers 503 until it is set.

Stripe__PriceIds__analyst Stripe__PriceIds__desk host API + Dashboard

Stripe price ids per plan code. Only mapped plans are purchasable.

Email__SmtpHost / SmtpPort / SmtpUser / SmtpPassword / FromAddress Secret host Dashboard (+API)

SMTP delivery, any provider. With the host unset, emails become log lines — which means the password-reset flow silently does nothing.

Billing lifecycle

Stripe-hosted throughout. Card data never touches the platform.

Checkout dashboard /actions/billing/checkout
Stripe-hosted payment page
checkout.session.completed — activates the paid plan, superseding the free-tier row (one live subscription per user)
customer.subscription.updated — mirrors status and period end, covering past_due
customer.subscription.deleted — cancels and drops the account back to the free tier

Every transition is audit-logged with actor billing.

Webhook configuration in Stripe: endpoint https://<api-host>/api/v1/billing/webhook, events: the three above.

Scaling

all hosts
Database:Provider default Sqlite

Sqlite for a single node, or Postgres via Npgsql. Postgres needs its own migration set.

Database:MigrateOnStartup default true

Set false on multi-node deployments and run the Migrator once first, so nodes don't race Migrate().

Redis:ConnectionString default unset

When set, the API quota and the login lockout use Redis — shared across nodes — instead of in-process counters. Required for horizontal scaling.

Database:Backup:Enabled default true

Scheduled verified backups in the bot host. Turn off only where an external process snapshots the volume.

Database:Backup:Directory default backups

Relative paths resolve next to the database. Set an absolute path on a separate volume in production — a same-disk copy dies with the disk.

Database:Backup:RetainCount default 7

Copies kept. Older ones are pruned after each run, ordered by the UTC timestamp in the filename.

Database:Backup:IntervalHours default 24

Gap between automatic runs. One always runs at host startup.

The Migrator is a one-shot console (Dockerfile.migrator) that applies migrations and seeds plans regardless of the flag — the pre-deploy schema step for a cluster.

Backups are SQLite-specific (VACUUM INTO) and are not registered when the provider is Postgres; that deployment backs up with the server's own tooling.

Dashboard host

QuantMarketEngine.Dashboard
Admin:Emails Set this first default [] empty

The only way anyone becomes an operator. A string array of email addresses; a signed-in user whose address is on it reaches the admin pages, and nobody else does.

Empty means nobody. AdminAccess fails closed, and the admin page says it is unconfigured rather than pretending the account lacks permission.

It is configuration and not a database row on purpose: an attacker who reaches the database through the application still cannot grant themselves the operator pages. It ships empty, and the buyer sets their own address.

ConnectionStrings__Quant default content-root quant.db

Must point at the same database as the API and the bot — /data/quant.db under Docker. All three hosts ship pointing at the bot's file: ../QuantMarketEngine.TelegramBot/quant.db from the API and the Dashboard, quant.db from the bot itself.

Getting this wrong does not error. The host attaches to a different file, and the interface looks empty while working perfectly.

DataProtection:KeysDirectory Required in production default unset

The host refuses to start without it outside Development. It persists cookie-encryption keys across restarts — /data/dp-keys under Docker.

Unset, ASP.NET Core generates a key ring per process, so every restart signs out every subscriber with nothing in the logs to say why.

Support:Email default placeholder

Shown on the Billing and Support pages. Set the real address before launch.

PdfExport:BrowserPath default auto-detect

Full path to Chrome, Edge or Chromium. Leave empty to search the usual install locations.

PdfExport:Enabled default true

false switches PDF export off even where a browser is installed.

PdfExport:NoSandbox Off by default default false

Starts the browser with its sandbox disabled. Needed where a container runs as root without the kernel namespaces Chrome requires — there it will not start at all.

Off by default because the flag makes the feature work everywhere and weakens it everywhere, and that is the host's call rather than ours. The log names this setting when a failed print looks like a sandbox refusal.

PdfExport:TimeoutSeconds default 30

Ceiling on one print before the browser is killed. A real print takes about a second.

PDF report export. The Reports page offers each archived session as a styled PDF — tables plus the probability-cone chart — printed by an installed Chromium-family browser in headless mode. Nothing installs that browser for you: on a host without one, the PDF link hides itself, the endpoint answers 503, and the Arabic, English and JSON exports are unaffected. The Docker images do not include a browser, so PDF export is off in a default container until one is installed.

The PDF is English only. Arabic in a PDF needs bidirectional layout, and the rasteriser this project uses reverses digits inside a mixed Arabic/Latin run — it printed 1.16805 as 50861.1. The Arabic report is still delivered as text.

Auth model. Email and password (UserCredential, PBKDF2) exchanged for a 7-day sliding HttpOnly session cookie. Self-serve registration provisions the free observer plan. A per-IP failed-login damper allows 8 failures in 10 minutes. API keys remain the machine credential; the dashboard mints and revokes them, and the raw key is rendered exactly once from the POST response.

Analysis layers

all hosts

Four layers of the pipeline are configurable, and two of them ship switched off. That is a decision rather than an omission, and each says why. CrossAsset and EconomicIndicators appear in no appsettings.json at all — they run on the code defaults below, which is worth knowing before you go looking for a section that is not there.

CrossAsset

On by default

What each analysed symbol is correlated against — the layer behind the correlation bars in the report.

CrossAsset:Enabled · default true. false skips the layer entirely.

CrossAsset:Map · default [], meaning the built-in EUR/USD institutional set: DXY, gold, the US 10-year yield. Entries are { BaseSymbol, Symbol, Label }.

An instrument the data provider cannot serve is skipped at run time rather than failing the report, so listing an index or a yield here is safe even on a forex-only free tier.

EconomicIndicators

On by default

The US macro backdrop: inflation, policy rate, labour.

Enabled true · Timeframe MONTHLY · FreshnessDays 45 · AttemptIntervalDays 1 · Indicators [] for the built-in dollar-macro set.

AttemptIntervalDays is the setting that governs cost, not FreshnessDays. US macro publishes on a one-to-two-month lag, so the newest print available is routinely older than any sane freshness gate and the data can never make itself look current. Before this existed, four indicators were re-fetched on every single run and returned the same figures each time.

Cost: at most one provider call per indicator per day — a few calls a month.

EconomicCalendar

Off in code · on as shipped

A hand-maintained list of upcoming high-impact releases, used to warn that a reading sits inside a blackout window. The code default is off because it is a list somebody has to keep writing: it has no feed behind it, and a stale calendar is worse than none because it warns about the wrong days.

The bot's appsettings.json turns it on and carries 14 central-bank and US releases through 17 December 2026. When that list runs out the layer goes quiet rather than wrong — but it is a maintenance task with a date on it, and the buyer should be told which date.

BlackoutWindowHours 24 · MinimumImpact High · Events entries are { Name, ScheduledAt (UTC), Impact, Currencies[] }, where empty currencies means global.

FundamentalAnalysis

Off, and fenced even when on

Enabled false · DataFilePath empty. Point the path at a hand-filled JSON file of releases, rates and stances to feed the layer with test data.

Turning it on does not put hand-entered figures in front of a client. The provider reports IsLiveSource=false and the output mapper drops the values. The layer exists to be developed against, and the fence is in code so that a future contributor cannot enable it into production by flipping a flag.

Bot host

QuantMarketEngine.TelegramBot
Telegram:BotToken default empty

From @BotFather. Empty and the bot stays idle — no receiver, no scheduler. The rest of the host still runs.

Telegram:ReviewerChatId default 0

Private chat id of the human reviewer. This is the approval gate's inbox.

Telegram:PublicChannelId default empty

Channel id or @username where approved reports are published.

Telegram:ActiveSymbols default []

Symbols the scheduled worker analyses each cycle. Intentionally empty in code: the .NET binder appends to a non-empty default array, so a default of ["EURUSD"] plus the same value in appsettings binds to ["EURUSD","EURUSD"] and generates every report twice. The value comes from configuration only.

Telegram:Timeframe default 4H

Timeframe the scheduled worker requests.

Telegram:ScheduleHours default 4

How often the scheduled worker runs.

ReviewerChatId and PublicChannelId currently sit in the bot's appsettings.json for convenience. They are deploy-specific: move them to environment variables anywhere shared.

Background workers

bot host

Both loops have safe defaults and run without any configuration.

EventTrigger:Enabled true in code · false as shipped

The event-driven watcher. It runs more often than the scheduled cycle but only queues a report when it detects a structural change or a regime flip.

The bot's appsettings.json switches it off. This is the "event-driven trigger ships disabled" line in the sale material — and it is a configuration value, not missing code.

EventTrigger:IntervalMinutes default 60

Minutes between detection passes. Clamped to a minimum of 1.

EventTrigger:StartupDelaySeconds default 45

Lets startup migrations and the data load settle first.

OutcomeEvaluation:Enabled On default true

The self-scoring loop — the background VaR backtest that produces the calibration record. Turning it off stops the product's central claim from being earned.

OutcomeEvaluation:IntervalMinutes default 60

Clamped to a minimum of 1.

OutcomeEvaluation:StartupDelaySeconds default 30

Lets startup migrations and the data load settle first.

OutcomeEvaluation:BatchSize default 200

Maximum analyses scored per pass.

Static site

Renders approved reports to flat HTML for GitHub, Cloudflare or Netlify Pages.

WebPublisher:OutputDirectory default empty

Empty disables web publishing. Set it and approved reports are written there.

WebPublisher:SiteTitle default Quant Market Monitor

Used on the Arabic pages, where it carries the Arabic tagline.

WebPublisher:SiteTitleEnglish default Metric Risk — Market Watch

Kept separately rather than derived from the above: printing the Arabic tagline on an English page was the last thing left untranslated when the site went bilingual.

The template is educational content only — it carries the neutral disclaimer and never a subscription or price call-to-action.

API host

QuantMarketEngine.API
Security:RequestsPerMinutePerIp default 120

Fixed-window per-IP perimeter limit; 429 beyond it. Health endpoints are exempt.

Security:AllowedOrigins default []

CORS origins. Empty means no CORS at all.

AnalysisCache:TtlSeconds default 300

TTL of the in-process analysis cache, per symbol and timeframe. 0 disables it.

Serilog:* default console, Information

Standard Serilog configuration section.

Engine:SupportedSymbols default EURUSD / 4H

The symbol catalogue. Shared by every host, so a symbol added here is a symbol the whole platform knows about.

Security model

Three tiers, checked at the endpoint rather than assumed from the route.

Public

/api/v1/plans · /api/symbols · /health/live · /health/ready · /openapi/v1.json

Subscriber

X-Api-Key scheme — /api/v1/intelligence/* and /api/v1/alerts (quota-metered), /api/v1/account (quota-free)

Operator

X-Admin-Key scheme — /api/v1/admin/* and all legacy routes: api/analysis, api/history, api/reports/*, api/review, api/calibration. These are internal ops tools; the customer surface is /api/v1 only, and the legacy routes are not part of the public contract.

Every privileged action writes an AuditLogEntries row. Every quota-bearing subscriber call writes or increments an ApiUsageRecords row — the source of truth for both billing and the dashboard.

Observability and operations

GET /health/live

Process liveness, with no dependencies.

GET /health/ready

Includes a database round-trip.

CorrelationId

Logs are structured via Serilog and every request carries one, honouring an inbound X-Correlation-Id.

Migrations apply automatically at API startup, on a single-instance assumption — revisit before scaling out.

SQLite backups snapshot the mounted /data volume, or use VACUUM INTO, on a schedule. A managed PostgreSQL migration is the path beyond one node.

CI builds, tests, and fails on any known-vulnerable NuGet package.