Extract option-contract details from tweets: $AAPL 350 C 11/06/2026 $1.2M 4.37avg becomes a ticker, strike, option type, expiry, premium and price.
Traders post option trades and unusual-options-flow alerts on X/Twitter in a terse, slang-heavy format that regexes and general NER models handle poorly. This project fine-tunes GLiNER2 models with LoRA adapters to pull the contract details out of that text, and publishes one ready-to-use model per base size on the Hugging Face Hub, together with the labeled dataset they were trained on.
- Models
- Dataset
- Quick start
- Installation
- Usage
- How it works
- Project structure
- Citation
- Contributing
- License
One repo per base model, so you pick a size by picking a repo. All are trained on identical data, splits and seed, and scored with exact-span match on the same held-out test set.
| Model | Base params | Overall F1 |
|---|---|---|
| options-recognizer-gliner2-large | 486M | 98.7% |
| options-recognizer-gliner2-base | 208M | 98.5% |
| options-recognizer-gliner2.5-small | 74M | 86.9% |
| options-recognizer-gliner2.5-base | 194M | 86.8% |
Best model: options-recognizer-gliner2-large has the highest F1, but options-recognizer-gliner2-base is within 0.2 points at less than half the size, so it is the sensible default unless you need every last point. The GLiNER2.5 variants score about 12 points lower on this task. Repos are overwritten when a better adapter is trained, and each version is tagged (v1, v2, ...).
Per-label F1 of the best model:
| ticker | strike | option_type | expiry | premium | price |
|---|---|---|---|---|---|
| 98.6% | 99.0% | 99.8% | 98.4% | 99.2% | 96.9% |
Each model card has the full precision/recall table, a usage snippet and examples. Without any fine-tuning the same base models reach only 25β64% F1, so the adapters are what make this work.
StephanAkkerman/options-ner holds the labeled tweets as character spans: 1,287 tweets in train and the 227-tweet held-out test split that every model was benchmarked on (2,526 test entities). Tweets were scraped, cleaned, pre-labeled with an LLM and corrected by hand in Label Studio.
from datasets import load_dataset
ds = load_dataset("StephanAkkerman/options-ner")import json
import re
from gliner2 import AutoExtractor
from huggingface_hub import snapshot_download
# AlnumBoundarySplitter: copy it from src/core/spans.py or the model card
adapter_dir = snapshot_download("StephanAkkerman/options-recognizer-gliner2-base")
cfg = json.load(open(f"{adapter_dir}/recognizer_config.json"))
model = AutoExtractor.from_pretrained(cfg["base_model"])
model.load_adapter(adapter_dir)
model.set_word_splitter(AlnumBoundarySplitter()) # required
print(model.extract_entities(
"Unusual flow: $TSLA $300 calls 01/16/2027 sweep, $2.5M premium, filled at 12.40",
cfg["entity_descriptions"],
threshold=cfg["threshold"],
))
# {'entities': {'ticker': ['$TSLA'], 'strike': ['$300'], 'option_type': ['calls'],
# 'expiry': ['01/16/2027'], 'premium': ['$2.5M'], 'price': ['12.40']}}Needs pip install "gliner2>=2" huggingface_hub. The adapter was trained with a custom word splitter, and without it accuracy drops sharply; the model cards explain why.
To train, benchmark or publish models yourself, clone the repo and install the requirements:
git clone https://github.com/StephanAkkerman/options-recognizer-model.git
cd options-recognizer-model
pip install -r requirements.txt
pip install -r requirements-train.txt # only for trainingThe data/ folder is not tracked in git; get the labeled data from the dataset.
Run everything from the repo root with python -m (needs pip install -r requirements.txt; training also needs requirements-train.txt).
| Step | Command |
|---|---|
Clean scraped tweets (data/raw/json/o.jsonl β data/cleaned/options.jsonl) |
python -m utils.labeling.clean_data |
| Publish the raw texts to the Hub | python -m utils.labeling.clean_data --push user/options-tweets |
| Pre-label with an LLM | python -m utils.synthetic.auto_label --interactive |
| Review in Label Studio | import data/preds/*.json, config in data/label_studio.xml, export to data/labeled/ |
| Check for duplicate tasks | python -m utils.labeling.check_labeled_duplicates |
| Hold out a test set | python -m src.maintenance.split_test_set |
| Train adapters (also splits, benchmarks) | python -m src.core.train --base-model gliner2-large-v1 (or all, or a comma list) |
| Benchmark (exact-span F1 + size/speed, cached) | python -m src.core.benchmark [--base-model ...] [--all] |
| Inspect errors / sweep thresholds / compare descriptions | python -m src.analysis.error_analysis, threshold_sweep, validate_descriptions (all take --base-model) |
| Publish labeled data (train + held-out test) | python -m utils.hf.push_dataset_to_hf --repo-id you/options-ner --test-folder data/test --public |
| Publish the best adapter of every base model | python -m utils.hf.push_model_to_hf --best --owner you --public |
| Publish one adapter | python -m utils.hf.push_model_to_hf models/options_adapter_<base>_vN --owner you --threshold 0.6 |
Pushing model cards runs each adapter on the example texts, which needs gliner2>=2 (pass --no-examples to skip). Run the tests with python -m pytest.
Adapters are trained for each base in src/core/models.py (gliner2.5-small-v1, gliner2.5-base-v1, gliner2-base-v1, gliner2-large-v1) on identical data, splits and seed, and each is published to its own repo, <owner>/options-recognizer-<base> (without the -v1 suffix). Pick a smaller base for speed or a larger one for accuracy; the benchmark table shows F1 next to base size and ms/doc. Each repo ships a recognizer_config.json (base model, labels, descriptions, threshold) so it loads without this codebase.
The entity labels and their GLiNER2 descriptions live in src/core/labels.py. GLiNER2 reads the descriptions at train and inference time, so editing them changes model behavior.
- Collect and clean tweets about options, then pre-label them with an LLM.
- Review the pre-labels by hand in Label Studio and export them to
data/labeled/. - Hold out about 15% as a fixed test set; training never sees it.
- Train a LoRA adapter (rank 32, early stopping) per base model, using a word splitter that breaks between letters and digits so spans such as
4.37avgare word-aligned. - Benchmark every adapter with exact-span precision/recall/F1 per label; results are cached per test set in
models/benchmark_results.json. - Publish the best adapter of each base to its own Hub repo, with a generated model card.
src/core/ labels, base-model registry, word splitter, training, benchmark
src/analysis/ error analysis, threshold sweep, description comparison
src/maintenance/ test-set split
utils/labeling/ cleaning and duplicate checks for the raw tweets
utils/synthetic/ LLM pre-labeling
utils/hf/ publishing the dataset and the model repos
tests/ pytest suite
If you use this project in your research, please cite as follows:
@misc{akkerman_options_recognizer,
author = {Stephan Akkerman},
title = {Options Recognizer},
year = {2026},
publisher = {GitHub},
journal = {GitHub repository},
howpublished = {\url{https://github.com/StephanAkkerman/options-recognizer-model}}
}Contributions are welcome! If you have a feature request, bug report, or proposal for code refactoring, please feel free to open an issue on GitHub. We appreciate your help in improving this project.
This project is licensed under the MIT License. See the LICENSE file for details.