Skip to content

Commit e757e13

Browse files
authored
Merge pull request #5 from quantmind/ls-deribit
Add command line client
2 parents 9d8c1a1 + b049465 commit e757e13

File tree

14 files changed

+1545
-1242
lines changed

14 files changed

+1545
-1242
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
FMP_API_KEY: ${{ secrets.FMP_API_KEY }}
1717
strategy:
1818
matrix:
19-
python-version: ["3.10", "3.11", "3.12"]
19+
python-version: ["3.11", "3.12"]
2020

2121
steps:
2222
- uses: actions/checkout@v3

dev/lint

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ fi
1212
echo black
1313
black quantflow quantflow_tests ${BLACK_ARG}
1414
echo ruff
15-
ruff quantflow quantflow_tests ${RUFF_ARG}
15+
ruff check quantflow quantflow_tests ${RUFF_ARG}
1616
echo mypy
1717
mypy quantflow
1818
echo mypy tests

notebooks/data/fmp.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ jupytext:
55
extension: .md
66
format_name: myst
77
format_version: 0.13
8-
jupytext_version: 1.14.7
8+
jupytext_version: 1.16.1
99
kernelspec:
1010
display_name: Python 3 (ipykernel)
1111
language: python

notebooks/data/timeseries.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
jupytext:
3+
text_representation:
4+
extension: .md
5+
format_name: myst
6+
format_version: 0.13
7+
jupytext_version: 1.16.1
8+
kernelspec:
9+
display_name: Python 3 (ipykernel)
10+
language: python
11+
name: python3
12+
---
13+
14+
## Timeseries
15+
16+
```{code-cell} ipython3
17+
from quantflow.data.fmp import FMP
18+
from quantflow.utils.plot import candlestick_plot
19+
cli = FMP()
20+
```
21+
22+
```{code-cell} ipython3
23+
prices = await cli.prices("ethusd", frequency="")
24+
```
25+
26+
```{code-cell} ipython3
27+
candlestick_plot(prices).update_layout(height=500)
28+
```
29+
30+
```{code-cell} ipython3
31+
from quantflow.utils.df import DFutils
32+
33+
df = DFutils(prices).with_rogers_satchel().with_parkinson()
34+
df
35+
```
36+
37+
```{code-cell} ipython3
38+
39+
```

poetry.lock

Lines changed: 1230 additions & 1205 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,24 @@ Repository = "https://github.com/quantmind/quantflow"
1212
Documentation = "https://quantmind.github.io/quantflow/"
1313

1414
[tool.poetry.dependencies]
15-
python = ">=3.10,<3.13"
15+
python = ">=3.11,<3.13"
1616
numpy = "^1.22.3"
1717
scipy = "^1.10.1"
18-
pandas = "^2.0.1"
1918
aiohttp = {version = "^3.8.1", optional = true}
2019
pydantic = "^2.0.2"
21-
pyarrow = "^15.0.0"
20+
ccy = {version="^1.4.0", extras=["cli"]}
21+
asciichart = "^0.1"
22+
python-dotenv = "^1.0.1"
23+
asciichartpy = "^1.5.25"
24+
prompt-toolkit = "^3.0.43"
25+
polars = {version = "^0.20.16", extras=["pandas", "pyarrow"]}
2226

2327
[tool.poetry.group.dev.dependencies]
2428
black = "^24.1.1"
2529
pytest-cov = "^4.0.0"
26-
mypy = "^1.4.0"
30+
mypy = "^1.9.0"
2731
ghp-import = "^2.0.2"
28-
ruff = "^0.1.14"
32+
ruff = "^0.3.4"
2933
pytest-asyncio = "^0.23.3"
3034

3135

@@ -36,14 +40,17 @@ data = ["aiohttp"]
3640
optional = true
3741

3842
[tool.poetry.group.book.dependencies]
39-
jupyter-book = "^0.15.1"
40-
nbconvert = "^6.4.5"
43+
jupyter-book = "^1.0.0"
44+
nbconvert = "^7.16.3"
4145
jupytext = "^1.13.8"
42-
plotly = "^5.7.0"
46+
plotly = "^5.20.0"
4347
jupyterlab = "^4.0.2"
4448
sympy = "^1.12"
4549
ipywidgets = "^8.0.7"
4650

