optionda 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. optionda-0.1.0/LICENSE +21 -0
  2. optionda-0.1.0/PKG-INFO +156 -0
  3. optionda-0.1.0/README.md +128 -0
  4. optionda-0.1.0/pyproject.toml +46 -0
  5. optionda-0.1.0/setup.cfg +4 -0
  6. optionda-0.1.0/src/optionda/__init__.py +3 -0
  7. optionda-0.1.0/src/optionda/add_resolve.py +111 -0
  8. optionda-0.1.0/src/optionda/batch.py +237 -0
  9. optionda-0.1.0/src/optionda/cli.py +945 -0
  10. optionda-0.1.0/src/optionda/config.py +61 -0
  11. optionda-0.1.0/src/optionda/credentials.py +78 -0
  12. optionda-0.1.0/src/optionda/display/__init__.py +3 -0
  13. optionda-0.1.0/src/optionda/display/table.py +459 -0
  14. optionda-0.1.0/src/optionda/engine.py +252 -0
  15. optionda-0.1.0/src/optionda/journal.py +235 -0
  16. optionda-0.1.0/src/optionda/market/__init__.py +3 -0
  17. optionda-0.1.0/src/optionda/market/alpaca.py +351 -0
  18. optionda-0.1.0/src/optionda/market/iv_mid.py +67 -0
  19. optionda-0.1.0/src/optionda/market/router.py +127 -0
  20. optionda-0.1.0/src/optionda/market/yahoo.py +234 -0
  21. optionda-0.1.0/src/optionda/models.py +110 -0
  22. optionda-0.1.0/src/optionda/occ.py +221 -0
  23. optionda-0.1.0/src/optionda/paths.py +15 -0
  24. optionda-0.1.0/src/optionda/pricing/__init__.py +3 -0
  25. optionda-0.1.0/src/optionda/pricing/bs.py +159 -0
  26. optionda-0.1.0/src/optionda/pricing/surface.py +307 -0
  27. optionda-0.1.0/src/optionda/shellenv.py +126 -0
  28. optionda-0.1.0/src/optionda/store.py +248 -0
  29. optionda-0.1.0/src/optionda.egg-info/PKG-INFO +156 -0
  30. optionda-0.1.0/src/optionda.egg-info/SOURCES.txt +45 -0
  31. optionda-0.1.0/src/optionda.egg-info/dependency_links.txt +1 -0
  32. optionda-0.1.0/src/optionda.egg-info/entry_points.txt +2 -0
  33. optionda-0.1.0/src/optionda.egg-info/requires.txt +8 -0
  34. optionda-0.1.0/src/optionda.egg-info/top_level.txt +1 -0
  35. optionda-0.1.0/tests/test_add_resolve.py +56 -0
  36. optionda-0.1.0/tests/test_alpaca_iv.py +195 -0
  37. optionda-0.1.0/tests/test_batch_parse.py +90 -0
  38. optionda-0.1.0/tests/test_bs.py +51 -0
  39. optionda-0.1.0/tests/test_cli_smoke.py +192 -0
  40. optionda-0.1.0/tests/test_credentials_and_poll.py +24 -0
  41. optionda-0.1.0/tests/test_flash.py +49 -0
  42. optionda-0.1.0/tests/test_init_rc.py +21 -0
  43. optionda-0.1.0/tests/test_journal.py +85 -0
  44. optionda-0.1.0/tests/test_occ.py +31 -0
  45. optionda-0.1.0/tests/test_shellenv.py +33 -0
  46. optionda-0.1.0/tests/test_store.py +122 -0
  47. optionda-0.1.0/tests/test_surface.py +412 -0
