Skip to content

Commit d889312

Browse files
committedDec 23, 2020
add cli
1 parent f6028e2 commit d889312

14 files changed

+842
-279
lines changed
 

‎.vscode/launch.json

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
// Verwendet IntelliSense zum Ermitteln möglicher Attribute.
3+
// Zeigen Sie auf vorhandene Attribute, um die zugehörigen Beschreibungen anzuzeigen.
4+
// Weitere Informationen finden Sie unter https://go.microsoft.com/fwlink/?linkid=830387
5+
"version": "0.2.0",
6+
"configurations": [
7+
{
8+
"name": "Python: Modul",
9+
"type": "python",
10+
"request": "launch",
11+
"module": "pytr",
12+
"args": [
13+
"portfolio"
14+
]
15+
}
16+
]
17+
}

‎LICENSE

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2020 nborrmann
3+
Copyright (c) 2020 marzzzello
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

‎README.md

+35-176
Original file line numberDiff line numberDiff line change
@@ -1,195 +1,54 @@
1-
# Trade Republic API
1+
# pytr: Use TradeRepublic in terminal
22

33
This is a library for the private API of the Trade Republic online brokerage. I am not affiliated with Trade Republic Bank GmbH.
44

55
## Installation
66

7-
```
8-
pip install git+https://github.com/ptrstn/pytr
9-
```
7+
Install with `pip install pytr`
108

11-
## Authentication
12-
13-
First you need to perform a device reset - a private key will be generated that pins your "device". The private key is saved to your keyfile. This procedure will log you out from your mobile device.
9+
Or you can clone the repo like so:
1410

15-
```python
16-
from py_tr import TradeRepublicApi
17-
tr = TradeRepublicApi(phone_no="+4900000000000", pin="0000", keyfile='keyfile.pem')
18-
tr.initiate_device_reset()
19-
tr.complete_device_reset("0000") # Substitute the 2FA token that is sent to you via SMS.
11+
```sh
12+
git clone https://github.com/marzzzello/pytr.git
13+
cd pytr
14+
pip install .
2015
```
2116

22-
If no credentials are supplied the library will look for them in the file `~/.pytr/credentials` (the first line must contain the phone number, the second line the pin). If no keyfile is supplied the library will default to `~/.pytr/keyfile.pem`.
23-
24-
## Api Subscriptions
25-
26-
The Trade Republic API works fully asynchronously via Websocket. You subscribe to a 'topic', by sending a request using the `tr.subscribe(payload)` call (or any of the helper methods provided by the library). This will return a `subscription_id`, that you can use to identify responses belonging to this subscription.
27-
28-
After subscribing you will get one initial response on that subscription and an update whenever the response changes. E.g. market data subscriptions can update multiple times per second, but also the portfolio or watchlist subscriptions will receive an update if the positions change.
29-
30-
To receive the next response on the websocket, call `tr.recv()`. This will return a tuple consisting of
31-
1. the `subscription_id` that this response belongs to
32-
1. a dictionary that contains all the subscription parameters
33-
1. the response dictionary
34-
35-
If the Api replies with an error, a `TradeRepublicError` (also containing all three values) will be raised.
36-
37-
To unsubscribe from a topic, call `tr.unsubscribe(subscription_id)`.
38-
39-
The library does bookkeeping on current subscriptions in the `tr.subscriptions` dictionary, mapping `subscription_id` to a dictionary of the subscription parameters.
40-
41-
Sample code:
42-
43-
```python
44-
import asyncio
45-
from py_tr import TradeRepublicApi
46-
47-
tr = TradeRepublicApi(phone_no="+4900000000000", pin="0000", keyfile='keyfile.pem')
48-
49-
async def my_loop():
50-
cash_subscription_id = await tr.cash()
51-
await tr.ticker("DE0007236101", "LSX")
52-
await tr.ticker("DE0007100000", "LSX")
53-
54-
while True:
55-
subscription_id, subscription, response = await tr.recv()
56-
57-
# Identify response by subscription_id:
58-
if cash_subscription_id == subscription_id:
59-
print(f"Cash balance is {response}")
60-
61-
# Or identify response by subscription type:
62-
if subscription["type"] == "ticker":
63-
print(f"Current tick for {subscription['id']} is {response}")
17+
## Usage
6418

