På wallnot.dk udgiver jeg en liste over gratisartikler fra en lang række medier, der benytter sig af betalingsmure/paywall. Siden er ment som en service til brugere, der ved, at de gerne vil læse nyhedsartikler, og at de ikke vil betale for dem.
Jeg syntes det var ærgerligt ikke at have Zetland med på Wallnot, så i stedet for at kigge efter links på forsiden, som Wallnot gør hos de andre medier, brugte jeg Twitters API til at trække artikellinks ud.
# -*- coding: utf-8 -*-
# Author: Morten Helmstedt. E-mail: helmstedt@gmail.com
""" This program uses the Twitter API to get a list of free articles from Zetland """
import requests
from bs4 import BeautifulSoup
from datetime import datetime
from datetime import date
import json
from nested_lookup import nested_lookup
import base64
# GETS TWITTER DATA #
# Key and secret from Twitter developer account: https://developer.twitter.com/en/apply/user
client_key = ''
client_secret = ''
# Key and secret encoding, preparing for Twitter request
key_secret = '{}:{}'.format(client_key, client_secret).encode('ascii')
b64_encoded_key = base64.b64encode(key_secret)
b64_encoded_key = b64_encoded_key.decode('ascii')
base_url = 'https://api.twitter.com/'
auth_url = '{}oauth2/token'.format(base_url)
auth_headers = {
'Authorization': 'Basic {}'.format(b64_encoded_key),
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
}
auth_data = {
'grant_type': 'client_credentials'
}
auth_resp = requests.post(auth_url, headers=auth_headers, data=auth_data)
auth_resp.json().keys()
access_token = auth_resp.json()['access_token']
search_headers = {
'Authorization': 'Bearer {}'.format(access_token)
}
# Search parameters for Zetland tweets
search_params = {
'user_id': 452898921,
'count': 35,
'tweet_mode': 'extended',
'exclude_replies': 'true',
'trim_user': 'true'
}
# Request url for searching user timelines
search_url = '{}1.1/statuses/user_timeline.json'.format(base_url)
# Request to Twitter
search_resp = requests.get(search_url, headers=search_headers, params=search_params)
# Response from Twitter in json format
tweet_data = search_resp.json()
#prettyjson = json.dumps(tweet_data, ensure_ascii=False, indent=4) # Only needed for debugging to pretify json
# Looks for all instances of expanded_url (that is, links) in json
linklist = list(set(nested_lookup('expanded_url', tweet_data)))
# Populates a list of links to Zetland articles
urllist = []
for link in linklist:
if "zetland.dk/historie" in link:
urllist.append(link)
# GETS ARTICLE DATA FROM ZETLAND #
# Requests articles and get titles and dates and sort by dates directly from Zetland site
articlelist = []
titlecheck = []
for url in urllist:
try:
data = requests.get(url)
result = data.text
# Soup site and create a dictionary of links and their titles and dates
articledict = {}
soup = BeautifulSoup(result, "lxml")
title = soup.find('meta', attrs={'property':'og:title'})
title = title['content']
timestamp = soup.find('meta', attrs={'property':'article:published_time'})
timestamp = timestamp['content']
timestamp = timestamp[:timestamp.find("+")]
dateofarticle = datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%S.%f')
if title not in titlecheck:
articlelist.append({"title": title, "url": url, "date": dateofarticle})
titlecheck.append(title)
except:
print(url)
# PREPARES LIST OF ARTICLES FOR WALLNOT.DK #
# Sort articles by date (newest first)
articlelist_sorted = sorted(articlelist, key=lambda k: k['date'], reverse=True)
# Removes articles older than approximately three months
articlelist_recent = []
now = datetime.now()
for article in articlelist_sorted:
timesincelast = now - article["date"]
if timesincelast.days < 92:
articlelist_recent.append(article)
# Converts dates to friendly format for display and outputs articles as html paragraphs
zet_linkstr = ""
for article in articlelist_recent:
friendlydate = article["date"].strftime("%d/%m")
zet_linkstr += '<p>' + friendlydate + ': ' + '<a href="' + article["url"] + '">' + article["title"] + '</a></p>\n'
# Prints list of articles
print(zet_linkstr)
Opdatering 18/02/2023: Nordnet ændrer tit på deres ting. På https://github.com/helmstedt/nordnet-utilities forsøger jeg at følge med, så hent gerne din kode der, hvis koden neden for ikke virker længere.
OPDATERING: Nordnet er ude i en ny version, som gør at man foreløbig er nødt til at ændre URLs i programmet til “classic.nordnet.dk” for at bruge den gamle version. På et tidspunkt virker det nok heller ikke længere. Jeg har opdateret koden neden for med den korrekte url.
I dag handler det om at få fat i kurser på værdipapirer, sådan man kan lave flotte grafer i Excel over den historiske udvikling.
Sådan kan en oversigt se ud i Excel. Der er små “huller” i kurserne for enkelte papirer. Jeg har ikke undersøgt nærmere hvorfor der ikke er kurser for de datoer.
Du behøver ikke at være kunde hos Nordnet og du kan – så vidt jeg kan se – udtrække helt aktuelle realtidskurser uden forsinkelse.
Sådan gjorde jeg
Jeg startede med at besøge et værdipapir på Nordnet. Her lagde jeg mærke til den fine graf over kursudviklingen. Sådan en graf må få data et sted fra. Jeg trykkede F12 for at åbne Developer Tools og kiggede på fanen Network. Når jeg ændrede periode for grafen, kunne jeg se, at der blev sendt en ny forespørgsel af sted:
En fin kursgraf fra Nordnet. Når jeg ændrer periode, kan jeg se, at Nordnet spørger om data. På billedet kan man se, at data bliver returneret i JSON-format med forskellige kursoplysninger og et tidsstempel.
Under fanen Headers kan jeg se oplysninger om, hvad for en URL, min browser sender forespørgsler til, og hvad den spørger om:
ForespørgslenParametre
I Python bygger jeg et program, der kan løbe en liste værdipapirer igennem. Jeg opdager et enkelt værdipapir, som ikke findes hos Nordnet. Derfor bygger jeg også et slags fallback, der kan hente kursdata fra Morningstar (med 15 minutters forsinkelse).
Det færdige program i Python
Her er så det færdige program i Python. Du er velkommen til at bruge det, videreudvikle, og hvad du ellers har lyst til.
# -*- coding: utf-8 -*-
# Author: Morten Helmstedt. E-mail: helmstedt@gmail.com
""" This program extracts historical stock prices from Nordnet (and Morningstar as a fallback) """
import requests
from datetime import datetime
from datetime import date
import os
import json
# DATE AND STOCK DATA. SHOULD BE EDITED FOR YOUR NEEDS #
# Start date (start of historical price period) and date today used as standard enddate for price period
startdate = '2013-01-01'
today = date.today()
enddate = str(today)
# List of shares to look up prices for.
# Format is: Name, Morningstar id, Nordnet stock identifier, Nordnet market number
# See e.g. https://www.nordnet.dk/mux/web/marknaden/aktiehemsidan/index.html?identifier=4804&marketid=14
# (identifier is 4804, market is 14)
# All shares must have a name (whatever you like). To get prices they must either have a Nordnet identifier
# and market number or a Morningstar id
sharelist = [
["Maj Invest Globale Obligationer","F0GBR064US",36432,14],
["Novo Nordisk B A/S","0P0000A5BQ",1158,14],
["BlackRock iShares Core S&P 500 UCITS ETF","0P0000OO21]22]1]","SXR8",4],
["Nordnet Superfonden Danmark","F00000TH8X","",""],
["Danske Invest Global Indeks Akk KL DKK h","F0GBR04EPX",38898,14]
]
# CREATE OUTPUT FOLDER AND VARIABLES FOR LATER USE. #
# Checking that we have an output folder to save our csv file
if not os.path.exists("./output"):
os.makedirs("./output")
# A variable to store historical prices before saving to csv
finalresult = ""
finalresult += '"date";"price";"instrument"' + '\n'
# LOOPS TO REQUEST HISTORICAL PRICES AT NORDNET AND MORNINGSTAR #
# Nordnet loop to get historical prices
for share in sharelist:
# Nordnet stock identifier and market number must both exist
if share[2] and share[3]:
url = "https://classic.nordnet.dk/graph/instrument/" + str(share[3]) + "/" + str(share[2])
payload = {"from": startdate, "to": enddate, "fields": "last"}
data = requests.get(url, params=payload)
result = data.text
jsondecode = json.loads(result)
# Sometimes the final date is returned twice. A list is created to check for duplicates.
datelist = []
for value in jsondecode:
price = str(value['last'])
price = price.replace(".",",")
date = datetime.fromtimestamp(value['time'] / 1000)
date = datetime.strftime(date, '%Y-%m-%d')
# Only adds a date if it has not been added before
if date not in datelist:
datelist.append(date)
finalresult += '"' + date + '"' + ";" + '"' + price + '"' + ";" + '"' + share[0] + '"' + "\n"
# Morningstar loop to get historical prices
for share in sharelist:
# Only runs for one specific fund in this instance
if share[0] == "Nordnet Superfonden Danmark":
payload = {"id": share[1], "currencyId": "DKK", "idtype": "Morningstar", "frequency": "daily", "startDate": startdate, "outputType": "COMPACTJSON"}
data = requests.get("http://tools.morningstar.dk/api/rest.svc/timeseries_price/nen6ere626", params=payload)
result = data.text
jsondecode = json.loads(result)
for lists in jsondecode:
price = str(lists[1])
price = price.replace(".",",")
date = datetime.fromtimestamp(lists[0] / 1000)
date = datetime.strftime(date, '%Y-%m-%d')
finalresult += '"' + date + '"' + ";" + '"' + price + '"' + ";" + '"' + share[0] + '"' + "\n"
# WRITE CSV OUTPUT TO FILE #
with open("./output/kurser1.csv", "w", newline='', encoding='utf8') as fout:
fout.write(finalresult)
Opdatering 18/02/2023: Nordnet ændrer tit på deres ting. På https://github.com/helmstedt/nordnet-utilities forsøger jeg at følge med, så hent gerne din kode der, hvis koden neden for ikke virker længere.
OPDATERING: Nordnet er ude i en ny version, som gør at man foreløbig er nødt til at ændre URLs i programmet til “classic.nordnet.dk” for at bruge den gamle version. På et tidspunkt virker det nok heller ikke længere. Jeg har opdateret koden neden for med den korrekte url.
Jeg kan godt lide at bruge Excel til at holde øje med min økonomi, så jeg har et ark med en pivottabel, som jeg bruger til at få overblik over min portefølje hos Nordnet. Nordnet har en funktion til at trække en CSV-fil med en oversigt over mine transaktioner, men det er lidt besværligt at skulle a) logge ind på Nordnet for derefter b) at gå ind på hver enkelt depot og c) trække en ny oversigt og copy/paste hver gang, der fx udbetales udbytte.
Her klikker du hos Nordnet for at generere en CSV-fil, du kan bruge til Excel
Derfor tænkte jeg: Kan jeg automatisere dette udtræk, sådan jeg altid har opdateret data i mit Excelark? Ja, det kan jeg. Med Python. Her fortæller jeg om hvordan og deler min kode.
Hvordan snakker man http med Nordnet?
Det første jeg gjorde, var at undersøge hvad der egentlig sker, når jeg beder Nordnet om en transaktionsfil. Det gør jeg i Chrome ved at trykke F12, vælge Network og undersøge hvad min browser sender af sted for at få en CSV-fil tilbage. Jeg kan se, at der ryger en cookie af sted og nogle parametre, der handler om bl.a. sortering og periode for de transaktioner, jeg vil have ud:
I Python bruger jeg modulet Requests til at snakke med Nordnet og forsøger at konstruere noget der ligner det, min browser smider af sted. Efter at have prøvet mig frem, finder jeg ud af, at det er den cookie, der hedder NOW, der er afgørende for, at modtage noget fra Nordnet. Jeg laver en cookie-ordbog, der foreløbig indeholder min NOW-værdi fra Chrome:
cookies = {'NOW': '63261fc324153bd1632006105c5b4444d97fc72a'}
Min forespørgsel, der giver mig data tilbage, ser sådan ud:
# LOGIN TO NORDNET #
# First part of cookie setting prior to login
url = 'https://www.nordnet.dk/mux/login/start.html?cmpi=start-loggain&state=signin'
r = requests.get(url)
cookies['LOL'] = r.cookies['LOL']
cookies['TUX-COOKIE'] = r.cookies['TUX-COOKIE']
# Second part of cookie setting prior to login
url = 'https://www.nordnet.dk/api/2/login/anonymous'
r = requests.post(url, cookies=cookies)
cookies['NOW'] = r.cookies['NOW']
# Actual login that gets us cookies required for primary account extraction
url = "https://www.nordnet.dk/api/2/authentication/basic/login"
r = requests.post(url,cookies=cookies, data = {'username': user, 'password': password})
cookies['NOW'] = r.cookies['NOW']
cookies['xsrf'] = r.cookies['xsrf']
Når denne procedure er gennemført, kan jeg med den genererede NOW-værdi trække transaktioner ud af mit primære depot (det jeg oprettede først, da jeg fik en Nordnet-konto).
For at trække transaktioner ud fra andre depoter, undersøger jeg hvad der sker, når jeg vælger et andet depot i Nordnet. Det sker ved at forespørge https://www.nordnet.dk/mux/ajax/session/bytdepa.html med mine gemte cookies og værdien fra den cookie, der hedder xrsf i headeren. Tilbage får jeg en ny NOW-værdi, som jeg kan bruge til at hente transaktioner på det andet depot, og en ny xrsf-værdi, som jeg kan bruge, hvis jeg har endnu flere depoter, jeg får brug for at skifte til:
# Switch to secondary account and set new cookies
url = 'https://www.nordnet.dk/mux/ajax/session/bytdepa.html'
headers = {'X-XSRF-TOKEN': cookies['xsrf']}
r = requests.post(url,cookies=cookies, headers=headers, data = {'portfolio': item['id']})
cookies['NOW'] = r.cookies['NOW']
cookies['xsrf'] = r.cookies['xsrf']
Til sidst skal jeg finde en fornuftig struktur for mit program, finde ud af hvordan jeg får lavet en god struktur i min CSV-fil (nogle gange returnerer Nordnet en CSV-fil med en kolonne for meget). Og så har jeg brug for en mulighed for at tilføje manuelle linjer til min CSV-fil (fordi jeg gerne vil have historisk data med fra et gammelt depot hos en anden bank).
Det færdige program i Python
Her er det færdige program. Du er velkommen til at bruge det, videreudvikle, osv.
<pre class="wp-block-syntaxhighlighter-code"><p># -*- coding: utf-8 -*-
# Author: Morten Helmstedt. E-mail: helmstedt@gmail.com
""" This program logs into a Nordnet account and extracts transactions as a csv file.
Handy for exporting to Excel with as few manual steps as possible """
import requests
from datetime import datetime
from datetime import date
import os
# USER ACCOUNT, PORTFOLIO AND PERIOD DATA. SHOULD BE EDITED FOR YOUR NEEDS #
# Nordnet user account credentials and name of primary portfolio (first one listed in Nordnet)
user = ''
password = ''
primaryportfolioname = "Frie midler"
# Names and portfolio ids for all any all secondary portfolios. The id is listed in
# Nordnet when selecting a portfolio. If no secondary portfolios the variable
# secondaryportfolioexists should be set to False.
secondaryportfolioexists = True
secondaryportfolios = [
{'name': 'Ratepension', 'id': ''},
]
# Start date (start of period for transactions) and date today used for extraction of transactions
startdate = '01.01.2013'
today = date.today()
enddate = datetime.strftime(today, '%d.%m.%Y')
# Manual date lines. These can be used if you have portfolios elsewhere that you would
# like to add manually to the data set. If no manual data the variable manualdataexists
# should be set to False
manualdataexists = True
manualdata = """
Id;Bogføringsdag;Handelsdag;Valørdag;Transaktionstype;Værdipapirer;Instrumenttyp;ISIN;Antal;Kurs;Rente;Afgifter;Beløb;Valuta;Indkøbsværdi;Resultat;Totalt antal;Saldo;Vekslingskurs;Transaktionstekst;Makuleringsdato;Verifikations-/Notanummer;Depot
;30-09-2013;30-09-2013;30-09-2013;KØBT;Obligationer 3,5%;Obligationer;;72000;;;;-69.891,54;DKK;;;;;;;;;Frie midler
"""
# CREATE OUTPUT FOLDER AND VARIABLES FOR LATER USE. #
# Checking that we have an output folder to save our csv file
if not os.path.exists("./output"):
os.makedirs("./output")
# Creates a dictionary to use with cookies
cookies = {}
# A variable to store transactions before saving to csv
transactions = ""
# Payload for transaction requests
payload = {
'year': 'all',
'month': 'all',
'trtyp': 'all',
'vp': 'all',
'curr': 'all',
'sorteringsordning': 'fallande',
'sortera': 'datum',
'startperiod': startdate,
'endperiod': enddate
}
# LOGIN TO NORDNET #
# First part of cookie setting prior to login
url = 'https://classic.nordnet.dk/mux/login/start.html?cmpi=start-loggain&state=signin'
r = requests.get(url)
cookies['LOL'] = r.cookies['LOL']
cookies['TUX-COOKIE'] = r.cookies['TUX-COOKIE']
# Second part of cookie setting prior to login
url = 'https://classic.nordnet.dk/api/2/login/anonymous'
r = requests.post(url, cookies=cookies)
cookies['NOW'] = r.cookies['NOW']
# Actual login that gets us cookies required for primary account extraction
url = "https://classic.nordnet.dk/api/2/authentication/basic/login"
r = requests.post(url,cookies=cookies, data = {'username': user, 'password': password})
cookies['NOW'] = r.cookies['NOW']
cookies['xsrf'] = r.cookies['xsrf']
# GET PRIMARY ACCOUNT TRANSACTION DATA #
# Get CSV for primary account
url = "https://classic.nordnet.dk/mux/laddaner/transaktionsfil.html"
data = requests.get(url, params=payload, cookies=cookies)
result = data.text
result = result.splitlines()
firstline = 0
for line in result:
if line and firstline == 0:
transactions += line + ';' + "Depot" + "\n"
firstline = 1
elif line:
# Sometimes Nordnet inserts one semicolon too many in the file. This removes the additional semicolon
if line.count(';') == 22:
position = line.rfind(';')
line = line [:position] + line[position+1:]
transactions += line + ';' + primaryportfolioname + "\n"
# GET TRANSACTION DATA FOR ALL/ANY SECONDARY ACCOUNTS #
if secondaryportfolioexists == True:
for item in secondaryportfolios:
# Switch to secondary account and set new cookies
url = 'https://classic.nordnet.dk/mux/ajax/session/bytdepa.html'
headers = {'X-XSRF-TOKEN': cookies['xsrf']}
r = requests.post(url,cookies=cookies, headers=headers, data = {'portfolio': item['id']})
cookies['NOW'] = r.cookies['NOW']
cookies['xsrf'] = r.cookies['xsrf']
# Get CSV for secondary account
url = "https://classic.nordnet.dk/mux/laddaner/transaktionsfil.html"
data = requests.get(url, params=payload, cookies=cookies)
result = data.text
result = result.split("\n",1)[1]
result = result.splitlines()
for line in result:
if line:
# Sometimes Nordnet inserts one semicolon too many in the file. This removes the additional semicolon
if line.count(';') == 22:
position = line.rfind(';')
line = line [:position] + line[position+1:]
transactions += line + ';' + item['name'] + "\n"
if manualdataexists == True:
manualdata = manualdata.split("\n",2)[2]
transactions += manualdata
# WRITE CSV OUTPUT TO FILE #
with open("./output/trans.csv", "w", encoding='utf8') as fout:
fout.write(transactions)</p></pre>