Configuration: one file for the finance blocks¶
A desk that runs schedules, pre-trade limits and a ring buffer does not want the
same numbers typed in five places. One TOML file holds the parameters, and the
blocks read it. A configuration that loads is a configuration the blocks accept:
load_config builds each block once and raises the block's own error, prefixed
with the table name.
1. Write a configuration and load it¶
The file has four tables. The values are the ones a small desk would use: 100 shares split over 4 slices, a 10-share pre-trade cap, a 3-row ring, three relative paths.
import tempfile
import textwrap
from pathlib import Path
from dense_armor.utility.finance.config import load_config
TOML = """
[schedule]
total = 100.0
n = 4
[pretrade]
max_notional = 1000000.0
max_size = 10.0
price_band = 0.01
max_orders = 5
window_s = 1.0
duplicate_window_s = 0.1
[ring]
capacity = 3
width = 2
[paths]
log = "log.jsonl"
feed = "feed.bin"
emergency_stop = "STOP"
"""
with tempfile.TemporaryDirectory() as d:
cfg = Path(d) / "config.toml"
cfg.write_text(textwrap.dedent(TOML), encoding="utf-8", newline="\n")
c = load_config(cfg)
print("schedule:", c.schedule.total, "in", c.schedule.n, "slices")
print("pretrade max_size:", c.pretrade.max_size)
print("ring:", c.ring.capacity, "rows x", c.ring.width)
print("log name:", c.paths.log.name)
print("feed name:", c.paths.feed.name)
print("stop name:", c.paths.emergency_stop.name)
schedule: 100.0 in 4 slices
pretrade max_size: 10.0
ring: 3 rows x 2
log name: log.jsonl
feed name: feed.bin
stop name: STOP
The three paths are pathlib.Path objects. A relative path is resolved against
the folder of the TOML file, so the file can be moved with its data. Each table
is a frozen dataclass: assigning to a field raises
dataclasses.FrozenInstanceError, so a loaded configuration cannot change under
the blocks.
2. Build a checker and send one order¶
FinanceConfig.make_checker() returns a new PreTradeChecker from the
pretrade table. make_ring() does the same for the ring. The two methods do
not cache: two calls return two independent objects.
import tempfile
import textwrap
from pathlib import Path
from dense_armor.utility.finance.config import load_config
TOML = """
[schedule]
total = 100.0
n = 4
[pretrade]
max_notional = 1000000.0
max_size = 10.0
price_band = 0.01
max_orders = 5
window_s = 1.0
duplicate_window_s = 0.1
[ring]
capacity = 3
width = 2
[paths]
log = "log.jsonl"
feed = "feed.bin"
emergency_stop = "STOP"
"""
with tempfile.TemporaryDirectory() as d:
cfg = Path(d) / "config.toml"
cfg.write_text(textwrap.dedent(TOML), encoding="utf-8", newline="\n")
c = load_config(cfg)
checker = c.make_checker()
dd = checker.check("AAPL", "buy", size=5.0, price=100.0, ref_price=100.0, t=0.0)
print(dd)
A 5-share buy at the reference price is inside the 10-share cap and the 1 % band, so the checker approves it.
3. A configuration the blocks refuse¶
The structure is checked first, then the ranges. A max_orders of 0 is an
integer, so the structure passes, and the PreTradeChecker refuses it. The
message names the table.
import tempfile
import textwrap
from pathlib import Path
from dense_armor.utility.finance.config import load_config
BAD = """
[schedule]
total = 100.0
n = 4
[pretrade]
max_notional = 1000000.0
max_size = 10.0
price_band = 0.01
max_orders = 0
window_s = 1.0
duplicate_window_s = 0.1
[ring]
capacity = 3
width = 2
[paths]
log = "log.jsonl"
feed = "feed.bin"
emergency_stop = "STOP"
"""
with tempfile.TemporaryDirectory() as d:
cfg = Path(d) / "bad.toml"
cfg.write_text(textwrap.dedent(BAD), encoding="utf-8", newline="\n")
load_config(cfg)
The prefix [pretrade]: is the rule of load_config: an error of a block is
raised again with the table name in front, so a caller that reads only the
first token knows which table is wrong.
Details¶
References.
- TOML v1.0.0 specification, sections
Table,
Integer,
Float,
String. The Float section says nan
and inf are valid floats; a nan limit is refused by the block, not by
the parser.
Choices stated explicitly.
- An integer is accepted where a number is expected, and stored as float.
TOML v1.0.0 has both types, and a hand-written file often writes 5
where the block wants 5.0; refusing it would be a rule of ours, not of the
language.
- A relative path is resolved against the folder of the TOML file. TOML
v1.0.0 defines no path semantics, so this is our choice, stated here and
tested in test_relative_path_resolved_against_file_folder.
- load_config builds each block once, so the ranges are validated by the
blocks of F1, F2 and F4 and not by a second copy of their rules. The tests
test_out_of_range_names_the_table and test_huge_int_in_number_gives_value_error
exercise that path.
- tomllib is imported inside load_config, so a Python 3.10 session raises
RuntimeError("tomllib needs Python 3.11 or later") only when a
configuration is read, not at import time.
Measured cost. Not measured. The page reports no timing, because no script of the repository printed a time for these functions.
Declared limits.
- No environment variables, no includes, no hot reload. The file is read once
per call; a change on disk is visible only on the next call.
- One file. A configuration that spans several files is not supported.
- The reading needs Python 3.11 or later, because tomllib is in the standard
library from 3.11. On Python 3.10 the call raises RuntimeError; the tests
that read a TOML file are skipped there with the reason written.
- The three paths are not created by load_config. A log path that does not
exist is not an error here; F6 creates the log at the first append.
These pages describe what the code does on the data shown. Not a forecast, not financial advice.