65-
asyncio.get_event_loop().run_until_complete(my_loop())
6619
```
20+
$ pytr help
21+
usage: pytr [-h] [-s {bash,zsh}] [-v {warning,info,debug}]
22+
{help,login,portfolio,dl_docs,set_price_alarms} ...
6723
68-
## Blocking Api Calls
24+
positional arguments:
25+
{help,login,portfolio,dl_docs,set_price_alarms}
26+
Desired action to perform
27+
help Print this help message
28+
login Check if credentials file exists. If not create it and
29+
ask for input. Try to login. Ask for device reset if
30+
needed
31+
portfolio Show current portfolio
32+
dl_docs Download all pdf documents from the timeline and sort
33+
them into folders
34+
set_price_alarms Set price alarms based on diff from current price
6935
70-
For convenience the library provides a helper function that communicates in a blocking manner:
71-
72-
```python
73-
portfolio = tr.run_blocking(tr.portfolio(), timeout=5.0)
36+
optional arguments:
37+
-h, --help show this help message and exit
38+
-s {bash,zsh}, --print-completion {bash,zsh}
39+
print shell completion script
40+
-v {warning,info,debug}, --verbosity {warning,info,debug}
41+
Set verbosity level
7442
```
7543

76-
This will subscribe to a topic, return the first response and immediately unsubscribe. If no response is returned this
77-
will time out after a default of five seconds. You can also prefix any method with `blocking_` to achieve the same result (eg: `tr.blocking_portfolio(timeout=5)`).
78-
79-
*Warning*: `tr.run_blocking()` will silently drop all messages belonging to different subscriptions, therefore do not use both approaches at the same time.
80-
81-
## All Subscriptions
82-
83-
The following subscriptions are supported by this library:
84-
85-
### Portfolio
86-
```python
87-
tr.portfolio()
88-
tr.cash()
89-
tr.available_cash_for_payout()
90-
tr.portfolio_status()
91-
tr.portfolio_history(timeframe)
92-
tr.experience()
93-
```
94-
### Watchlist
95-
```python
96-
tr.watchlist()
97-
tr.add_watchlist(isin)
98-
tr.remove_watchlist(isin)
99-
```
100-
### Market Data
101-
```python
102-
tr.instrument_details(isin)
103-
tr.instrument_suitability(isin)
104-
tr.stock_details(isin)
105-
tr.ticker(isin, exchange="LSX")
106-
tr.performance(isin, exchange="LSX")
107-
tr.performance_history(isin, timeframe, exchange="LSX", resolution=None)
108-
```
109-
### Timeline
110-
```python
111-
tr.timeline(after=None)
112-
tr.timeline_detail(timeline_id)
113-
tr.timeline_detail_order(order_id)
114-
tr.timeline_detail_savings_plan(savings_plan_id)
115-
```
116-
### Search
117-
```python
118-
tr.search_tags()
119-
tr.search_suggested_tags(query)
120-
tr.search(query, asset_type="stock", page=1, page_size=20, aggregate=False, only_savable=False,
121-
filter_index=None, filter_country=None, filter_sector=None, filter_region=None)
122-
tr.search_derivative(underlying_isin, product_type)
123-
```
124-
### Orders
44+
## Authentication
12545

126-
Be careful, these methods can create actual live trades.
46+
First you need to perform a device reset - a private key will be generated that pins your "device". The private key is saved to your keyfile. This procedure will log you out from your mobile device.
12747

128-
```python
129-
tr.order_overview()
130-
tr.cash_available_for_order()
131-
tr.size_available_for_order(isin, exchange)
132-
tr.price_for_order(isin, exchange, order_type)
133-
tr.market_order(isin, exchange, order_type, size, expiry, sell_fractions, expiry_date=None, warnings_shown=None)
134-
tr.limit_order(isin, exchange, order_type, size, limit, expiry, expiry_date=None, warnings_shown=None)
135-
tr.stop_market_order(isin, exchange, order_type, size, stop, expiry, expiry_date=None, warnings_shown=None)
136-
tr.cancel_order(order_id)
137-
```
138-
### Savings Plans
139-
```python
140-
tr.savings_plan_overview()
141-
tr.savings_plan_parameters(isin)
142-
tr.create_savings_plan(isin, amount, interval, start_date, start_date_type, start_date_value)
143-
tr.change_savings_plan(savings_plan_id, isin, amount, interval, start_date, start_date_type, start_date_value)
144-
tr.cancel_savings_plan(savings_plan_id)
145-
```
146-
### Price Alarms
147-
```python
148-
tr.price_alarm_overview()
149-
tr.create_price_alarm(isin, price)
150-
tr.cancel_price_alarm(price_alarm_id)
48+
```sh
49+
$ pytr login
50+
$ # or
51+
$ pytr login --phone_no +49123456789 --pin 1234
15152
```
152-
### News
153-
```python
154-
tr.news(isin)
155-
tr.news_subscriptions()
156-
tr.subscribe_news(isin)
157-
tr.unsubscribe_news(isin)
158-
```
159-
### Other
160-
```python
161-
tr.motd()
162-
tr.neon_cards()
163-
```
164-
### REST calls
165-
These Api calls are not asynchronous, but plain old rest calls.
166-
```python
167-
tr.settings()
168-
tr.order_cost(isin, exchange, order_mode, order_type, size, sell_fractions)
169-
tr.savings_plan_cost(isin, amount, interval)
170-
tr.payout(amount)
171-
tr.confirm_payout(process_id, code)
172-
```
173-
Payouts need two-factor-authentication: the `payout()` call will respond with a process_id and trigger an SMS with a code. Confirm the payout by calling `confirm_payout()` with the process_id and code.
174-
175-
### Parameters
176-
* **isin** `string`: the *International Securities Identification Number*
177-
* **timeframe** `string`: allowed values are `"1d"`, `"5d"`, `"1m"`, `"3m"`, `"6m"`, `"1y"`, `"5y"`, `"max"`
178-
* **exchange** `string`: identifies a stock exchange, usually `"LSX"` for *Lang & Schwarz Exchange*, other allowed values can be seen in the `instrument_details()` call
179-
* **resolution** `int` (optional): resolution for timeseries in milliseconds, minimum seems to be 60,000
180-
* **asset_type** `string`: allowed values are `"stock"`, `"fund"`, `"derivative"`
181-
* **order_type** `string`: allowed values are `"buy"` or `"sell"`
182-
* **size** `int`: how many shares to trade
183-
* **sell_fractions** `bool`: sell remaining fractional shares
184-
* **limit** `float`: limit price
185-
* **stop** `float`: stop price
186-
* **expiry** `string`: allowed values are `"gfd"` (good for day), `"gtd"` (good till date) and `"gtc"` (good till cancelled)
187-
* **expiry_date** `string` (optional): if expiry is `"gtd"`, specify a date in the format `"yyyy-mm-dd"`
188-
* **warnings_shown** `list of strings` (optional): may contain one or more of the following values: `"targetMarket"`, `"userExperience"`, `"unknown"` - however an empty list also seems to always be accepted
189-
* **amount** `int` savings plan amount in euro
190-
* **interval** `string` interval for savings plan execution, allowed values are `"everySecondWeek"`, `"weekly"`, `"twoPerMonth"`, `"monthly"`, `"quarterly"`
191-
* **start_date** `string` first execution date for savings plans, in format `"yyyy-mm-dd"`
192-
* **start_date_type** `string` allowed values are `"dayOfMonth"`, `"weekday"`
193-
* **start_date_value** `int` either the day of month (0-30), or the weekday (0-6) on which to execute the savings plan
19453

