From 3e7dfbf9e6ef734c7f913a5218ce9b05bfd94268 Mon Sep 17 00:00:00 2001 From: Xurxo Freitas Pereira Date: Wed, 20 Aug 2025 22:59:47 +0200 Subject: [PATCH] Added prices subscription --- README.md | 5 ++-- pyproject.toml | 2 +- pytr/main.py | 14 +++++++++- pytr/portfolio.py | 12 ++++++--- pytr/subscriptions.py | 59 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 84 insertions(+), 8 deletions(-) create mode 100644 pytr/subscriptions.py diff --git a/README.md b/README.md index 7e973cd5..ddccc2a9 100644 --- a/README.md +++ b/README.md @@ -62,12 +62,12 @@ $ uvx --with git+https://github.com/pytr-org/pytr.git pytr ```console usage: pytr [-h] [-V] [-v {warning,info,debug}] [--debug-logfile DEBUG_LOGFILE] [--debug-log-filter DEBUG_LOG_FILTER] - {help,login,dl_docs,portfolio,details,get_price_alarms,set_price_alarms,export_transactions,completion} ... + {help,login,dl_docs,portfolio,details,get_price_alarms,set_price_alarms,export_transactions,ticker,completion} ... Use "pytr command_name --help" to get detailed help to a specific command Commands: - {help,login,dl_docs,portfolio,details,get_price_alarms,set_price_alarms,export_transactions,completion} + {help,login,dl_docs,portfolio,details,get_price_alarms,set_price_alarms,export_transactions,ticker,completion} Desired action to perform help Print this help message login Check if credentials file exists. If not create it and ask for input. Try to @@ -81,6 +81,7 @@ Commands: set_price_alarms Set new price alarms export_transactions Create a CSV with the deposits and removals ready for importing into Portfolio Performance + ticker Subscribe to the price of a stock completion Print shell tab completion Options: diff --git a/pyproject.toml b/pyproject.toml index b9dfa2da..2a2318eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "pytr" -version = "0.4.3" +version = "0.4.4" description = "Use TradeRepublic in terminal" readme = "README.md" requires-python = ">=3.10" diff --git a/pytr/main.py b/pytr/main.py index 88164f06..6f2b6a23 100644 --- a/pytr/main.py +++ b/pytr/main.py @@ -17,6 +17,7 @@ from pytr.dl import DL from pytr.event import Event from pytr.portfolio import PORTFOLIO_COLUMNS, Portfolio +from pytr.subscriptions import run_subscribe_price_command from pytr.transactions import SUPPORTED_LANGUAGES, TransactionExporter from pytr.utils import check_version, get_logger @@ -296,6 +297,15 @@ def formatter(prog): help="The output file format.", ) + # ticker + info = "Subscribe to the price of a stock" + parser_ticker = parser_cmd.add_parser( + "ticker", formatter_class=formatter, help=info, description=info, parents=[parser_login_args] + ) + parser_ticker.add_argument("isin", help="ISIN of the stock") + parser_ticker.add_argument("--exchange", default="LSX", help="Exchange (default: LSX)") + parser_ticker.set_defaults(func=run_subscribe_price_command) + info = "Print shell tab completion" parser_completion = parser_cmd.add_parser( "completion", @@ -339,7 +349,9 @@ def main(): if args.verbosity.upper() == "DEBUG": log.debug("logging is set to debug") - if args.command == "login": + if hasattr(args, "func"): + args.func(args) + elif args.command == "login": login( phone_no=args.phone_no, pin=args.pin, diff --git a/pytr/portfolio.py b/pytr/portfolio.py index 253f9d2e..58b5b7a2 100644 --- a/pytr/portfolio.py +++ b/pytr/portfolio.py @@ -209,9 +209,11 @@ def portfolio_to_csv(self): csv_lines = [] for pos in sorted(self.portfolio, key=self._get_sort_func(), reverse=self.sort_descending): + exchange = pos["exchangeIds"][0] if pos.get("exchangeIds") and len(pos["exchangeIds"]) > 0 else "" csv_lines.append( f"{pos['name']};" f"{pos['instrumentId']};" + f"{exchange};" f"{self._decimal_format(pos['netSize'], precision=6)};" f"{self._decimal_format(pos['price'], precision=4)};" f"{self._decimal_format(pos['averageBuyIn'], precision=4)};" @@ -220,7 +222,7 @@ def portfolio_to_csv(self): Path(self.output).parent.mkdir(parents=True, exist_ok=True) with open(self.output, "w", encoding="utf-8") as f: - f.write("Name;ISIN;quantity;price;avgCost;netValue\n") + f.write("Name;ISIN;Exchange;quantity;price;avgCost;netValue\n") f.write("\n".join(csv_lines) + ("\n" if csv_lines else "")) print(f"Wrote {len(csv_lines) + 1} lines to {self.output}") @@ -231,10 +233,11 @@ def overview(self): if not self.output: print( - "Name ISIN avgCost * quantity = buyCost -> netValue price diff %-diff" + "Name ISIN Exchange avgCost * quantity = buyCost -> netValue price diff %-diff" ) for pos in sorted(self.portfolio, key=self._get_sort_func(), reverse=self.sort_descending): + exchange = pos["exchangeIds"][0] if pos.get("exchangeIds") and len(pos["exchangeIds"]) > 0 else "" buyCost = (Decimal(pos["averageBuyIn"]) * Decimal(pos["netSize"])).quantize( Decimal("0.01"), rounding=ROUND_HALF_UP ) @@ -246,7 +249,8 @@ def overview(self): if not self.output: print( f"{pos['name']:<25.25} " - f"{pos['instrumentId']} " + f"{pos['instrumentId']:<12} " + f"{exchange:<10} " f"{Decimal(pos['averageBuyIn']):>10.2f} * " f"{Decimal(pos['netSize']):>10.6f} = " f"{buyCost:>10.2f} -> " @@ -258,7 +262,7 @@ def overview(self): if not self.output: print( - "Name ISIN avgCost * quantity = buyCost -> netValue price diff %-diff" + "Name ISIN Exchange avgCost * quantity = buyCost -> netValue price diff %-diff" ) print() diff --git a/pytr/subscriptions.py b/pytr/subscriptions.py new file mode 100644 index 00000000..64be8026 --- /dev/null +++ b/pytr/subscriptions.py @@ -0,0 +1,59 @@ +import asyncio + +from websockets.exceptions import ConnectionClosedError, ConnectionClosedOK # modificado + +from pytr.account import login +from pytr.api import TradeRepublicApi + + +async def subscribe_price(tr_api: TradeRepublicApi, isin: str, exchange="LSX"): + # Inicia la suscripción al precio mediante ticker + await tr_api.ticker(isin, exchange) + print(f"Subscribed to ticker for {isin} on {exchange}.") + + # Loop para recibir actualizaciones + while True: + subscription_id, subscription, response = await tr_api.recv() + if subscription.get("type") == "ticker": + price = response.get("last", {}).get("price") + print(f"Updated price for {isin}: {price}") + + +async def subscribe_price_command(args): + # Se usa login para obtener una instancia autenticada de TradeRepublicApi + tr_api = await asyncio.to_thread( + login, + phone_no=args.phone_no, + pin=args.pin, + web=not args.applogin, + store_credentials=args.store_credentials, + ) + exchange = getattr(args, "exchange", "LSX") + while True: + try: + await subscribe_price(tr_api, args.isin, exchange) + except (ConnectionClosedError, ConnectionClosedOK): + print("Connection closed, reconnecting...") + await asyncio.sleep(1) + continue + except ValueError as e: + if "validate connection token failed" in str(e): + print("Token validation failed, re-authenticating...") + tr_api = await asyncio.to_thread( + login, + phone_no=args.phone_no, + pin=args.pin, + web=not args.applogin, + store_credentials=args.store_credentials, + ) + await asyncio.sleep(1) + continue + else: + raise + else: + break + + +# Función para ejecutar la suscripción desde el CLI +def run_subscribe_price_command(args): + asyncio.run(subscribe_price_command(args))