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.
- optionda-0.1.0/LICENSE +21 -0
- optionda-0.1.0/PKG-INFO +156 -0
- optionda-0.1.0/README.md +128 -0
- optionda-0.1.0/pyproject.toml +46 -0
- optionda-0.1.0/setup.cfg +4 -0
- optionda-0.1.0/src/optionda/__init__.py +3 -0
- optionda-0.1.0/src/optionda/add_resolve.py +111 -0
- optionda-0.1.0/src/optionda/batch.py +237 -0
- optionda-0.1.0/src/optionda/cli.py +945 -0
- optionda-0.1.0/src/optionda/config.py +61 -0
- optionda-0.1.0/src/optionda/credentials.py +78 -0
- optionda-0.1.0/src/optionda/display/__init__.py +3 -0
- optionda-0.1.0/src/optionda/display/table.py +459 -0
- optionda-0.1.0/src/optionda/engine.py +252 -0
- optionda-0.1.0/src/optionda/journal.py +235 -0
- optionda-0.1.0/src/optionda/market/__init__.py +3 -0
- optionda-0.1.0/src/optionda/market/alpaca.py +351 -0
- optionda-0.1.0/src/optionda/market/iv_mid.py +67 -0
- optionda-0.1.0/src/optionda/market/router.py +127 -0
- optionda-0.1.0/src/optionda/market/yahoo.py +234 -0
- optionda-0.1.0/src/optionda/models.py +110 -0
- optionda-0.1.0/src/optionda/occ.py +221 -0
- optionda-0.1.0/src/optionda/paths.py +15 -0
- optionda-0.1.0/src/optionda/pricing/__init__.py +3 -0
- optionda-0.1.0/src/optionda/pricing/bs.py +159 -0
- optionda-0.1.0/src/optionda/pricing/surface.py +307 -0
- optionda-0.1.0/src/optionda/shellenv.py +126 -0
- optionda-0.1.0/src/optionda/store.py +248 -0
- optionda-0.1.0/src/optionda.egg-info/PKG-INFO +156 -0
- optionda-0.1.0/src/optionda.egg-info/SOURCES.txt +45 -0
- optionda-0.1.0/src/optionda.egg-info/dependency_links.txt +1 -0
- optionda-0.1.0/src/optionda.egg-info/entry_points.txt +2 -0
- optionda-0.1.0/src/optionda.egg-info/requires.txt +8 -0
- optionda-0.1.0/src/optionda.egg-info/top_level.txt +1 -0
- optionda-0.1.0/tests/test_add_resolve.py +56 -0
- optionda-0.1.0/tests/test_alpaca_iv.py +195 -0
- optionda-0.1.0/tests/test_batch_parse.py +90 -0
- optionda-0.1.0/tests/test_bs.py +51 -0
- optionda-0.1.0/tests/test_cli_smoke.py +192 -0
- optionda-0.1.0/tests/test_credentials_and_poll.py +24 -0
- optionda-0.1.0/tests/test_flash.py +49 -0
- optionda-0.1.0/tests/test_init_rc.py +21 -0
- optionda-0.1.0/tests/test_journal.py +85 -0
- optionda-0.1.0/tests/test_occ.py +31 -0
- optionda-0.1.0/tests/test_shellenv.py +33 -0
- optionda-0.1.0/tests/test_store.py +122 -0
- 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.
|
optionda-0.1.0/PKG-INFO
ADDED
|
@@ -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.
|
optionda-0.1.0/README.md
ADDED
|
@@ -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"]
|
optionda-0.1.0/setup.cfg
ADDED
|
@@ -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
|
+
)
|