195-
Allowed values for search filters can be found using the `search_tags()` call.
54+
If no arguments are supplied pytr will look for them in the file `~/.pytr/credentials` (the first line must contain the phone number, the second line the pin). If the file doesn't exist pytr will ask for for the phone number and pin.

‎pytr/__init__.py

Whitespace-only changes.

‎pytr/__main__.py

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/usr/bin/env python3
2+
import logging
3+
4+
from pytr.main import main
5+
6+
if __name__ == '__main__':
7+
try:
8+
main()
9+
except KeyboardInterrupt:
10+
log = logging.getLogger(__name__)
11+
log.info('Exiting...')
12+
exit()
13+
except Exception as e:
14+
log = logging.getLogger(__name__)
15+
log.fatal(e)
16+
raise

‎pytr/account.py

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import json
2+
import os
3+
import pathlib
4+
import sys
5+
6+
from pygments import highlight, lexers, formatters
7+
8+
from pytr.api import TradeRepublicApi
9+
from pytr.utils import get_logger
10+
11+
12+
def get_settings(tr):
13+
formatted_json = json.dumps(tr.settings(), indent=2)
14+
if sys.stdout.isatty():
15+
colorful_json = highlight(formatted_json, lexers.JsonLexer(), formatters.TerminalFormatter())
16+
return colorful_json
17+
else:
18+
return formatted_json
19+
20+
21+
def reset(tr):
22+
tr.initiate_device_reset()
23+
print("You should have received a SMS with a token. Please type it in:")
24+
token = input()
25+
tr.complete_device_reset(token)
26+
print("Reset done")
27+
28+
29+
def login(phone_no=None, pin=None):
30+
"""
31+
Check if credentials file exists else create it.
32+
If no parameters are set but are needed then ask for input
33+
Try to login. Ask for device reset if needed
34+
"""
35+
home = pathlib.Path.home()
36+
credentials_file = os.path.join(home, ".pytr", "credentials")
37+
log = get_logger(__name__)
38+
39+
if os.path.isfile(credentials_file):
40+
log.info("Found credentials file")
41+
with open(credentials_file) as f:
42+
lines = f.readlines()
43+
phone_no = lines[0].strip()
44+
pin = lines[1].strip()
45+
log.info(f"Phone: {phone_no}, PIN: {pin}")
46+
else:
47+
log.info("Credentials file not found")
48+
if phone_no is None:
49+
print("Please enter your TradeRepbulic phone number in the format +49123456678:")
50+
phone_no = input()
51+
if pin is None:
52+
print("Please enter your TradeRepbulic pin:")
53+
pin = input()
54+
55+
with open(credentials_file, "w") as f:
56+
f.writelines([phone_no + "\n", pin + "\n"])
57+
58+
log.info(f"Saved credentials in {credentials_file}")
59+
60+
# use ~/.pytr/credentials and ~/.pytr/keyfile.pem
61+
tr = TradeRepublicApi()
62+
63+
try:
64+
tr.login()
65+
except (KeyError, AttributeError):
66+
# old keyfile or no keyfile
67+
print("Error logging in. Reset device? (y)")
68+
confirmation = input()
69+
if confirmation == "y":
70+
reset(tr)
71+
else:
72+
print("Cancelling reset")
73+
exit(1)
74+
log.info("Logged in")
75+
log.debug(get_settings(tr))
76+
return tr

