First campaign#
This page runs a campaign from one of the repository’s configs, reads its outcome, and
judges the accepted factors with acceptance. The configs in configs/ are complete
examples: us3000_ret5.yaml opens daily price and volume, us3000_fund_ret5.yaml adds
Sharadar fundamentals, us3000_hf_ret5.yaml adds daily statistics of one-minute trade
bars, and us3000_intraday_ret5.yaml adds the one-minute trade bars themselves, which
candidates compute on (see intraday families).
1. Read the config#
A campaign is one YAML file. Its main sections:
run_dir: runs/my_campaign # everything the run writes
market: adjusted daily bars of every US common stock
segments: # only training is ever shown to the LLM
training: {start: 2000-02-02, end: 2014-12-31}
validation: {start: 2015-01-02, end: 2019-12-31}
holdout: {start: 2020-01-02, end: 2026-10-15}
purge_bars: 5
data: {prices: ..., label: ..., membership: ...} # quantlab components or store folders
scoring: {neutralize: {exposures: ..., size_column: marketcap, industry_column: industry}}
fields: [fields/sharadar_price_volume.yaml] # the input families candidates may read
operators: operators/kunquant_example.yaml # the operators they may write with
reference: [...] # the factors candidates must add to
known: [...] # other existing factors, checked for novelty
directions: [...] # seed research directions for the planner
round: {candidates: 20, groups: 4, max_rounds: 30, patience: 5}
llm:
planner: {backend: openai_compatible, model: deepseek-v4-pro, base_url: https://api.deepseek.com,
api_key_env: DEEPSEEK_API_KEY, max_tokens: 32000}
proposer: {...}
reviewer: {...}
distiller: {...}
acceptance: {model: {...}, seeds: [0, 1, 2]}
The campaign config describes every section. Copy a config,
change run_dir, and check the data paths before the first start.
2. Run the campaign#
from factor_agent.campaign import Campaign
from factor_agent.config import CampaignConfig
config = CampaignConfig.from_file("configs/us3000_ret5.yaml")
result = Campaign(config).run()
print(result.stop, len(result.accepted), "accepted of", len(result.records))
A campaign runs for hours, most of it waiting for the LLM, so start it in the background and keep the log:
nohup uv run python -c \
"from factor_agent.campaign import Campaign; from factor_agent.config import CampaignConfig; \
Campaign(CampaignConfig.from_file('configs/us3000_ret5.yaml')).run()" > campaign.log 2>&1 &
Before any LLM call, run() checks the field, operator and template files and reports
every problem at once. The run stops at round.max_rounds, round.max_trials,
round.patience rounds in a row without an acceptance, or when the provider’s balance is
exhausted. Calling run() again on the same run_dir resumes where it stopped.
3. Read the outcome#
import json
for line in open("runs/my_campaign/ledger.jsonl"):
record = json.loads(line)
if record["status"] == "accepted":
metrics = record["metrics"]
print(record["round"], record["candidate"]["description"][:80],
f"IC {metrics['ic_mean']:+.4f}, partial IC t {metrics['partial_ic_t']:+.2f}")
Every candidate is one line of ledger.jsonl: accepted, rejected (by the reviewer or by
admission) or failed (refused by the parser or the Gate, or its graph did not compile),
with the stage where it ended and the reasons. The run directory
lists the other files. To look at your runs in a browser instead, start
the web frontend: uv run factor-agent-web runs/.
4. Judge the accepted factors#
from factor_agent.acceptance import Acceptance
acceptance = Acceptance(config)
report = acceptance.validate() # baseline vs baseline + accepted, on validation
print(report.markdown())
validate() trains the acceptance model once per seed with and without the accepted
factors and compares rank IC, deciles and a plain TopK book on the validation segment.
Then, once, open_holdout(selection) makes the same comparison on the hold-out, and
export({...}) writes the chosen factors as quantlab factors and stores. See
acceptance.