Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,12 @@ $ uvx --with git+https://github.com/pytr-org/pytr.git pytr
<!-- runcmd code:console COLUMNS=120 uv run --python 3.13 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
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
14 changes: 13 additions & 1 deletion pytr/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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,
Expand Down
12 changes: 8 additions & 4 deletions pytr/portfolio.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)};"
Expand All @@ -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}")
Expand All @@ -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
)
Expand All @@ -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} -> "
Expand All @@ -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()

Expand Down
59 changes: 59 additions & 0 deletions pytr/subscriptions.py
Original file line number Diff line number Diff line change
@@ -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))