‎src/py_tr/py_tr.py ‎pytr/api.py

+193-79
Large diffs are not rendered by default.

‎pytr/dl.py

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import os
2+
import re
3+
4+
from concurrent.futures import as_completed
5+
from requests_futures.sessions import FuturesSession
6+
7+
from pytr.utils import preview, Timeline
8+
9+
10+
class DL:
11+
def __init__(self, tr, output_path, headers={'User-Agent': 'pytr'}):
12+
self.tr = tr
13+
self.output_path = output_path
14+
self.headers = headers
15+
16+
self.session = FuturesSession()
17+
self.futures = []
18+
19+
self.docs_request = 0
20+
self.done = 0
21+
22+
self.tl = Timeline(self.tr)
23+
24+
async def dl_loop(self):
25+
await self.tl.get_next_timeline()
26+
27+
while True:
28+
_subscription_id, subscription, response = await self.tr.recv()
29+
30+
if subscription["type"] == "timeline":
31+
await self.tl.get_next_timeline(response)
32+
elif subscription["type"] == "timelineDetail":
33+
await self.tl.timelineDetail(response, self)
34+
else:
35+
print(f"unmatched subscription of type '{subscription['type']}':\n{preview(response)}")
36+
37+
def dl_doc(self, doc, titleText, subtitleText, subfolder=None):
38+
"""
39+
send asynchronous request, append future with filepath to self.futures
40+
"""
41+
doc_url = doc["action"]["payload"]
42+
43+
date = doc["detail"]
44+
iso_date = "-".join(date.split(".")[::-1])
45+
46+
# extract time from subtitleText
47+
time = re.findall("um (\\d+:\\d+) Uhr", subtitleText)
48+
if time == []:
49+
time = ""
50+
else:
51+
time = f" {time[0]}"
52+
53+
if subfolder is not None:
54+
directory = os.path.join(self.output_path, subfolder)
55+
else:
56+
directory = self.output_path
57+
58+
# If doc_type is something like "Kosteninformation 2", then strip the 2 and save it in doc_type_num
59+
doc_type = doc['title'].rsplit(" ")
60+
if doc_type[-1].isnumeric() is True:
61+
doc_type_num = f" {doc_type.pop()}"
62+
else:
63+
doc_type_num = ""
64+
65+
doc_type = " ".join(doc_type)
66+
filepath = os.path.join(directory, doc_type, f"{iso_date}{time} {titleText}{doc_type_num}.pdf")
67+
68+
# if response['titleText'] == "Shopify":
69+
# print(json.dumps(response))
70+
71+
if os.path.isfile(filepath) is False:
72+
self.docs_request += 1
73+
future = self.session.get(doc_url)
74+
future.filepath = filepath
75+
self.futures.append(future)
76+
else:
77+
print("file {filepath} already exists. Skipping...")
78+
79+
def work_responses(self):
80+
"""
81+
process responses of async requests
82+
"""
83+
for future in as_completed(self.futures):
84+
r = future.result()
85+
os.makedirs(os.path.dirname(future.filepath), exist_ok=True)
86+
with open(future.filepath, "wb") as f:
87+
f.write(r.content)
88+
self.done += 1
89+
90+
print(f"done: {self.done:>3}/{self.docs_request} {os.path.basename(future.filepath)}")
91+
92+
if self.done == self.docs_request:
93+
print("Done.")
94+
exit(0)
95+
96+
def dl_all(output_path):
97+
"""
98+
todo
99+
"""
100+
pass