optionda-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yuanben
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,156 @@
1
+ Metadata-Version: 2.4
2
+ Name: optionda
3
+ Version: 0.1.0
4
+ Summary: Terminal options desk: freeze IV, reprice on live spot (Yahoo or Alpaca).
5
+ Author: Yuanben
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/ybenzou/optionda
8
+ Keywords: options,black-scholes,cli,alpaca,yfinance
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Financial and Insurance Industry
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Office/Business :: Financial :: Investment
17
+ Requires-Python: >=3.11
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: httpx>=0.27
21
+ Requires-Dist: pydantic>=2.7
22
+ Requires-Dist: rich>=13.7
23
+ Requires-Dist: typer>=0.12
24
+ Requires-Dist: yfinance>=0.2.40
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=8.0; extra == "dev"
27
+ Dynamic: license-file
28
+
29
+ # optionda
30
+
31
+ Terminal options desk: manage small multi-account option books, freeze IV, and reprice on live underlying prints.
32
+
33
+ ```bash
34
+ pip install optionda
35
+ # or from this repo:
36
+ pip install -e ./optionda
37
+ ```
38
+
39
+ **MODEL marks only** — delayed/indicative data, not executable quotes.
40
+
41
+ ## Quick start (no API key)
42
+
43
+ Requires Python 3.11+.
44
+
45
+ ```bash
46
+ py -3.11 -m venv .venv
47
+ source .venv/Scripts/activate
48
+ pip install -e .
49
+
50
+ # one-time (like conda init)
51
+ optionda init
52
+ eval "$(optionda shellenv)" # or open a new shell — default prompt: [optionda] (cyan)
53
+
54
+ optionda create demo
55
+ optionda activate demo # prompt → [demo]
56
+ optionda deactivate # prompt → [optionda]
57
+ optionda activate hedge # prompt → [hedge]
58
+
59
+ optionda add AAPL270115C00200000 --qty 2 --entry 5.20
60
+
61
+ # per-line qty + cost (semicolon batch):
62
+ optionda add "INTC 261016 140 C x10 @ 3.482; SKHY 261016 200 C x1 @ 9.5"
63
+
64
+ # easiest batch: bare add → paste lines → blank line to finish
65
+ optionda add
66
+ # INTC 261016 140 C x10 @ 3.482
67
+ # TSLA 261218 500 C x2 @ 5.75
68
+ # <empty line>
69
+
70
+ optionda export
71
+ optionda run
72
+ ```
73
+
74
+ `pip install` cannot safely edit your shell config (unlike the Conda installer). Run **`optionda init` once**, then **`optionda activate <name>`** each session (like `conda activate`). Undo init with `optionda init --reverse`.
75
+
76
+ Without `activate`, `export` / `run` / `add` / `delete` cannot read or change any account book — only the session-active account is visible.
77
+
78
+ **Cost is required on every `add`**: use `@ 5.20` on the line or `--entry 5.20`. Re-adding the same OCC+side merges qty and sets cost to the quantity-weighted average `(q1·c1 + q2·c2) / (q1+q2)`.
79
+
80
+ `add` **without** `--iv` pulls IV from Alpaca (if key configured) or Yahoo. Use `--iv` only as fallback.
81
+
82
+ In the table, **`Model$`** is the Black–Scholes theoretical premium (per share), **`Cost`** is your avg entry, and **`uPnL$`** compares them. Not a live option bid/ask.
83
+
84
+ UI uses **Rich** (`Panel`, `Rule`, `Table`, `Live` spinner). No `tqdm` / `popen` required for the desk view.
85
+
86
+ ## Optional Alpaca key (15s refresh)
87
+
88
+ ```bash
89
+ optionda key alpaca <KEY_ID> <SECRET> # verifies against Alpaca before saving
90
+ optionda key status # re-checks live credentials
91
+ optionda run # refresh every 15s
92
+ optionda key clear alpaca
93
+ ```
94
+
95
+ `key alpaca` probes `data.alpaca.markets` (SPY latest trade). Invalid keys are **not** saved.
96
+
97
+ Credentials live in `~/.optionda/credentials.toml` (mode `0600` when the OS allows). Override the data root with `OPTIONDA_HOME`.
98
+
99
+ Per-account tracking files (under the optionda data library, **not** your shell cwd):
100
+
101
+ Two separate write paths:
102
+
103
+ | Path | Role | Write mode |
104
+ |------|------|------------|
105
+ | `~/.optionda/books/<account>.txt` | Current book only (human snapshot) | **Overwrite** on add/delete/refresh |
106
+ | `~/.optionda/logs/<account>.jsonl` | Full event stream for charts / history | **Append only** |
107
+ | `~/.optionda/surfaces/<underlying>.json` | Last valid Alpaca IV smile | **Overwrite** only on successful `refresh-iv` |
108
+
109
+ JSONL `event` types: `add`, `merge`, `delete`, `refresh_iv`, `export`, `run`.
110
+ `refresh_iv` records calibrated surface metadata. `export`/`run` rows include `valuation_mode`, `surface_iv`, and `surface_as_of`.
111
+
112
+ ```bash
113
+ optionda add … # rewrite book + append add/merge event
114
+ optionda delete … # rewrite book + append delete event
115
+ optionda refresh-iv # calibrate Alpaca vendor-IV smiles while quotes are fresh
116
+ optionda export # print surface/frozen Model$ + append export mark
117
+ optionda run # each tick appends a run mark
118
+ ```
119
+
120
+ **Spot (24/5):** Alpaca stock spots query `overnight` → `boats` → `delayed_sip` → `iex` and keep the **newest** trade/quote. Basic plans usually get `overnight` (≈Futu night session); `boats` needs a higher data tier.
121
+
122
+ ### Local overnight IV surface
123
+
124
+ Run `optionda refresh-iv` **during the US option session**, when Alpaca chain quotes are current. It collects valid Alpaca vendor-IV nodes, filters zero/wide/stale quotes, and saves a per-underlying expiry smile.
125
+
126
+ At night, `run` / `export` update the 24/5 stock Spot, use the saved expiry smile with **sticky delta** interpolation, and then reprice with Black–Scholes. The `IVsrc` column is `surface` when this succeeds and `frozen` when optionda falls back to the stored position IV. Surfaces older than 36 hours are not used.
127
+
128
+ This is a local, auditable model—not a copy of Futu's proprietary IV surface. Alpaca's free `indicative` chain is still the calibration input; OPRA improves the input only when the user has a subscription. optionda deliberately does **not** infer a new IV from frozen overnight option quotes.
129
+
130
+ ```toml
131
+ # ~/.optionda/config.toml
132
+ alpaca_options_feed = "auto" # try opra, then indicative
133
+ ```
134
+
135
+ For a paid match to exchange IV, subscribe to Alpaca OPRA.
136
+
137
+ ## Commands
138
+
139
+ | Command | Purpose |
140
+ |---------|---------|
141
+ | `optionda create <name>` | Create account |
142
+ | `optionda list` | List accounts (`*` = session-active) |
143
+ | `optionda book` | Show current positions (no fetch / no log write) |
144
+ | `optionda activate <name>` | Session-activate (prompt → cyan `[name]`) |
145
+ | `optionda deactivate` | Back to cyan `[optionda]` |
146
+ | `optionda init` | Persist hook in shell rc (like `conda init`) |
147
+ | `optionda add …` | Add with required cost; same OCC+side merges qty + avg cost |
148
+ | `optionda delete <id\|OCC>` | Remove position |
149
+ | `optionda refresh-iv` | Calibrate local Alpaca IV smiles and refresh fallback IVs |
150
+ | `optionda run` | Live table until Ctrl+C |
151
+ | `optionda export` | One-shot snapshot |
152
+ | `optionda key …` | Configure Alpaca credentials |
153
+
154
+ ## Repository
155
+
156
+ Standalone project: [github.com/ybenzou/optionda](https://github.com/ybenzou/optionda). Not part of the Next.js frontend app.
@@ -0,0 +1,128 @@
1
+ # optionda
2
+
3
+ Terminal options desk: manage small multi-account option books, freeze IV, and reprice on live underlying prints.
4
+
5
+ ```bash
6
+ pip install optionda
7
+ # or from this repo:
8
+ pip install -e ./optionda
9
+ ```
10
+
11
+ **MODEL marks only** — delayed/indicative data, not executable quotes.
12
+
13
+ ## Quick start (no API key)
14
+
15
+ Requires Python 3.11+.
16
+
17
+ ```bash
18
+ py -3.11 -m venv .venv
19
+ source .venv/Scripts/activate
20
+ pip install -e .
21
+
22
+ # one-time (like conda init)
23
+ optionda init
24
+ eval "$(optionda shellenv)" # or open a new shell — default prompt: [optionda] (cyan)
25
+
26
+ optionda create demo
27
+ optionda activate demo # prompt → [demo]
28
+ optionda deactivate # prompt → [optionda]
29
+ optionda activate hedge # prompt → [hedge]
30
+
31
+ optionda add AAPL270115C00200000 --qty 2 --entry 5.20
32
+
33
+ # per-line qty + cost (semicolon batch):
34
+ optionda add "INTC 261016 140 C x10 @ 3.482; SKHY 261016 200 C x1 @ 9.5"
35
+
36
+ # easiest batch: bare add → paste lines → blank line to finish
37
+ optionda add
38
+ # INTC 261016 140 C x10 @ 3.482
39
+ # TSLA 261218 500 C x2 @ 5.75
40
+ # <empty line>
41
+
42
+ optionda export
43
+ optionda run
44
+ ```
45
+
46
+ `pip install` cannot safely edit your shell config (unlike the Conda installer). Run **`optionda init` once**, then **`optionda activate <name>`** each session (like `conda activate`). Undo init with `optionda init --reverse`.
47
+
48
+ Without `activate`, `export` / `run` / `add` / `delete` cannot read or change any account book — only the session-active account is visible.
49
+
50
+ **Cost is required on every `add`**: use `@ 5.20` on the line or `--entry 5.20`. Re-adding the same OCC+side merges qty and sets cost to the quantity-weighted average `(q1·c1 + q2·c2) / (q1+q2)`.
51
+
52
+ `add` **without** `--iv` pulls IV from Alpaca (if key configured) or Yahoo. Use `--iv` only as fallback.
53
+
54
+ In the table, **`Model$`** is the Black–Scholes theoretical premium (per share), **`Cost`** is your avg entry, and **`uPnL$`** compares them. Not a live option bid/ask.
55
+
56
+ UI uses **Rich** (`Panel`, `Rule`, `Table`, `Live` spinner). No `tqdm` / `popen` required for the desk view.
57
+
58
+ ## Optional Alpaca key (15s refresh)
59
+
60
+ ```bash
61
+ optionda key alpaca <KEY_ID> <SECRET> # verifies against Alpaca before saving
62
+ optionda key status # re-checks live credentials
63
+ optionda run # refresh every 15s
64
+ optionda key clear alpaca
65
+ ```
66
+
67
+ `key alpaca` probes `data.alpaca.markets` (SPY latest trade). Invalid keys are **not** saved.
68
+
69
+ Credentials live in `~/.optionda/credentials.toml` (mode `0600` when the OS allows). Override the data root with `OPTIONDA_HOME`.
70
+
71
+ Per-account tracking files (under the optionda data library, **not** your shell cwd):
72
+
73
+ Two separate write paths:
74
+
75
+ | Path | Role | Write mode |
76
+ |------|------|------------|
77
+ | `~/.optionda/books/<account>.txt` | Current book only (human snapshot) | **Overwrite** on add/delete/refresh |
78
+ | `~/.optionda/logs/<account>.jsonl` | Full event stream for charts / history | **Append only** |
79
+ | `~/.optionda/surfaces/<underlying>.json` | Last valid Alpaca IV smile | **Overwrite** only on successful `refresh-iv` |
80
+
81
+ JSONL `event` types: `add`, `merge`, `delete`, `refresh_iv`, `export`, `run`.
82
+ `refresh_iv` records calibrated surface metadata. `export`/`run` rows include `valuation_mode`, `surface_iv`, and `surface_as_of`.
83
+
84
+ ```bash
85
+ optionda add … # rewrite book + append add/merge event
86
+ optionda delete … # rewrite book + append delete event
87
+ optionda refresh-iv # calibrate Alpaca vendor-IV smiles while quotes are fresh
88
+ optionda export # print surface/frozen Model$ + append export mark
89
+ optionda run # each tick appends a run mark
90
+ ```
91
+
92
+ **Spot (24/5):** Alpaca stock spots query `overnight` → `boats` → `delayed_sip` → `iex` and keep the **newest** trade/quote. Basic plans usually get `overnight` (≈Futu night session); `boats` needs a higher data tier.
93
+
94
+ ### Local overnight IV surface
95
+
96
+ Run `optionda refresh-iv` **during the US option session**, when Alpaca chain quotes are current. It collects valid Alpaca vendor-IV nodes, filters zero/wide/stale quotes, and saves a per-underlying expiry smile.
97
+
98
+ At night, `run` / `export` update the 24/5 stock Spot, use the saved expiry smile with **sticky delta** interpolation, and then reprice with Black–Scholes. The `IVsrc` column is `surface` when this succeeds and `frozen` when optionda falls back to the stored position IV. Surfaces older than 36 hours are not used.
99
+
100
+ This is a local, auditable model—not a copy of Futu's proprietary IV surface. Alpaca's free `indicative` chain is still the calibration input; OPRA improves the input only when the user has a subscription. optionda deliberately does **not** infer a new IV from frozen overnight option quotes.
101
+
102
+ ```toml
103
+ # ~/.optionda/config.toml
104
+ alpaca_options_feed = "auto" # try opra, then indicative
105
+ ```
106
+
107
+ For a paid match to exchange IV, subscribe to Alpaca OPRA.
108
+
109
+ ## Commands
110
+
111
+ | Command | Purpose |
112
+ |---------|---------|
113
+ | `optionda create <name>` | Create account |
114
+ | `optionda list` | List accounts (`*` = session-active) |
115
+ | `optionda book` | Show current positions (no fetch / no log write) |
116
+ | `optionda activate <name>` | Session-activate (prompt → cyan `[name]`) |
117
+ | `optionda deactivate` | Back to cyan `[optionda]` |
118
+ | `optionda init` | Persist hook in shell rc (like `conda init`) |
119
+ | `optionda add …` | Add with required cost; same OCC+side merges qty + avg cost |
120
+ | `optionda delete <id\|OCC>` | Remove position |
121
+ | `optionda refresh-iv` | Calibrate local Alpaca IV smiles and refresh fallback IVs |
122
+ | `optionda run` | Live table until Ctrl+C |
123
+ | `optionda export` | One-shot snapshot |
124
+ | `optionda key …` | Configure Alpaca credentials |
125
+
126
+ ## Repository
127
+
128
+ Standalone project: [github.com/ybenzou/optionda](https://github.com/ybenzou/optionda). Not part of the Next.js frontend app.
@@ -0,0 +1,46 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "optionda"
7
+ version = "0.1.0"
8
+ description = "Terminal options desk: freeze IV, reprice on live spot (Yahoo or Alpaca)."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.11"
12
+ authors = [{ name = "Yuanben" }]
13
+ keywords = ["options", "black-scholes", "cli", "alpaca", "yfinance"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Environment :: Console",
17
+ "Intended Audience :: Financial and Insurance Industry",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Topic :: Office/Business :: Financial :: Investment",
23
+ ]
24
+ dependencies = [
25
+ "httpx>=0.27",
26
+ "pydantic>=2.7",
27
+ "rich>=13.7",
28
+ "typer>=0.12",
29
+ "yfinance>=0.2.40",
30
+ ]
31
+
32
+ [project.optional-dependencies]
33
+ dev = ["pytest>=8.0"]
34
+
35
+ [project.scripts]
36
+ optionda = "optionda.cli:main"
37
+
38
+ [project.urls]
39
+ Homepage = "https://github.com/ybenzou/optionda"
40
+
41
+ [tool.setuptools.packages.find]
42
+ where = ["src"]
43
+
44
+ [tool.pytest.ini_options]
45
+ testpaths = ["tests"]
46
+ pythonpath = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """optionda — terminal options desk (MODEL marks, frozen IV)."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,111 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from optionda.batch import read_batch_lines
6
+ from optionda.occ import OccError, parse_leg_line
7
+
8
+
9
+ def split_semi_separated(text: str) -> list[str]:
10
+ """Split 'A; B; C' into position lines (keeps spaces inside each part)."""
11
+ parts = [p.strip() for p in text.replace("\n", ";").split(";")]
12
+ return [p for p in parts if p and not p.startswith("#")]
13
+
14
+
15
+ def read_interactive_lines(
16
+ *,
17
+ prompt_print=print,
18
+ line_input=input,
19
+ ) -> list[str]:
20
+ """Read pasted lines until a blank line or EOF (Ctrl+Z Enter on Windows)."""
21
+ prompt_print("Paste positions (one per line): ROOT YYMMDD STRIKE C|P xQTY @ cost")
22
+ prompt_print("Finish with an empty line, or Ctrl+Z then Enter (Windows).")
23
+ prompt_print("")
24
+ lines: list[str] = []
25
+ while True:
26
+ try:
27
+ line = line_input()
28
+ except EOFError:
29
+ break
30
+ if not line.strip():
31
+ break
32
+ if line.strip().startswith("#"):
33
+ continue
34
+ lines.append(line.strip())
35
+ return lines
36
+
37
+
38
+ def _validated_line(token: str) -> str:
39
+ """Validate parseability but keep the original text (preserves @ cost)."""
40
+ parse_leg_line(token)
41
+ return token.strip()
42
+
43
+
44
+ def resolve_add_lines(items: list[str]) -> list[str]:
45
+ """Normalize CLI tokens into one or more position lines.
46
+
47
+ Supports:
48
+ - single/multiple OCC symbols (with optional @ cost)
49
+ - one human line split across argv: INTC 261016 140 C @ 5.20
50
+ - semicolon-separated in one argv
51
+ - file path or '-' (stdin) for multi-line batch
52
+
53
+ Original line text is preserved so trailing '@ cost' survives.
54
+ """
55
+ if not items:
56
+ raise ValueError("no positions provided")
57
+
58
+ if len(items) == 1:
59
+ token = items[0]
60
+ if token == "-" or Path(token).is_file():
61
+ lines = read_batch_lines(token)
62
+ if not lines:
63
+ raise ValueError("no positions to add (empty input)")
64
+ return lines
65
+ if ";" in token:
66
+ parts = split_semi_separated(token)
67
+ if not parts:
68
+ raise ValueError("no positions to add")
69
+ return parts
70
+ try:
71
+ return [_validated_line(token)]
72
+ except OccError as exc:
73
+ raise ValueError(str(exc)) from exc
74
+
75
+ # Multiple argv tokens: either many OCCs, or one spaced human line
76
+ joined = " ".join(items)
77
+ if ";" in joined:
78
+ parts = split_semi_separated(joined)
79
+ if parts:
80
+ return parts
81
+
82
+ try:
83
+ return [_validated_line(joined)]
84
+ except OccError:
85
+ pass
86
+
87
+ lines: list[str] = []
88
+ for token in items:
89
+ try:
90
+ lines.append(_validated_line(token))
91
+ except OccError as exc:
92
+ raise ValueError(
93
+ f"could not parse {token!r} (also not a single human line: {joined!r})"
94
+ ) from exc
95
+ return lines
96
+
97
+
98
+ def looks_like_field_add(
99
+ items: list[str],
100
+ underlying: str | None,
101
+ expiry: str | None,
102
+ strike: float | None,
103
+ option_type: str | None,
104
+ ) -> bool:
105
+ return (
106
+ not items
107
+ and underlying is not None
108
+ and expiry is not None
109
+ and strike is not None
110
+ and option_type is not None
111
+ )