Skip to content

Commit

Permalink
Merge pull request #5 from quantmind/ls-deribit
Browse files Browse the repository at this point in the history
Add command line client
  • Loading branch information
lsbardel committed Mar 24, 2024
2 parents 9d8c1a1 + b049465 commit e757e13
Show file tree
Hide file tree
Showing 14 changed files with 1,545 additions and 1,242 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
FMP_API_KEY: ${{ secrets.FMP_API_KEY }}
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
python-version: ["3.11", "3.12"]

steps:
- uses: actions/checkout@v3
Expand Down
2 changes: 1 addition & 1 deletion dev/lint
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ fi
echo black
black quantflow quantflow_tests ${BLACK_ARG}
echo ruff
ruff quantflow quantflow_tests ${RUFF_ARG}
ruff check quantflow quantflow_tests ${RUFF_ARG}
echo mypy
mypy quantflow
echo mypy tests
Expand Down
2 changes: 1 addition & 1 deletion notebooks/data/fmp.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ jupytext:
extension: .md
format_name: myst
format_version: 0.13
jupytext_version: 1.14.7
jupytext_version: 1.16.1
kernelspec:
display_name: Python 3 (ipykernel)
language: python
Expand Down
39 changes: 39 additions & 0 deletions notebooks/data/timeseries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
jupytext:
text_representation:
extension: .md
format_name: myst
format_version: 0.13
jupytext_version: 1.16.1
kernelspec:
display_name: Python 3 (ipykernel)
language: python
name: python3
---

## Timeseries

```{code-cell} ipython3
from quantflow.data.fmp import FMP
from quantflow.utils.plot import candlestick_plot
cli = FMP()
```

```{code-cell} ipython3
prices = await cli.prices("ethusd", frequency="")
```

```{code-cell} ipython3
candlestick_plot(prices).update_layout(height=500)
```

```{code-cell} ipython3
from quantflow.utils.df import DFutils
df = DFutils(prices).with_rogers_satchel().with_parkinson()
df
```

```{code-cell} ipython3
```
2,435 changes: 1,230 additions & 1,205 deletions poetry.lock

Large diffs are not rendered by default.

51 changes: 25 additions & 26 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,24 @@ Repository = "https://github.com/quantmind/quantflow"
Documentation = "https://quantmind.github.io/quantflow/"

[tool.poetry.dependencies]
python = ">=3.10,<3.13"
python = ">=3.11,<3.13"
numpy = "^1.22.3"
scipy = "^1.10.1"
pandas = "^2.0.1"
aiohttp = {version = "^3.8.1", optional = true}
pydantic = "^2.0.2"
pyarrow = "^15.0.0"
ccy = {version="^1.4.0", extras=["cli"]}
asciichart = "^0.1"
python-dotenv = "^1.0.1"
asciichartpy = "^1.5.25"
prompt-toolkit = "^3.0.43"
polars = {version = "^0.20.16", extras=["pandas", "pyarrow"]}

[tool.poetry.group.dev.dependencies]
black = "^24.1.1"
pytest-cov = "^4.0.0"
mypy = "^1.4.0"
mypy = "^1.9.0"
ghp-import = "^2.0.2"
ruff = "^0.1.14"
ruff = "^0.3.4"
pytest-asyncio = "^0.23.3"


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

[tool.poetry.group.book.dependencies]
jupyter-book = "^0.15.1"
nbconvert = "^6.4.5"
jupyter-book = "^1.0.0"
nbconvert = "^7.16.3"
jupytext = "^1.13.8"
plotly = "^5.7.0"
plotly = "^5.20.0"
jupyterlab = "^4.0.2"
sympy = "^1.12"
ipywidgets = "^8.0.7"