‎pytr/main.py

+140
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
#!/usr/bin/env python
2+
3+
import argparse
4+
import asyncio
5+
import signal
6+
7+
8+
import shtab
9+
10+
from pytr.utils import get_logger
11+
from pytr.dl import DL
12+
from pytr.account import login
13+
from pytr.portfolio import Portfolio
14+
15+
16+
# async def my_loop(tr, dl):
17+
# # await tr.subscribe({"type": "unsubscribeNews"})
18+
# # await tr.order_overview()
19+
20+
# # await tr.timeline_detail("98d13dc6-5bd3-43c8-b74a-dae4e7728f4f")
21+
22+
# # await tr.ticker("DE0007236101", "LSX")
23+
# # await tr.ticker("DE0007100000", "LSX")
24+
25+
# while True:
26+
# _subscription_id, subscription, response = await tr.recv()
27+
28+
# # Identify response by subscription_id:
29+
# # if portfolio_subscription_id == subscription_id:
30+
31+
# if subscription["type"] == "orders":
32+
# print(f"Orders: {response}")
33+
34+
# # Or identify response by subscription type:
35+
# elif subscription["type"] == "ticker":
36+
# print(f"Current tick for {subscription['id']} is {response}")
37+
38+
# else:
39+
# print(f"unmatched subscription of type '{subscription['type']}':\n{preview(response)}")
40+
41+
42+
def get_main_parser():
43+
parser = argparse.ArgumentParser()
44+
shtab.add_argument_to(parser, ["-s", "--print-completion"]) # magic!
45+
46+
parser.add_argument(
47+
'-v', '--verbosity', help='Set verbosity level', choices=['warning', 'info', 'debug'], default='info'
48+
)
49+
subparsers = parser.add_subparsers(help='Desired action to perform', dest='command')
50+
51+
# help
52+
subparsers.add_parser('help', help='Print this help message')
53+
54+
# Create parent subparser for {dl_docs, check}-parsers with common arguments
55+
parent_parser = argparse.ArgumentParser(add_help=False)
56+
57+
# Subparsers based on parent
58+
59+
parser_login = subparsers.add_parser(
60+
'login',
61+
parents=[parent_parser],
62+
help='Check if credentials file exists. If not create it and ask for input. Try to login. Ask for device reset if needed',
63+
)
64+
parser_login.add_argument('-n', '--phone_no', help='TradeRepbulic phone number (international format)')
65+
parser_login.add_argument('-p', '--pin', help='TradeRepbulic pin')
66+
67+
subparsers.add_parser('portfolio', parents=[parent_parser], help='Show current portfolio')
68+
69+
parser_dl_docs = subparsers.add_parser(
70+
'dl_docs',
71+
parents=[parent_parser],
72+
help='Download all pdf documents from the timeline and sort them into folders',
73+
)
74+
parser_dl_docs.add_argument('output', help='Output directory', metavar='PATH')
75+
76+
parser_set_price_alarms = subparsers.add_parser(
77+
'set_price_alarms', parents=[parent_parser], help='Set price alarms based on diff from current price'
78+
)
79+
parser_set_price_alarms.add_argument(
80+
'-p',
81+
'--percent',
82+
help='Percentage +/-',
83+
# choices=range(-1000, 1001),
84+
metavar='[-1000 ... 1000]',
85+
type=int,
86+
default=-10,
87+
)
88+
return parser
89+
90+
91+
def exit_gracefully(signum, frame):
92+
# restore the original signal handler as otherwise evil things will happen
93+
# in input when CTRL+C is pressed, and our signal handler is not re-entrant
94+
global original_sigint
95+
signal.signal(signal.SIGINT, original_sigint)
96+
97+
try:
98+
if input("\nReally quit? (y/n)> ").lower().startswith('y'):
99+
exit(1)
100+
101+
except KeyboardInterrupt:
102+
print("Ok ok, quitting")
103+
exit(1)
104+
105+
# restore the exit gracefully handler here
106+
signal.signal(signal.SIGINT, exit_gracefully)
107+
108+
109+
def main():
110+
# store the original SIGINT handler
111+
global original_sigint
112+
original_sigint = signal.getsignal(signal.SIGINT)
113+
signal.signal(signal.SIGINT, exit_gracefully)
114+
115+
parser = get_main_parser()
116+
args = parser.parse_args()
117+
# print(vars(args))
118+
119+
log = get_logger(__name__, args.verbosity)
120+
log.setLevel(args.verbosity.upper())
121+
log.debug('logging is set to debug')
122+
123+
if args.command == "login":
124+
login(phone_no=args.phone_no, pin=args.pin)
125+
126+
elif args.command == "dl_docs":
127+
dl = DL(login(), args.output)
128+
asyncio.get_event_loop().run_until_complete(dl.dl_loop())
129+
elif args.command == "set_price_alarms":
130+
# TODO
131+
pass
132+
elif args.command == "portfolio":
133+
p = Portfolio(login())
134+
p.get()
135+
else:
136+
parser.print_help()
137+
138+
139+
if __name__ == '__main__':
140+
main()

