Skip to content

Commit 27d377f

Browse files
authored
Fred charts (#12)
1 parent 7e54fe6 commit 27d377f

File tree

17 files changed

+254
-99
lines changed

17 files changed

+254
-99
lines changed

dev/lint

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
#!/usr/bin/env bash
22
set -e
33

4+
ISORT_ARGS="-c"
45
BLACK_ARG="--check"
56
RUFF_ARG=""
67

78
if [ "$1" = "fix" ] ; then
9+
ISORT_ARGS=""
810
BLACK_ARG=""
911
RUFF_ARG="--fix"
1012
fi
1113

14+
echo isort
15+
isort quantflow quantflow_tests ${ISORT_ARGS}
1216
echo black
1317
black quantflow quantflow_tests ${BLACK_ARG}
1418
echo ruff

poetry.lock

Lines changed: 15 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ mypy = "^1.13.0"
3131
ghp-import = "^2.0.2"
3232
ruff = "^0.8.1"
3333
pytest-asyncio = "^0.25.0"
34+
isort = "^5.13.2"
3435

3536

3637
[tool.poetry.extras]

quantflow/cli/app.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
import os
22
from dataclasses import dataclass, field
3-
from typing import Any
43
from functools import partial
4+
from typing import Any
5+
56
import click
67
from prompt_toolkit import PromptSession
78
from prompt_toolkit.history import FileHistory
89
from rich.console import Console
910
from rich.text import Text
10-
from .commands import fred, stocks, vault
11-
from quantflow.data.vault import Vault
11+
1212
from quantflow.data.fmp import FMP
1313
from quantflow.data.fred import Fred
14+
from quantflow.data.vault import Vault
1415

1516
from . import settings
17+
from .commands import fred, stocks, vault
1618

1719

1820
@click.group()

quantflow/cli/commands/base.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING, Self, cast
4+
5+
import click
6+
7+
from quantflow.data.fmp import FMP
8+
from quantflow.data.fred import Fred
9+
10+
if TYPE_CHECKING:
11+
from quantflow.cli.app import QfApp
12+
13+
14+
class QuantContext(click.Context):
15+
16+
@classmethod
17+
def current(cls) -> Self:
18+
return cast(Self, click.get_current_context())
19+
20+
@property
21+
def qf(self) -> QfApp:
22+
return self.obj # type: ignore
23+
24+
def fmp(self) -> FMP:
25+
if key := self.qf.vault.get("fmp"):
26+
return FMP(key=key)
27+
else:
28+
raise click.UsageError("No FMP API key found")
29+
30+
def fred(self) -> Fred:
31+
if key := self.qf.vault.get("fred"):
32+
return Fred(key=key)
33+
else:
34+
raise click.UsageError("No FRED API key found")
35+
36+
37+
class QuantCommand(click.Command):
38+
context_class = QuantContext
39+
40+
41+
class QuantGroup(click.Group):
42+
context_class = QuantContext
43+
command_class = QuantCommand

quantflow/cli/commands/fred.py

Lines changed: 110 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,149 @@
11
from __future__ import annotations
22

3-
import click
43
import asyncio
5-
import pandas as pd
64
from typing import TYPE_CHECKING
5+
6+
import click
7+
import pandas as pd
8+
from asciichartpy import plot
9+
from ccy.cli.console import df_to_rich
710
from fluid.utils.data import compact_dict
811
from fluid.utils.http_client import HttpResponseError
9-
from ccy.cli.console import df_to_rich
10-
from quantflow.data.fmp import FMP
11-
from .stocks import from_context
1212

13+
from quantflow.data.fred import Fred
14+
15+
from .base import QuantContext, QuantGroup
1316

14-
FREQUENCIES = tuple(FMP().historical_frequencies())
17+
FREQUENCIES = tuple(Fred.freq)
1518

1619
if TYPE_CHECKING:
17-
from quantflow.cli.app import QfApp
20+
pass
1821

1922

20-
@click.group(invoke_without_command=True)
21-
@click.pass_context
22-
def fred(ctx: click.Context) -> None:
23+
@click.group(invoke_without_command=True, cls=QuantGroup)
24+
def fred() -> None:
2325
"""Federal Reserve of St. Louis data"""
26+
ctx = QuantContext.current()
2427
if ctx.invoked_subcommand is None:
25-
app = from_context(ctx)
26-
app.print("Welcome to FRED data commands!")
27-
app.print(ctx.get_help())
28+
ctx.qf.print("Welcome to FRED data commands!")
29+
ctx.qf.print(ctx.get_help())
2830

2931

3032
@fred.command()
31-
@click.pass_context
3233
@click.argument("category-id", required=False)
33-
def subcategories(ctx: click.Context, category_id: str | None = None) -> None:
34+
def subcategories(category_id: str | None = None) -> None:
3435
"""List subcategories for a Fred category"""
35-
app = from_context(ctx)
36+
ctx = QuantContext.current()
3637
try:
37-
data = asyncio.run(get_subcategories(app, category_id))
38+
data = asyncio.run(get_subcategories(ctx, category_id))
3839
except HttpResponseError as e:
39-
app.error(e)
40+
ctx.qf.error(e)
4041
else:
4142
df = pd.DataFrame(data["categories"], columns=["id", "name"])
42-
app.print(df_to_rich(df))
43+
ctx.qf.print(df_to_rich(df))
4344

4445

4546
@fred.command()
46-
@click.pass_context
4747
@click.argument("category-id")
48-
def series(ctx: click.Context, category_id: str) -> None:
48+
def series(category_id: str) -> None:
4949
"""List series for a Fred category"""
50-
app = from_context(ctx)
50+
ctx = QuantContext.current()
5151
try:
52-
data = asyncio.run(get_series(app, category_id))
52+
data = asyncio.run(get_series(ctx, category_id))
5353
except HttpResponseError as e:
54-
app.error(e)
54+
ctx.qf.error(e)
5555
else:
56-
app.print(data)
56+
ctx.qf.print(data)
5757
# df = pd.DataFrame(data["categories"], columns=["id", "name"])
5858
# app.print(df_to_rich(df))
5959

6060

61-
async def get_subcategories(app: QfApp, category_id: str | None) -> dict:
62-
async with app.fred() as cli:
61+
@fred.command()
62+
@click.argument("series-id")
63+
@click.option(
64+
"-l",
65+
"--length",
66+
type=int,
67+
default=100,
68+
show_default=True,
69+
help="Number of data points",
70+
)
71+
@click.option(
72+
"-f",
73+
"--frequency",
74+
type=click.Choice(FREQUENCIES),
75+
default="d",
76+
show_default=True,
77+
help="Frequency of data",
78+
)
79+
def data(series_id: str, length: int, frequency: str) -> None:
80+
"""Display a series data"""
81+
ctx = QuantContext.current()
82+
try:
83+
df = asyncio.run(get_serie_data(ctx, series_id, length, frequency))
84+
except HttpResponseError as e:
85+
ctx.qf.error(e)
86+
else:
87+
ctx.qf.print(df_to_rich(df))
88+
89+
90+
@fred.command()
91+
@click.argument("series-id")
92+
@click.option(
93+
"-h",
94+
"--height",
95+
type=int,
96+
default=20,
97+
show_default=True,
98+
help="Chart height",
99+
)
100+
@click.option(
101+
"-l",
102+
"--length",
103+
type=int,
104+
default=100,
105+
show_default=True,
106+
help="Number of data points",
107+
)
108+
@click.option(
109+
"-f",
110+
"--frequency",
111+
type=click.Choice(FREQUENCIES),
112+
default="w",
113+
show_default=True,
114+
help="Frequency of data",
115+
)
116+
def chart(series_id: str, height: int, length: int, frequency: str) -> None:
117+
"""Chart a serie"""
118+
ctx = QuantContext.current()
119+
try:
120+
df = asyncio.run(get_serie_data(ctx, series_id, length, frequency))
121+
except HttpResponseError as e:
122+
ctx.qf.error(e)
123+
else:
124+
data = list(reversed(df["value"].tolist()[:length]))
125+
ctx.qf.print(plot(data, {"height": height}))
126+
127+
128+
async def get_subcategories(ctx: QuantContext, category_id: str | None) -> dict:
129+
async with ctx.fred() as cli:
63130
return await cli.subcategories(params=compact_dict(category_id=category_id))
64131

65132

66-
async def get_series(app: QfApp, category_id: str) -> dict:
67-
async with app.fred() as cli:
133+
async def get_series(ctx: QuantContext, category_id: str) -> dict:
134+
async with ctx.fred() as cli:
68135
return await cli.series(params=compact_dict(category_id=category_id))
136+
137+
138+
async def get_serie_data(
139+
ctx: QuantContext, series_id: str, length: int, frequency: str
140+
) -> dict:
141+
async with ctx.fred() as cli:
142+
return await cli.serie_data(
143+
params=dict(
144+
series_id=series_id,
145+
limit=length,
146+
frequency=frequency,
147+
sort_order="desc",
148+
)
149+
)

0 commit comments

Comments
 (0)