[tool.poetry.scripts]
qf = "quantflow.cli:main"

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
Expand All @@ -64,7 +71,7 @@ filterwarnings = [
profile = "black"

[tool.ruff]
select = ["E", "F"]
lint.select = ["E", "F"]
extend-exclude = ["fluid_apps/db/migrations"]
line-length = 88

Expand All @@ -78,21 +85,13 @@ disallow_untyped_defs = true
warn_no_return = true

[[tool.mypy.overrides]]
module = "quantflow_tests.*"
disallow_untyped_defs = false

[[tool.mypy.overrides]]
module = "IPython.*"
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = "pandas.*"
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = "plotly.*"
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = "scipy.*"
module = [
"asciichartpy.*",
"quantflow_tests.*",
"IPython.*",
"pandas.*",
"plotly.*",
"scipy.*"
]
ignore_missing_imports = true
disallow_untyped_defs = false
131 changes: 131 additions & 0 deletions quantflow/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import asyncio
import os
from dataclasses import dataclass, field
from typing import Any

import click
import dotenv
import pandas as pd
from asciichartpy import plot
from ccy.cli.console import df_to_rich
from prompt_toolkit import PromptSession
from prompt_toolkit.history import FileHistory
from rich.console import Console
from rich.text import Text

from quantflow.data.fmp import FMP

from . import settings

dotenv.load_dotenv()

FREQUENCIES = tuple(FMP().historical_frequencies())


@click.group()
def qf() -> None:
pass


@qf.command()
@click.argument("symbol")
def profile(symbol: str) -> None:
"""Company profile"""
data = asyncio.run(get_profile(symbol))[0]
main.print(data.pop("description"))
df = pd.DataFrame(data.items(), columns=["Key", "Value"])
main.print(df_to_rich(df))


@qf.command()
@click.argument("symbol")
@click.option(
"-h",
"--height",
type=int,
default=20,
show_default=True,
help="Chart height",
)
@click.option(
"-l",
"--length",
type=int,
default=100,
show_default=True,
help="Number of data points",
)
@click.option(
"-f",
"--frequency",
type=click.Choice(FREQUENCIES),
default="",
help="Number of data points",
)
def chart(symbol: str, height: int, length: int, frequency: str) -> None:
"""Symbol chart"""
df = asyncio.run(get_prices(symbol, frequency))
data = list(reversed(df["close"].tolist()[:length]))
print(plot(data, {"height": height}))


async def get_prices(symbol: str, frequency: str) -> pd.DataFrame:
async with FMP() as cli:
return await cli.prices(symbol, frequency)


async def get_profile(symbol: str) -> list[dict]:
async with FMP() as cli:
return await cli.profile(symbol)


@dataclass
class App:
console: Console = field(default_factory=Console)

def __call__(self) -> None:
os.makedirs(settings.SETTINGS_DIRECTORY, exist_ok=True)
history = FileHistory(str(settings.HIST_FILE_PATH))
session: PromptSession = PromptSession(history=history)

self.print("Welcome to QuantFlow!", style="bold green")
self.handle_command("help")

try:
while True:
try:
text = session.prompt("quantflow> ")
except KeyboardInterrupt:
break
else:
self.handle_command(text)
except click.Abort:
self.console.print(Text("Bye!", style="bold magenta"))

def print(self, text_alike: Any, style: str = "") -> None:
if isinstance(text_alike, str):
style = style or "cyan"
text_alike = Text(f"\n{text_alike}\n", style="cyan")
self.console.print(text_alike)

def error(self, err: str | Exception) -> None:
self.console.print(Text(f"\n{err}\n", style="bold red"))

def handle_command(self, text: str) -> None:
self.current_command = text.split(" ")[0].strip()
if not text:
return
elif text == "help":
return qf.main(["--help"], standalone_mode=False)
elif text == "exit":
raise click.Abort()

try:
qf.main(text.split(), standalone_mode=False)
except click.exceptions.MissingParameter as e:
self.error(e)
except click.exceptions.NoSuchOption as e:
self.error(e)


main = App()
11 changes: 11 additions & 0 deletions quantflow/cli/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# IMPORTATION STANDARD
from pathlib import Path

# Installation related paths
HOME_DIRECTORY = Path.home()
PACKAGE_DIRECTORY = Path(__file__).parent.parent.parent
REPOSITORY_DIRECTORY = PACKAGE_DIRECTORY.parent

SETTINGS_DIRECTORY = HOME_DIRECTORY / ".quantflow"
SETTINGS_ENV_FILE = SETTINGS_DIRECTORY / ".env"
HIST_FILE_PATH = SETTINGS_DIRECTORY / ".quantflow.his"
4 changes: 2 additions & 2 deletions quantflow/data/client.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import json
import os
from dataclasses import dataclass
from typing import Any
from typing import Any, Self

from aiohttp import ClientResponse, ClientSession
from aiohttp.client_exceptions import ContentTypeError
Expand Down Expand Up @@ -52,7 +52,7 @@ async def close(self) -> None:
await self.session.close()
self.session = None

async def __aenter__(self) -> "HttpClient":
async def __aenter__(self) -> Self:
return self

async def __aexit__(self, *args: Any) -> None:
Expand Down
16 changes: 11 additions & 5 deletions quantflow/data/fmp.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import os
from dataclasses import dataclass
from dataclasses import dataclass, field
from datetime import date, timedelta
from typing import Any, cast

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

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

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

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

async def prices(self, ticker: str, frequency: str = "", **kw: Any) -> pd.DataFrame:
async def prices(
self, ticker: str, frequency: str = "", to_date: bool = False, **kw: Any
) -> pd.DataFrame:
base = (
"historical-price-full/"
if not frequency
Expand All @@ -121,10 +123,14 @@ async def prices(self, ticker: str, frequency: str = "", **kw: Any) -> pd.DataFr
if isinstance(data, dict):
data = data.get("historical", [])
df = pd.DataFrame(data)
if "date" in df.columns:
if to_date and "date" in df.columns:
df["date"] = pd.to_datetime(df["date"])
return df

# forex
async def forex_list(self) -> list[dict]:
return await self.get_path("v3/symbol/available-forex-currency-pairs")

def historical_frequencies(self) -> dict:
return {
"1min": 1,
Expand Down

0 comments on commit e757e13

Please sign in to comment.