Instruments, groups and sessions
The tradable catalog and its lifecycle, symbol groups as your dealing-desk economics, account tiers with margin call and stop-out levels, and trading-hours calendars.
Four tables decide what a customer can trade and what it costs them.
- Instruments — the catalog. One row per tradable symbol, with its contract specification and its lifecycle status.
- Symbol groups — your economics. Leverage, spread markup, commission, swap policy and which calendar applies. Every instrument belongs to one.
- Account groups — your risk tiers. Margin-call level, stop-out level, negative-balance protection and a leverage cap. Every account belongs to one.
- Session calendars — when a market is open. Attached to symbol groups.
Get these right and the desk mostly runs itself. Get them wrong and you either have no customers or no money.
The instrument catalog
Admin → Forex Trading → Market Data → Instruments
A symbol is written CURRENCY/PAIR: EUR/USD, XAU/USD for gold, AAPL/USD
for an Apple CFD, SPY/USD for the S&P proxy. The combination is unique.
Each row carries a metadata block holding the contract specification, which is
what turns an order size into money:
| Field | Meaning |
|---|---|
contractSize |
Units in one standard lot — 100,000 for FX, 100 for gold, 5,000 for silver, 1,000 for energy, 1 for equities |
pipSize / pointSize |
The pip and the point, in price terms. JPY pairs use 0.01 / 0.001; everything else 0.0001 / 0.00001 |
digits |
Price precision shown in the terminal |
limits.amount |
Minimum, maximum and step size in base units |
stopsLevel |
The minimum distance, in points, a stop loss, take profit or pending order may sit from the market |
delayed |
True for seeded stocks and indices — display-only unless you opt in |
Pip value and point value are deliberately never stored. They are derived at
use time from pointSize × contractSize in the quote currency and converted at
the current rate, because storing them would make every value wrong the moment
the market moved.
swapLong and swapShort sit on the instrument, in points. Negative means the
customer is charged, positive means credited.
Two display flags, isTrending and isHot, do more than decorate the markets
rail: the tick engine keeps those symbols subscribed even when nobody holds a
position in them.
The lifecycle
Status is not a boolean, because "off" while a customer holds a position is not a state you want to be able to reach.
| Status | Meaning |
|---|---|
INACTIVE |
Imported, invisible to customers, not tradable |
ACTIVE |
Fully tradable |
CLOSE_ONLY |
No new positions and no margin-increasing orders; existing positions can still be closed |
HALTED |
Temporary freeze — a corporate action, a provider outage, a news event |
DELISTED |
Terminal. Nothing reaches this except an empty CLOSE_ONLY instrument |
Allowed moves are exactly:
INACTIVE -> ACTIVE
ACTIVE -> CLOSE_ONLY | HALTED | INACTIVE
CLOSE_ONLY -> ACTIVE | HALTED | DELISTED
HALTED -> ACTIVE | CLOSE_ONLY
DELISTED -> (nothing)Two guards apply:
- Activating requires a provider symbol mapping for the currently active provider. A mapping belonging to a different, inactive vendor is refused with the list of vendors that are mapped, because such instruments activate cleanly and then quote nothing.
INACTIVEandDELISTEDare refused while open positions exist. The error names the count. Route throughCLOSE_ONLYand wait.
ACTIVE → CLOSE_ONLY → watch the position count fall to zero → DELISTED.
Customers keep the ability to exit the whole time. Trying to shortcut it just
produces a 400.
Symbol groups — your economics
Admin → Forex Trading → Market Data → Symbol Groups
Everything a fill costs a customer, and therefore everything it earns you, is here.
Three of these deserve elaboration.
Spread markup is your revenue line. With spreadMarkupPips at 1 on EUR/USD,
a customer buying pays the feed's ask plus half a pip and sells at the feed's
bid minus half a pip. On a standard lot that is roughly $10 round turn. Set it
to 0 and you are quoting raw broker prices with nothing in it for you.
Hedged margin rate decides whether hedging is free. At 0, a customer holding
one lot long and one lot short of the same symbol pays margin on the larger side
only — the classic MetaTrader hedged-margin behaviour. Raise it towards 1 and
the covered volume starts costing margin too. The formula the engine uses per
symbol is max(long, short) / leverage + hedgedMarginRate × min(long, short) / leverage.
Commission is charged in full at open. It is priced as a round turn, so
there is no second charge at close. commissionPerLot is in the account
currency, not the quote currency.
The marginCurrency column is a reserved no-op. Margin is always computed in
the instrument's quote currency and converted to the account currency at the
current rate; the column is kept for schema compatibility and is not accepted by
the admin API.
Account groups
Account groups are your risk tiers. Admin → Forex Trading → Trading →
Accounts shows the accounts; the tiers behind them are fx_account_group
rows.
Nothing creates a default tier. An account with no group falls back to a
margin-call level of 100% and a stop-out level of 50%, and no tier leverage cap
applies. That is a workable ESMA-ish default, but it is a fallback, not a
decision. Create your tiers and mark one defaultForType: LIVE and one
defaultForType: DEMO so new accounts land somewhere on purpose.
Margin level is equity / usedMargin × 100. At 100% the customer's equity
exactly covers their requirement. At the margin-call level they get an email and
an in-app notification (FxTradingMarginCall), and the platform refuses any
order that would increase margin, plus any withdrawal. At the stop-out level the
risk engine starts closing positions — largest loser first, only positions whose
session is open — and re-checks after each close, up to ten in one sweep.
Negative-balance protection runs after everything is closed and only if the
balance is still below zero. It writes an NBP_CORRECTION deal that zeroes the
account. That deficit is yours.
Session calendars
Admin → Forex Trading → Market Data → Calendars
A calendar is a timezone, a list of weekly session windows and a list of
holidays. A window is openDay + openTime → closeDay + closeTime, with
day 0 being Sunday.
Three ship with the seed catalog, all anchored to America/New_York:
| Calendar | Windows |
|---|---|
| FX 24/5 | Sunday 17:00 → Friday 17:00, one continuous window |
| US Stocks RTH | Monday–Friday 09:30 → 16:00 |
| CME Metals & Energy | Sunday–Thursday 18:00 → next day 17:00 |
Attach null for a 24/7 market — that is what the Crypto CFDs group does.
The calendar is authoritative, not advisory. When it says closed, the tick
engine stamps the symbol SESSION_CLOSED, market orders and closes are refused
with "Market is closed", and the terminal shows the next open time. Pending
orders may still be placed while closed — they park and trigger on reopen
ticks — with one exception: a DAY order needs a session close to expire
against, so it can only be placed while the session is open.
Deleting a calendar is refused while any symbol group still references it.
Nothing populates the holiday list. If you leave it empty, Christmas Day is an ordinary trading day as far as the engine is concerned — the desk will quote whatever thin, gapped prices the provider streams, and fill against them.
Overnight swaps
Financing settles at 17:00 America/New_York, and the swap cron runs hourly
rather than once a day. Every run settles the most recent elapsed 17:00 cutoff
plus any earlier ones missed during an outage, up to seven days back. Each
(position, rollover date) pair is idempotent, so running every hour is safe.
The amount is derived from the instrument's swapLong or swapShort points,
the position size and contract size, then the group's swapMarkupPercent, then
a day multiplier — 3 on the group's triple-swap day, 0 on days the group does
not charge. The result is converted into the account currency and booked as a
SWAP deal.
Converting the quote currency into the account currency routes through a USD hub
pair, and those pairs must be ACTIVE and streaming. A EUR-denominated account
holding GBP/JPY needs USD/JPY and EUR/USD listed and quoting. If they are
not, the rollover cannot be converted, is retried hourly, and then expires
uncharged after seven days. The engine pins the missing legs to the tick stream
and raises a critical swap-conversion-unavailable alert when this happens —
but only if those instruments exist in your catalog at all.
Accounts flagged swapFree skip the charge on groups where swapFreeAllowed is
true. That combination is how you offer Islamic accounts without exempting them
from crypto CFD financing.
Economic calendar and news
Two optional feeds render in the terminal.
Admin → Forex Trading → Market Data → Economic Calendar and Market News
both hold two kinds of row: PROVIDER rows written by the 15-minute sync, and
MANUAL rows you author. Manual rows are permanently exempt from the sync,
which is how a desk runs a usable calendar with no paid calendar feed at all.
Content edits to a provider-sourced row are not durable — the next sync refreshes rows whose published figures moved. To suppress a bad provider row permanently, set its status to hidden rather than editing or deleting it; the sync never writes status, but it will happily re-insert a row you deleted.
Calendar → Feed status tells you why the tables look the way they do: the
last sync's outcome per feed (OK, NOT_IMPLEMENTED, PLAN_LIMIT, ERROR),
which provider served it, and which of your configured providers could serve it
at all. Neither feed is bound to the active quote provider — a Finnhub key alone
is enough for news.
Next
- The trading terminal — how these settings surface to customers
- The trading desk — watching the exposure these settings create
- Settings reference — every key in one place