‎pytr/portfolio.py

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import asyncio
2+
from pytr.utils import preview
3+
4+
5+
class Portfolio:
6+
def __init__(self, tr):
7+
self.tr = tr
8+
9+
async def portfolio_loop(self):
10+
recv = 0
11+
await self.tr.portfolio()
12+
await self.tr.cash()
13+
# await self.tr.available_cash_for_payout()
14+
15+
while True:
16+
_subscription_id, subscription, response = await self.tr.recv()
17+
18+
if subscription["type"] == "portfolio":
19+
recv += 1
20+
self.portfolio = response
21+
elif subscription["type"] == "cash":
22+
recv += 1
23+
self.cash = response
24+
# elif subscription["type"] == "availableCashForPayout":
25+
# recv += 1
26+
# self.payoutCash = response
27+
else:
28+
print(f"unmatched subscription of type '{subscription['type']}':\n{preview(response)}")
29+
30+
if recv == 2:
31+
return
32+
33+
def overview(self):
34+
for x in ["netValue", "unrealisedProfit", "unrealisedProfitPercent", "unrealisedCost"]:
35+
print(f"{x:24}: {self.portfolio[x]:>10.2f}")
36+
print()
37+
38+
print("ISIN avgCost * quantity = buyCost -> netValue diff %-diff")
39+
totalBuyCost = 0.0
40+
totalNetValue = 0.0
41+
positions = self.portfolio["positions"]
42+
for pos in sorted(positions, key=lambda x: x['netValue'], reverse=True):
43+
buyCost = pos["unrealisedAverageCost"] * pos["netSize"]
44+
diff = pos["netValue"] - buyCost
45+
diffP = ((pos["netValue"] / buyCost) - 1) * 100
46+
totalBuyCost += buyCost
47+
totalNetValue += pos["netValue"]
48+
49+
print(
50+
f"{pos['instrumentId']} {pos['unrealisedAverageCost']:>10.2f} * {pos['netSize']:>10.2f} = {buyCost:>10.2f} -> {pos['netValue']:>10.2f} {diff:>10.2f} {diffP:>7.1f}%"
51+
)
52+
53+
print("ISIN avgCost * quantity = buyCost -> netValue diff %-diff")
54+
print()
55+
56+
diff = totalNetValue - totalBuyCost
57+
diffP = ((totalNetValue / totalBuyCost) - 1) * 100
58+
print(f"Depot {totalBuyCost:>43.2f} -> {totalNetValue:>10.2f} {diff:>10.2f} {diffP:>7.1f}%")
59+
60+
cash = self.cash[0]['amount']
61+
currency = self.cash[0]['currencyId']
62+
print(f"Cash {currency} {cash:>40.2f} -> {cash:>10.2f}")
63+
print(f"Total {cash+totalBuyCost:>43.2f} -> {cash+totalNetValue:>10.2f}")
64+
65+
def get(self):
66+
asyncio.get_event_loop().run_until_complete(self.portfolio_loop())
67+
68+
self.overview()