51+
[tool.poetry.scripts]
52+
qf = "quantflow.cli:main"
53+
4754
[build-system]
4855
requires = ["poetry-core>=1.0.0"]
4956
build-backend = "poetry.core.masonry.api"
@@ -64,7 +71,7 @@ filterwarnings = [
6471
profile = "black"
6572

6673
[tool.ruff]
67-
select = ["E", "F"]
74+
lint.select = ["E", "F"]
6875
extend-exclude = ["fluid_apps/db/migrations"]
6976
line-length = 88
7077

@@ -78,21 +85,13 @@ disallow_untyped_defs = true
7885
warn_no_return = true
7986

8087
[[tool.mypy.overrides]]
81-
module = "quantflow_tests.*"
82-
disallow_untyped_defs = false
83-
84-
[[tool.mypy.overrides]]
85-
module = "IPython.*"
86-
ignore_missing_imports = true
87-
88-
[[tool.mypy.overrides]]
89-
module = "pandas.*"
90-
ignore_missing_imports = true
91-
92-
[[tool.mypy.overrides]]
93-
module = "plotly.*"
94-
ignore_missing_imports = true
95-
96-
[[tool.mypy.overrides]]
97-
module = "scipy.*"
88+
module = [
89+
"asciichartpy.*",
90+
"quantflow_tests.*",
91+
"IPython.*",
92+
"pandas.*",
93+
"plotly.*",
94+
"scipy.*"
95+
]
9896
ignore_missing_imports = true
97+
disallow_untyped_defs = false

quantflow/cli/__init__.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import asyncio
2+
import os
3+
from dataclasses import dataclass, field
4+
from typing import Any
5+
6+
import click
7+
import dotenv
8+
import pandas as pd
9+
from asciichartpy import plot
10+
from ccy.cli.console import df_to_rich
11+
from prompt_toolkit import PromptSession
12+
from prompt_toolkit.history import FileHistory
13+
from rich.console import Console
14+
from rich.text import Text
15+
16+
from quantflow.data.fmp import FMP
17+
18+
from . import settings
19+
20+
dotenv.load_dotenv()
21+
22+
FREQUENCIES = tuple(FMP().historical_frequencies())
23+
24+
25+
@click.group()
26+
def qf() -> None:
27+
pass
28+
29+
30+
@qf.command()
31+
@click.argument("symbol")
32+
def profile(symbol: str) -> None:
33+
"""Company profile"""
34+
data = asyncio.run(get_profile(symbol))[0]
35+
main.print(data.pop("description"))
36+
df = pd.DataFrame(data.items(), columns=["Key", "Value"])
37+
main.print(df_to_rich(df))
38+
39+
40+
@qf.command()
41+
@click.argument("symbol")
42+
@click.option(
43+
"-h",
44+
"--height",
45+
type=int,
46+
default=20,
47+
show_default=True,
48+
help="Chart height",
49+
)
50+
@click.option(
51+
"-l",
52+
"--length",
53+
type=int,
54+
default=100,
55+
show_default=True,
56+
help="Number of data points",
57+
)
58+
@click.option(
59+
"-f",
60+
"--frequency",
61+
type=click.Choice(FREQUENCIES),
62+
default="",
63+
help="Number of data points",
64+
)
65+
def chart(symbol: str, height: int, length: int, frequency: str) -> None:
66+
"""Symbol chart"""
67+
df = asyncio.run(get_prices(symbol, frequency))
68+
data = list(reversed(df["close"].tolist()[:length]))
69+
print(plot(data, {"height": height}))
70+
71+
72+
async def get_prices(symbol: str, frequency: str) -> pd.DataFrame:
73+
async with FMP() as cli:
74+
return await cli.prices(symbol, frequency)
75+
76+
77+
async def get_profile(symbol: str) -> list[dict]:
78+
async with FMP() as cli:
79+
return await cli.profile(symbol)
80+
81+
82+
@dataclass
83+
class App:
84+
console: Console = field(default_factory=Console)
85+
86+
def __call__(self) -> None:
87+
os.makedirs(settings.SETTINGS_DIRECTORY, exist_ok=True)
88+
history = FileHistory(str(settings.HIST_FILE_PATH))
89+
session: PromptSession = PromptSession(history=history)
90+
91+
self.print("Welcome to QuantFlow!", style="bold green")
92+
self.handle_command("help")
93+
94+
try:
95+
while True:
96+
try:
97+
text = session.prompt("quantflow> ")
98+
except KeyboardInterrupt:
99+
break
100+
else:
101+
self.handle_command(text)
102+
except click.Abort:
103+
self.console.print(Text("Bye!", style="bold magenta"))
104+
105+
def print(self, text_alike: Any, style: str = "") -> None:
106+
if isinstance(text_alike, str):
107+
style = style or "cyan"
108+
text_alike = Text(f"\n{text_alike}\n", style="cyan")
109+
self.console.print(text_alike)
110+
111+
def error(self, err: str | Exception) -> None:
112+
self.console.print(Text(f"\n{err}\n", style="bold red"))
113+
114+
def handle_command(self, text: str) -> None:
115+
self.current_command = text.split(" ")[0].strip()
116+
if not text:
117+
return
118+
elif text == "help":
119+
return qf.main(["--help"], standalone_mode=False)
120+
elif text == "exit":
121+
raise click.Abort()
122+
123+
try:
124+
qf.main(text.split(), standalone_mode=False)
125+
except click.exceptions.MissingParameter as e:
126+
self.error(e)
127+
except click.exceptions.NoSuchOption as e:
128+
self.error(e)
129+
130+
131+
main = App()

quantflow/cli/settings.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# IMPORTATION STANDARD
2+
from pathlib import Path
3+
4+
# Installation related paths
5+
HOME_DIRECTORY = Path.home()
6+
PACKAGE_DIRECTORY = Path(__file__).parent.parent.parent
7+
REPOSITORY_DIRECTORY = PACKAGE_DIRECTORY.parent
8+
9+
SETTINGS_DIRECTORY = HOME_DIRECTORY / ".quantflow"
10+
SETTINGS_ENV_FILE = SETTINGS_DIRECTORY / ".env"
11+
HIST_FILE_PATH = SETTINGS_DIRECTORY / ".quantflow.his"

quantflow/data/client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import json
22
import os
33
from dataclasses import dataclass
4-
from typing import Any
4+
from typing import Any, Self
55

66
from aiohttp import ClientResponse, ClientSession
77
from aiohttp.client_exceptions import ContentTypeError
@@ -52,7 +52,7 @@ async def close(self) -> None:
5252
await self.session.close()
5353
self.session = None
5454

55-
async def __aenter__(self) -> "HttpClient":
55+
async def __aenter__(self) -> Self:
5656
return self
5757

5858
async def __aexit__(self, *args: Any) -> None:

quantflow/data/fmp.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import os
2-
from dataclasses import dataclass
2+
from dataclasses import dataclass, field
33
from datetime import date, timedelta
44
from typing import Any, cast
55

@@ -12,7 +12,7 @@
1212
@dataclass
1313
class FMP(HttpClient):
1414
url: str = "https://financialmodelingprep.com/api"
15-
key: str = os.environ.get("FMP_API_KEY", "")
15+
key: str = field(default_factory=lambda: os.environ.get("FMP_API_KEY", ""))
1616

1717
async def stocks(self, **kw: Any) -> list[dict]:
1818
return await self.get_path("v3/stock/list", **kw)
@@ -62,7 +62,7 @@ async def insider_trading(self, ticker: str, **kw: Any) -> list[dict]:
6262
# Rating
6363

6464
async def rating(self, ticker: str, **kw: Any) -> list[dict]:
65-
"""Company quote - real time"""
65+
"""Company rating - real time"""
6666
return await self.get_path(f"v3/rating/{ticker}", **kw)
6767

6868
async def etf_holders(self, ticker: str, **kw: Any) -> list[dict]:
@@ -111,7 +111,9 @@ async def search(
111111
**self.params(compact(query=query, exchange=exchange, limit=limit), **kw),
112112
)
113113

114-
async def prices(self, ticker: str, frequency: str = "", **kw: Any) -> pd.DataFrame:
114+
async def prices(
115+
self, ticker: str, frequency: str = "", to_date: bool = False, **kw: Any
116+
) -> pd.DataFrame:
115117
base = (
116118
"historical-price-full/"
117119
if not frequency
@@ -121,10 +123,14 @@ async def prices(self, ticker: str, frequency: str = "", **kw: Any) -> pd.DataFr
121123
if isinstance(data, dict):
122124
data = data.get("historical", [])
123125
df = pd.DataFrame(data)
124-
if "date" in df.columns:
126+
if to_date and "date" in df.columns:
125127
df["date"] = pd.to_datetime(df["date"])
126128
return df
127129

130+
# forex
131+
async def forex_list(self) -> list[dict]:
132+
return await self.get_path("v3/symbol/available-forex-currency-pairs")
133+
128134
def historical_frequencies(self) -> dict:
129135
return {
130136
"1min": 1,

0 commit comments

Comments
 (0)