From 7ba20d0f82f5d0fd60f6b4d9859a8c61db90176c Mon Sep 17 00:00:00 2001 From: Luca Date: Fri, 20 Dec 2024 17:36:22 +0000 Subject: [PATCH 1/2] Add search to client --- quantflow/cli/app.py | 78 +++-------------------------- quantflow/cli/commands.py | 102 ++++++++++++++++++++++++++++++++++++++ quantflow/cli/script.py | 4 ++ 3 files changed, 114 insertions(+), 70 deletions(-) create mode 100644 quantflow/cli/commands.py diff --git a/quantflow/cli/app.py b/quantflow/cli/app.py index dfe1fb5..fc2b834 100644 --- a/quantflow/cli/app.py +++ b/quantflow/cli/app.py @@ -1,25 +1,15 @@ -import asyncio import os from dataclasses import dataclass, field -from typing import Any, Self +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()) +from . import settings, commands @click.group() @@ -27,68 +17,16 @@ def qf() -> None: pass -@qf.command() -@click.argument("symbol") -@click.pass_context -def profile(ctx: click.Context, symbol: str) -> None: - """Company profile""" - app = QfApp.from_context(ctx) - data = asyncio.run(get_profile(symbol))[0] - app.print(data.pop("description")) - df = pd.DataFrame(data.items(), columns=["Key", "Value"]) - app.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) +qf.add_command(commands.exit) +qf.add_command(commands.profile) +qf.add_command(commands.search) +qf.add_command(commands.chart) @dataclass class QfApp: console: Console = field(default_factory=Console) - @classmethod - def from_context(cls, ctx: click.Context) -> Self: - return ctx.obj # type: ignore - def __call__(self) -> None: os.makedirs(settings.SETTINGS_DIRECTORY, exist_ok=True) history = FileHistory(str(settings.HIST_FILE_PATH)) @@ -123,8 +61,6 @@ def handle_command(self, text: str) -> None: return elif text == "help": return qf.main(["--help"], standalone_mode=False, obj=self) - elif text == "exit": - raise click.Abort() try: qf.main(text.split(), standalone_mode=False, obj=self) @@ -132,3 +68,5 @@ def handle_command(self, text: str) -> None: self.error(e) except click.exceptions.NoSuchOption as e: self.error(e) + except click.exceptions.UsageError as e: + self.error(e) diff --git a/quantflow/cli/commands.py b/quantflow/cli/commands.py new file mode 100644 index 0000000..4aa897a --- /dev/null +++ b/quantflow/cli/commands.py @@ -0,0 +1,102 @@ +from __future__ import annotations + +import click +import asyncio +import pandas as pd +from typing import TYPE_CHECKING +from asciichartpy import plot +from ccy.cli.console import df_to_rich +from quantflow.data.fmp import FMP + +FREQUENCIES = tuple(FMP().historical_frequencies()) + +if TYPE_CHECKING: + from quantflow.cli.app import QfApp + + +def from_context(ctx: click.Context) -> QfApp: + return ctx.obj # type: ignore + + +@click.command() +def exit() -> None: + """Exit the program""" + raise click.Abort() + + +@click.command() +@click.argument("symbol") +@click.pass_context +def profile(ctx: click.Context, symbol: str) -> None: + """Company profile""" + app = from_context(ctx) + data = asyncio.run(get_profile(symbol)) + if not data: + app.error(f"Company {symbol} not found - try searching") + else: + d = data[0] + app.print(d.pop("description") or "") + df = pd.DataFrame(d.items(), columns=["Key", "Value"]) + app.print(df_to_rich(df)) + + +@click.command() +@click.argument("text") +@click.pass_context +def search(ctx: click.Context, text: str) -> None: + """Search companies""" + app = from_context(ctx) + data = asyncio.run(search_company(text)) + df = pd.DataFrame(data, columns=["symbol", "name", "currency", "stockExchange"]) + app.print(df_to_rich(df)) + + +@click.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="Frequency of data - if not provided it is daily", +) +def chart(symbol: str, height: int, length: int, frequency: str) -> None: + """Symbol chart""" + df = asyncio.run(get_prices(symbol, frequency)) + if df.empty: + raise click.UsageError( + f"No data for {symbol} - are you sure the symbol exists?" + ) + 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) + + +async def search_company(text: str) -> list[dict]: + async with FMP() as cli: + return await cli.search(text) diff --git a/quantflow/cli/script.py b/quantflow/cli/script.py index a3a2136..6dddfc9 100644 --- a/quantflow/cli/script.py +++ b/quantflow/cli/script.py @@ -1,3 +1,7 @@ +import dotenv + +dotenv.load_dotenv() + try: from .app import QfApp except ImportError: From 56388ea04ffab74384c64a1bac082e28f1b60faa Mon Sep 17 00:00:00 2001 From: Luca Date: Fri, 20 Dec 2024 19:14:10 +0000 Subject: [PATCH 2/2] raise dont print --- quantflow/cli/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantflow/cli/commands.py b/quantflow/cli/commands.py index 4aa897a..7cafd8a 100644 --- a/quantflow/cli/commands.py +++ b/quantflow/cli/commands.py @@ -32,7 +32,7 @@ def profile(ctx: click.Context, symbol: str) -> None: app = from_context(ctx) data = asyncio.run(get_profile(symbol)) if not data: - app.error(f"Company {symbol} not found - try searching") + raise click.UsageError(f"Company {symbol} not found - try searching") else: d = data[0] app.print(d.pop("description") or "")