‎pytr/utils.py

+165
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
#!/usr/bin/env python3
2+
3+
import logging
4+
import coloredlogs
5+
import json
6+
7+
8+
log_level = None
9+
10+
11+
def get_logger(name=__name__, verbosity=None):
12+
"""
13+
Colored logging
14+
15+
:param name: logger name (use __name__ variable)
16+
:param verbosity:
17+
:return: Logger
18+
"""
19+
global log_level
20+
if verbosity is not None:
21+
if log_level is None:
22+
log_level = verbosity
23+
else:
24+
raise RuntimeError('Verbosity has already been set.')
25+
26+
shortname = name.replace('fdroid_mirror_monitor.', '')
27+
logger = logging.getLogger(shortname)
28+
29+
# no logging of libs (and fix double logs because of fdroidserver)
30+
logger.propagate = False
31+
32+
fmt = '%(asctime)s %(threadName)-12s %(name)-7s %(levelname)-8s %(message)s'
33+
datefmt = '%Y-%m-%d %H:%M:%S%z'
34+
35+
fs = {
36+
'asctime': {'color': 'green'},
37+
'hostname': {'color': 'magenta'},
38+
'levelname': {'color': 'red', 'bold': True},
39+
'name': {'color': 'magenta'},
40+
'programname': {'color': 'cyan'},
41+
'username': {'color': 'yellow'},
42+
}
43+
44+
ls = {
45+
'critical': {'color': 'red', 'bold': True},
46+
'debug': {'color': 'green'},
47+
'error': {'color': 'red'},
48+
'info': {},
49+
'notice': {'color': 'magenta'},
50+
'spam': {'color': 'green', 'faint': True},
51+
'success': {'color': 'green', 'bold': True},
52+
'verbose': {'color': 'blue'},
53+
'warning': {'color': 'yellow'},
54+
}
55+
56+
coloredlogs.install(level=log_level, logger=logger, fmt=fmt, datefmt=datefmt, level_styles=ls, field_styles=fs)
57+
58+
return logger
59+
60+
61+
def preview(response, num_lines=5):
62+
lines = json.dumps(response, indent=2).splitlines()
63+
head = "\n".join(lines[:num_lines])
64+
tail = len(lines) - num_lines
65+
66+
if tail <= 0:
67+
return f"{head}\n"
68+
else:
69+
return f"{head}\n{tail} more lines hidden"
70+
71+
72+
class Timeline:
73+
def __init__(self, tr):
74+
self.tr = tr
75+
76+
self.received_detail = 0
77+
self.requested_detail = 0
78+
79+
async def get_next_timeline(self, response=None):
80+
"""
81+
Get timelines and save time in global list timelines.
82+
Extract id of timeline events and save them in global list timeline_detail_ids
83+
"""
84+
85+
if response is None:
86+
# empty response / first timeline
87+
print("Awaiting #1 timeline")
88+
self.timelines = []
89+
self.timeline_detail_ids = []
90+
self.timeline_events = []
91+
await self.tr.timeline()
92+
else:
93+
self.timelines.append(response)
94+
try:
95+
after = response["cursors"]["after"]
96+
except KeyError:
97+
# last timeline is reached
98+
print(f"Received #{len(self.timelines):<2} (last) timeline")
99+
await self.get_timeline_details(5)
100+
else:
101+
print(f"Received #{len(self.timelines):<2} timeline, awaiting #{len(self.timelines)+1:<2} timeline")
102+
await self.tr.timeline(after)
103+
104+
# print(json.dumps(response))
105+
for event in response["data"]:
106+
self.timeline_events.append(event)
107+
self.timeline_detail_ids.append(event["data"]["id"])
108+
109+
async def get_timeline_details(self, num_torequest):
110+
self.requested_detail += num_torequest
111+
112+
while num_torequest > 0:
113+
num_torequest -= 1
114+
try:
115+
event = self.timeline_events.pop()
116+
except IndexError:
117+
return
118+
else:
119+
await self.tr.timeline_detail(event["data"]["id"])
120+
121+
async def timelineDetail(self, response, dl):
122+
123+
self.received_detail += 1
124+
125+
if self.received_detail == self.requested_detail:
126+
await self.get_timeline_details(5)
127+
128+
timeline_detail_id = response["id"]
129+
for event in self.timeline_events:
130+
if timeline_detail_id == event["data"]["id"]:
131+
self.timeline_events.remove(event)
132+
133+
print(f"len timeline_events: {len(self.timeline_events)}")
134+
135+
print(f"R: {self.received_detail}/{len(self.timeline_detail_ids)}")
136+
137+
if response["subtitleText"] == "Sparplan":
138+
isSavingsPlan = True
139+
else:
140+
isSavingsPlan = False
141+
# some savingsPlan don't have the subtitleText == "Sparplan" but there are actions just for savingsPans
142+
for section in response["sections"]:
143+
if section["type"] == "actionButtons":
144+
for button in section["data"]:
145+
if button["action"]["type"] in ["editSavingsPlan", "deleteSavingsPlan"]:
146+
isSavingsPlan = True
147+
break
148+
149+
print(f"Detail: {response['titleText']} -- {response['subtitleText']} -- istSparplan: {isSavingsPlan}")
150+
151+
for section in response["sections"]:
152+
if section["type"] == "documents":
153+
for doc in section["documents"]:
154+
155+
# save all savingsplan documents in a subdirectory
156+
if isSavingsPlan:
157+
dl.dl_doc(doc, response['titleText'], response["subtitleText"], subfolder="Sparplan")
158+
else:
159+
dl.dl_doc(doc, response['titleText'], response["subtitleText"])
160+
161+
if self.received_detail == len(self.timeline_detail_ids):
162+
print("received all details, downloading docs..")
163+
dl.work_responses()
164+
else:
165+
print(f"r: {self.received_detail}/{len(self.timeline_detail_ids)} - istSparplan: {isSavingsPlan}")

‎requirements.txt

-3
This file was deleted.

‎setup.py

+31-19
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,40 @@
1-
import setuptools
1+
from os import path
2+
from setuptools import setup
23

3-
with open("README.md", "r") as fh:
4-
long_description = fh.read()
54

6-
setuptools.setup(
7-
name="py_tr",
8-
version="1.0.2",
9-
author="Nils Borrmann",
10-
author_email="n.borrmann@googlemail.com",
5+
def readme():
6+
this_directory = path.abspath(path.dirname(__file__))
7+
with open(path.join(this_directory, 'README.md'), encoding='utf-8') as f:
8+
return f.read()
9+
10+
11+
setup(
12+
name='pytr',
13+
version='0.0.1',
14+
description='Use TradeRepublic in terminal',
15+
long_description=readme(),
16+
long_description_content_type='text/markdown',
17+
url='https://gitlab.com/marzzzello/pytr/',
18+
author='marzzzello',
19+
author_email='853485-marzzzello@users.noreply.gitlab.com',
1120
license='MIT',
12-
description="Unoffical Python Interface for the Trade Republic API",
13-
long_description=long_description,
14-
long_description_content_type="text/markdown",
15-
url="https://github.com/nborrmann/pytr",
21+
packages=['pytr'],
22+
python_requires='>=3.5',
23+
entry_points={
24+
'console_scripts': [
25+
'pytr = pytr.main:main',
26+
],
27+
},
28+
# scripts=['traderep'],
29+
# install_requires=['py_tr'],
30+
install_requires=['coloredlogs', 'ecdsa', 'pygments', 'requests_futures', 'shtab', 'websockets'],
1631
classifiers=[
1732
"License :: OSI Approved :: MIT License",
18-
"Programming Language :: Python :: 3",
33+
'Programming Language :: Python :: 3 :: Only',
1934
"Operating System :: OS Independent",
20-
"Development Status :: 5 - Production/Stable",
35+
'Development Status :: 3 - Alpha',
2136
"Topic :: Office/Business :: Financial",
22-
"Topic :: Office/Business :: Financial :: Investment"
37+
"Topic :: Office/Business :: Financial :: Investment",
2338
],
24-
install_requires=["requests", "websockets", "ecdsa"],
25-
python_requires='>=3.5',
26-
package_dir={'': 'src'},
27-
packages=setuptools.find_packages('src'),
39+
zip_safe=False,
2840
)

‎src/py_tr/__init__.py

-1
This file was deleted.

0 commit comments

Comments
 (0)
Please sign in to comment.