mirror of
https://github.com/cmallwitz/Financials-Extension.git
synced 2026-08-24 10:04:10 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ec1908948 | ||
|
|
03770b4a2e | ||
|
|
fef8d3e442 | ||
|
|
af69d7f9fd | ||
|
|
b5b1ca5d09 | ||
|
|
e50f084886 | ||
|
|
d3e2937286 | ||
|
|
a0f723cfba | ||
|
|
376e0999c9 | ||
|
|
7b451bf872 | ||
|
|
28ae9efa74 | ||
|
|
090e604d1d | ||
|
|
a4432a85e6 | ||
|
|
a593336b08 | ||
|
|
06d9b91793 | ||
|
|
60d5d7b764 | ||
|
|
345f57420c | ||
|
|
88b039d471 | ||
|
|
7e751928de | ||
|
|
0c07c4c3ba | ||
|
|
3f6249a6b6 | ||
|
|
d05ca9bb55 | ||
|
|
023fa71a81 | ||
|
|
a89edcc5fc | ||
|
|
c71825143e | ||
|
|
587b391393 | ||
|
|
605e9ef185 | ||
|
|
e0491318c3 | ||
|
|
24b1406772 | ||
|
|
1d9c4e824f | ||
|
|
c3a6d8d69a | ||
|
|
d0fc0ae2bf | ||
|
|
e4d395240d | ||
|
|
c3dc5c35fc | ||
|
|
b356a1bca4 | ||
|
|
befd0956f0 | ||
|
|
9e842b30de | ||
|
|
6d0f56c87d | ||
|
|
cd41959559 | ||
|
|
52e7119081 | ||
|
|
520d2441cb | ||
|
|
8b370dfc73 | ||
|
|
af82f16740 | ||
|
|
4e626c272e | ||
|
|
e22dc7448f | ||
|
|
ab34a246e3 | ||
|
|
aa20845ae4 | ||
|
|
7fccc7c0ad | ||
|
|
7ddfbd084f | ||
|
|
e22d4f46b8 | ||
|
|
6a3ab22801 | ||
|
|
46e6a68f6c | ||
|
|
ddaa4dc0aa | ||
|
|
9c8a80b1e3 | ||
|
|
e99a1109df | ||
|
|
372c878bad | ||
|
|
c3308ee9ec | ||
|
|
85bc40f6bd | ||
|
|
601b91c8c3 | ||
|
|
7b806ce527 | ||
|
|
240cfea126 | ||
|
|
99c660f1ad | ||
|
|
19215e4e98 | ||
|
|
0fc9d4335f | ||
|
|
625a18eb02 | ||
|
|
1c10237b60 | ||
|
|
8933347508 | ||
|
|
7aadaad7dc | ||
|
|
01538a13a8 | ||
|
|
9b464e22b1 | ||
|
|
d7a7e4f791 | ||
|
|
12ecbfbbf8 | ||
|
|
5783736d23 | ||
|
|
bf7f3f3c1e | ||
|
|
01ec715296 | ||
|
|
423c8a9f99 | ||
|
|
111e46d375 | ||
|
|
e21e3c51ab | ||
|
|
e113b5f2f2 | ||
|
|
9eee2fb96c | ||
|
|
39d4424ce4 | ||
|
|
da3733f285 | ||
|
|
70ef0ad4ec | ||
|
|
901ff46906 |
+2
-1
@@ -1,4 +1,5 @@
|
||||
.idea
|
||||
src/__pycache__
|
||||
build/*
|
||||
Financials-Extension.oxt
|
||||
Financials-Extension.oxt
|
||||
_trial_temp
|
||||
|
||||
Binary file not shown.
@@ -1,16 +1,202 @@
|
||||
# Financials-Extension
|
||||
|
||||
Extension for LibreOffice Calc to access stock market data. Currently supports Yahoo and Google.
|
||||
## Overview
|
||||
|
||||
Only tested this with Ubuntu 16.04 and LibreOffice 5
|
||||
This is a Python based extension for LibreOffice Calc to make market data available in Calc
|
||||
spreadsheets - currently supporting Yahoo's (FX, crypto, equities, indices, futures, options) and Financial Times'
|
||||
(FX, equities, indices, futures) websites using old-fashioned web scraping.
|
||||
|
||||
To Build:
|
||||
## Latest version vs Yahoo HTTPS fingerprinting
|
||||
|
||||
cd ~/tech/IdeaProjects/Financials-Extension/
|
||||
Latest version 3.8.0 was created to bypass Yahoo's recently adding crazy HTTPS fingerprinting
|
||||
to their website. In a step back to before or rather a return to times long gone some Python
|
||||
modules need to be installed such that LibreOffice can find them - otherwise Yahoo will not work.
|
||||
|
||||
python3 src/test_yahoo.py
|
||||
Update for version: 3.8.2 - this bundles the Python module 'requests' and dependencies so users only
|
||||
using 'FT' as source should not require anything else.
|
||||
|
||||
python3 src/test_google.py
|
||||
Everyone else using 'Yahoo' as source needs to install module 'curl_cffi'.
|
||||
|
||||
### Ubuntu / Linux Mint / etc.
|
||||
|
||||
Install Python curl_cffi module as root (such that LibroOffice can find it)
|
||||
|
||||
- Optionally, if you don't have pip3 installed: ```sudo apt install python3-pip```
|
||||
|
||||
- Then ```sudo pip3 install curl_cffi --upgrade```
|
||||
|
||||
Note: For a normal Python script just installing curl_cffi is enough to bypass Yahoo's HTTPS fingerprinting.
|
||||
Because LibreOffice on Linux is loading the stock curl library long before executing the extension
|
||||
code directly, a second step are required.
|
||||
|
||||
The second bit requires a download of [curl-impersonate](https://github.com/lwthiker/curl-impersonate/releases) e.g.
|
||||
(currently) libcurl-impersonate-v0.6.1.x86_64-linux-gnu.tar.gz - unpack it somewhere
|
||||
|
||||
Then I used the below (adjust your location of libcurl-impersonate-chrome.so) to run LibreOffice Calc
|
||||
directly from command line - alternatively you could define/export LD_PRELOAD and CURL_IMPERSONATE
|
||||
e.g. in /etc/environment or ~/.bashrc - but make sure the variables are really set when you run LibreOffice.
|
||||
Note: the setting chrome101 is just that - a setting on what browser to "impersonate". You don't
|
||||
need to use or install Chrome for this.
|
||||
|
||||
```
|
||||
LD_PRELOAD=/tmp/curl-impersonate/libcurl-impersonate-chrome.so CURL_IMPERSONATE=chrome101 /usr/lib/libreoffice/program/soffice.bin --calc
|
||||
```
|
||||
|
||||
In LibreOffice Calc this I can see something like the below in the output
|
||||
from `=GETREALTIME("SUPPORT")` and the examples.ods file from this repo can load data
|
||||
for Yahoo again.
|
||||
|
||||
```
|
||||
...
|
||||
requests=curl_cffi_0.10.0
|
||||
LD_PRELOAD=/tmp/curl-impersonate/libcurl-impersonate-chrome.so
|
||||
CURL_IMPERSONATE=chrome101
|
||||
curl_version="libcurl/8.1.1 BoringSSL zlib/1.2.11 brotli/1.0.9 nghttp2/1.56.0"
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
- Download the script https://bootstrap.pypa.io/get-pip.py to your computer
|
||||
|
||||
- Start a Command Prompt (CMD) as Administrator on the command prompt run (change path as required)
|
||||
|
||||
```"c:\Program Files\LibreOffice\program\python.exe" c:\temp\get-pip.py``` and then
|
||||
|
||||
```"c:\Program Files\LibreOffice\program\python.exe" -m pip install curl_cffi --upgrade```
|
||||
|
||||
- Add a new user environment variable CURL_IMPERSONATE, setting it to value chrome101, the Linux
|
||||
LD_PRELOAD is not needed for the LibreOffice 7.1 I tested this with.
|
||||
|
||||
## Usage of extension:
|
||||
|
||||
Under 'Releases' on GitHub is a [downloadable](https://github.com/cmallwitz/Financials-Extension/releases) **Financials-Extension.oxt** file - load it into Calc
|
||||
under menu item: Tools, Extension Manager...
|
||||
|
||||
Please make sure, not to rename the OXT file when downloading and before installing: LO will mess up the installation otherwise and the extension won't work.
|
||||
|
||||
Getting data should be as simple as having this in a cell:
|
||||
- `=GETREALTIME("IBM",21,"YAHOO")`
|
||||
- `=GETREALTIME("IBM:NYQ",21,"FT")`
|
||||
- `=GETREALTIME("EURUSD","LAST_PRICE","FT")`
|
||||
- `=GETHISTORIC("IBM",90,"2020-12-01","YAHOO")`
|
||||
- `=GETREALTIME("ETH-USD","LAST_PRICE","COINBASE")`
|
||||
|
||||
Codes 21 and 90 stand for "last price" and "close" (see below), respectively.
|
||||
Only Yahoo has historic data available.
|
||||
|
||||
There is a file **examples.ods** in the same Release area with usage examples
|
||||
and possible arguments to functions.
|
||||
|
||||
You have to check the respective websites to work out what symbol is the right one for you. Make sure today or the date
|
||||
requested is a trading day (exchange is not closed). If a website doesn't have
|
||||
the symbol/asset you want, this extension can't help you either. Having said that, I mostly look at US and West European
|
||||
equities, ETFs and mutual funds and major FX rates - if you have issues with the data available for other assets or
|
||||
assets in other regions, drop me a line (best to include full URLs and possibly the same asset listed on more than one
|
||||
site for comparison). While data for last price is most likely consistent across sites, they may differ for other data
|
||||
points.
|
||||
|
||||
List of example URLs for checking symbols. You can start from these and search for other symbols. If you think some data
|
||||
is available on the website but not from the extension, a good place to start when raising an issue is to include a similar
|
||||
URL to compare results.
|
||||
|
||||
| Website | Symbol | Example URL for Vodafone Group Plc UK |
|
||||
|:---------|:--------|:-----------------------------------------------------------------|
|
||||
| YAHOO | VOD.L | https://finance.yahoo.com/quote/VOD.L |
|
||||
| FT | VOD:LSE | https://markets.ft.com/data/equities/tearsheet/summary?s=VOD:LSE |
|
||||
| COINBASE | ETH-EUR | https://api.exchange.coinbase.com/products/ETH-EUR/stats |
|
||||
|
||||
### LibreOffice: using , (comma) vs ; (semicolon) to separate arguments in formula
|
||||
|
||||
There is a setting in "Tools" / "Options..." / "LibreOffice Calc" / "Formula" called "Functions". Here the user can specify the character used to separate arguments in formula.
|
||||
|
||||
Mine is set to , (comma) - when I enter ; (semicolon) in a formula (Ubuntu / UK English), no error is reported but the semicolon is converted to , (comma)
|
||||
|
||||
Depending on your system's language and default LibreOffice settings, you maybe better off using ; instead of , in your formulas.
|
||||
|
||||
### List of supported data points
|
||||
|
||||
You can either specify numbers or names (lower or upper case) - not all bits are available from all sources, and they are not necessarily consistent across sources either.
|
||||
|
||||
| Name | Code | YAHOO | FT | YAHOO (historic) | COINBASE | Notes |
|
||||
|:---------------------|:-----|:----------------:|:---:|:----------------:|----------|:----------:|
|
||||
| PREV_CLOSE | 5 | Yes | Yes |||
|
||||
| OPEN | 6 | Yes | Yes | Yes | Yes ||
|
||||
| CHANGE | 7 | Yes | Yes |||
|
||||
| LAST_PRICE_DATE | 8 | | Yes | | | yyyy-mm-dd |
|
||||
| LAST_PRICE_TIME | 10 | | Yes |||
|
||||
| CHANGE_IN_PERCENT | 11 | Yes | Yes |||
|
||||
| LOW | 14 | Yes | Yes | Yes | Yes ||
|
||||
| HIGH | 16 | Yes | Yes | Yes | Yes ||
|
||||
| LAST_PRICE | 21 | Yes | Yes | | Yes |
|
||||
| BID | 22 | Yes | Yes |||
|
||||
| ASK | 25 | Yes | Yes |||
|
||||
| HIGH_52_WEEK | 24 | Yes | Yes |||
|
||||
| LOW_52_WEEK | 26 | Yes | Yes |||
|
||||
| MARKET_CAP | 27 | Yes | Yes |||
|
||||
| BIDSIZE | 30 | Yes | Yes |||
|
||||
| ASKSIZE | 31 | Yes | Yes |||
|
||||
| VOLUME | 35 | Yes | Yes | Yes | Yes ||
|
||||
| AVG_DAILY_VOL_3MONTH | 39 | Yes | Yes |||
|
||||
| BETA | 67 | Yes | Yes |||
|
||||
| EPS | 68 | Yes | Yes |||
|
||||
| PE_RATIO | 69 | Yes | Yes |||
|
||||
| DIV | 70 | Yes | Yes |||
|
||||
| DIV_YIELD | 71 | Yes | Yes |||
|
||||
| EX_DIV_DATE | 72 | Yes | Yes | | | yyyy-mm-dd |
|
||||
| PAYOUT_RATIO | 73 | Yes | No |||
|
||||
| EXPIRY_DATE | 74 | Yes (on options) | No | | | yyyy-mm-dd |
|
||||
| SHARES_OUT | 75 | Yes | Yes | | ||
|
||||
| FREE_FLOAT | 76 | Yes | Yes | | ||
|
||||
| SETTLEMENT_DATE | 77 | Yes (on futures) | | | | yyyy-mm-dd |
|
||||
| CLOSE | 90 | No | No | Yes | ||
|
||||
| ADJ_CLOSE | 91 | No | No | Yes | ||
|
||||
| SECTOR | 98 | Yes | Yes |||
|
||||
| INDUSTRY | 99 | Yes | Yes |||
|
||||
| TICKER | 101 | Yes | Yes | | Yes |
|
||||
| EXCHANGE | 102 | Yes | No |||
|
||||
| CURRENCY | 103 | Yes | Yes | | Yes |
|
||||
| NAME | 104 | Yes | Yes |||
|
||||
| TIMEZONE | 105 | Yes | Yes |||
|
||||
|
||||
### Dealing with missing data:
|
||||
|
||||
A hint for using LibreCalc: if you want to refresh data you can press SHIFT-CTRL-F9 - this will force a
|
||||
recalculation of all formulas in all sheets.
|
||||
|
||||
Secondly the extension saves some debug information under your user directory in a directory ".financials-extension":
|
||||
the HTML for each stock symbol is saved in a separate file (depending on the source and ticker symbol). You can open it
|
||||
your favorite web browser (or other tools) to check if the page actually contained the information you are looking for.
|
||||
If it does, the file trace.log has a record of all calls to the extension with the value returned to LibreOffice.
|
||||
Otherwise, the file extension.log in the same location might have more details about errors or exceptions.
|
||||
|
||||
In general, web scraping can't be compared to using a stable API - the websites might have issues - from a technical or
|
||||
data perspective. I have found especially on the weekend it can sometimes be "flaky" and closing/reopening LibreCalc can
|
||||
refresh things.
|
||||
|
||||
### Build:
|
||||
|
||||
I only ever tried building on a Linux box.
|
||||
|
||||
You need to install LibreOffice SDK packages: libreoffice-dev libreoffice-java-common libreoffice-script-provider-python
|
||||
Since the Yahoo HTTPS fingerprinting issue, additionally curl_cffi needs to be installed (see beginning of README)
|
||||
|
||||
\# depending on your location...
|
||||
|
||||
cd ~/tech/Financials-Extension/
|
||||
|
||||
\# Assuming curl-cffi is installed, LD_PRELOAD is not required here
|
||||
|
||||
CURL_IMPERSONATE=chrome101 python3 -m unittest discover src
|
||||
|
||||
\# This builds file **Financials-Extension.oxt**
|
||||
|
||||
./compile.sh
|
||||
|
||||
### Tested with:
|
||||
- Ubuntu 22.04.5 / LibreOffice Calc 7.3.7.2 / Python 3.10.12
|
||||
|
||||
(Previously)
|
||||
- Windows 10 / LibreOffice Calc 7.1.2.2 / Python 3.8.8
|
||||
- MacOS 10.15.7 / LibreOffice Calc 7.2.0.4 / Python 3.8.10
|
||||
- Debian 10.3 / LibreOffice Calc 6.1.5.2 / Python 3.7.3
|
||||
- Ubuntu 20.04.5 / LibreOffice Calc 6.4.7.2 / Python 3.8.10
|
||||
|
||||
Binary file not shown.
+86
-11
@@ -1,7 +1,24 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
export PATH=$PATH:/usr/lib/libreoffice/sdk/bin
|
||||
export PATH=$PATH:/usr/lib/libreoffice/program
|
||||
set -o errexit
|
||||
set -o nounset
|
||||
set -o pipefail
|
||||
set -o noclobber
|
||||
|
||||
if [[ $OSTYPE == "darwin"* ]]; then
|
||||
echo MacOS
|
||||
# Assuming both are installed in the applications folder
|
||||
# Required some steps to make it work for MacOS M1, mind the `find` call which could return more than one (shouldn't)
|
||||
# install_name_tool -change @__VIA_LIBRARY_PATH__/libreglo.dylib $(find /Applications -name "libreglo.dylib") /Applications/LibreOffice7.4_SDK/bin/idlc
|
||||
# install_name_tool -change @__VIA_LIBRARY_PATH__/libuno_sal.dylib.3 $(find /Applications -name "libuno_sal.dylib.3") /Applications/LibreOffice7.4_SDK/bin/idlc
|
||||
# install_name_tool -change @__VIA_LIBRARY_PATH__/libuno_salhelpergcc3.dylib.3 $(find /Applications -name "libuno_salhelpergcc3.dylib.3") /Applications/LibreOffice7.4_SDK/bin/idlc
|
||||
# codesign --force -s - $(find /Applications -name "idlc")
|
||||
export PATH=$PATH:/Applications/LibreOffice7.4_SDK/bin
|
||||
export PATH=$PATH:/Applications/LibreOffice.app/Contents/MacOS
|
||||
else
|
||||
export PATH=$PATH:/usr/lib/libreoffice/sdk/bin
|
||||
export PATH=$PATH:/usr/lib/libreoffice/program
|
||||
fi
|
||||
|
||||
# Setup build directories
|
||||
|
||||
@@ -12,13 +29,23 @@ mkdir "${PWD}"/build/META-INF/
|
||||
|
||||
# Compile the binaries
|
||||
|
||||
echo "Calling idlc..."
|
||||
idlc -w -verbose "${PWD}"/idl/XFinancials.idl
|
||||
if command -v idlc &> /dev/null
|
||||
then
|
||||
echo "Calling idlc..."
|
||||
idlc -w -verbose "${PWD}"/idl/XFinancials.idl
|
||||
|
||||
echo "Calling regmerge..."
|
||||
regmerge -v "${PWD}"/build/XFinancials.rdb UCR "${PWD}"/idl/XFinancials.urd
|
||||
echo "Calling regmerge..."
|
||||
regmerge -v "${PWD}"/build/XFinancials.rdb UCR "${PWD}"/idl/XFinancials.urd
|
||||
|
||||
rm "${PWD}"/idl/XFinancials.urd
|
||||
rm "${PWD}"/idl/XFinancials.urd
|
||||
else
|
||||
# LibreOffice 7.4+
|
||||
export unoTypes=/usr/lib/libreoffice/program/types.rdb
|
||||
export offTypes=/usr/lib/libreoffice/program/types/offapi.rdb
|
||||
|
||||
echo "Calling unoidl-write..."
|
||||
unoidl-write $unoTypes $offTypes "${PWD}"/idl/XFinancials.idl "${PWD}"/build/XFinancials.rdb
|
||||
fi
|
||||
|
||||
echo "Generating meta files..."
|
||||
python3 "${PWD}"/src/generate_metainfo.py
|
||||
@@ -26,9 +53,57 @@ python3 "${PWD}"/src/generate_metainfo.py
|
||||
cp -f "${PWD}"/src/financials.py "${PWD}"/build/
|
||||
cp -f "${PWD}"/src/datacode.py "${PWD}"/build/
|
||||
cp -f "${PWD}"/src/baseclient.py "${PWD}"/build/
|
||||
cp -f "${PWD}"/src/jsonParser.py "${PWD}"/build/
|
||||
cp -f "${PWD}"/src/google.py "${PWD}"/build/
|
||||
cp -f "${PWD}"/src/yahoo.py "${PWD}"/build/
|
||||
cp -f "${PWD}"/src/naivehtmlparser.py "${PWD}"/build/
|
||||
cp -f "${PWD}"/src/tz.py "${PWD}"/build/
|
||||
cp -f "${PWD}"/src/financials_ft.py "${PWD}"/build/
|
||||
cp -f "${PWD}"/src/financials_yahoo.py "${PWD}"/build/
|
||||
cp -f "${PWD}"/src/financials_coinbase.py "${PWD}"/build/
|
||||
|
||||
# this copies python some modules to extension so they doesn't have to be installed by user
|
||||
|
||||
TMPFILE=`mktemp`
|
||||
|
||||
# https://pypi.org/project/python-dateutil/
|
||||
wget "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl" -O $TMPFILE
|
||||
unzip $TMPFILE dateutil/\* -d "${PWD}"/build/
|
||||
rm $TMPFILE
|
||||
|
||||
# https://pypi.org/project/pytz/
|
||||
wget "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl" -O $TMPFILE
|
||||
unzip $TMPFILE pytz/\* -d "${PWD}"/build/
|
||||
rm $TMPFILE
|
||||
|
||||
# https://pypi.org/project/pyparsing/
|
||||
# lastest version of "single-file" pyparsing 2.x - used by Ubuntu 22.04 as python3-pyparsing
|
||||
wget "https://files.pythonhosted.org/packages/8a/bb/488841f56197b13700afd5658fc279a2025a39e22449b7cf29864669b15d/pyparsing-2.4.7-py2.py3-none-any.whl" -O $TMPFILE
|
||||
unzip $TMPFILE pyparsing.py -d "${PWD}"/build/
|
||||
rm $TMPFILE
|
||||
|
||||
# https://pypi.org/project/six/
|
||||
wget "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl" -O $TMPFILE
|
||||
unzip $TMPFILE six.py -d "${PWD}"/build/
|
||||
rm $TMPFILE
|
||||
|
||||
# https://pypi.org/project/requests/
|
||||
wget "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl" -O $TMPFILE
|
||||
unzip $TMPFILE requests/\* -d "${PWD}"/build/
|
||||
rm $TMPFILE
|
||||
|
||||
# https://pypi.org/project/urllib3/
|
||||
# urllib3-2.2.3 is last version supporting Python 3.8 used by LibreOffice 7.1
|
||||
wget "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl" -O $TMPFILE
|
||||
unzip $TMPFILE urllib3/\* -d "${PWD}"/build/
|
||||
rm $TMPFILE
|
||||
|
||||
# https://pypi.org/project/certifi/
|
||||
wget "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl" -O $TMPFILE
|
||||
unzip $TMPFILE certifi/\* -d "${PWD}"/build/
|
||||
rm $TMPFILE
|
||||
|
||||
# https://pypi.org/project/idna/
|
||||
wget "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl" -O $TMPFILE
|
||||
unzip $TMPFILE idna/\* -d "${PWD}"/build/
|
||||
rm $TMPFILE
|
||||
|
||||
echo "Package into oxt file..."
|
||||
pushd "${PWD}"/build/
|
||||
|
||||
Binary file not shown.
+228
-100
@@ -8,123 +8,151 @@
|
||||
# version 3 of the License, or (at your option) any later version.
|
||||
|
||||
|
||||
import codecs
|
||||
import gzip
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import random
|
||||
import sys
|
||||
import select
|
||||
|
||||
from http.client import HTTPConnection, HTTPSConnection
|
||||
from http import cookiejar
|
||||
|
||||
import urllib.request
|
||||
|
||||
from importlib import util
|
||||
from datacode import Datacode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def log(str):
|
||||
# print(str, file=sys.stderr)
|
||||
pass
|
||||
|
||||
# logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
curl_cffi_present = not util.find_spec("curl_cffi") is None
|
||||
requests_present = not util.find_spec("requests") is None
|
||||
|
||||
if curl_cffi_present:
|
||||
logger.debug("Importing curl_cffi...")
|
||||
from curl_cffi import requests, __version__ as requests_version, __name__ as requests_name
|
||||
elif requests_present:
|
||||
logger.debug("Importing requests...")
|
||||
import requests
|
||||
requests_version = requests.__version__
|
||||
requests_name = requests.__name__
|
||||
else:
|
||||
raise Exception("Neither curl_cffi nor requests found.")
|
||||
|
||||
# import requests
|
||||
|
||||
|
||||
class HttpException(Exception):
|
||||
def __init__(self, url, response):
|
||||
self.url = url
|
||||
self.response = response
|
||||
|
||||
def __str__(self):
|
||||
if self.response is None:
|
||||
return f"url='{self.url}'"
|
||||
if type(self.response) is str:
|
||||
return f"url='{self.url}' status='{self.response}'"
|
||||
if self.response.headers:
|
||||
h = '\n'.join(sorted(self.response.headers.__str__().splitlines(), key=lambda l: l.lower()))
|
||||
return f"url='{self.url}' status={self.response.status_code} reason='{self.response.reason}' headers={h}\n"
|
||||
else:
|
||||
return f"url='{self.url}' status={self.response.status_code} reason='{self.response.reason}'"
|
||||
|
||||
|
||||
class BaseClient:
|
||||
def __init__(self):
|
||||
self.connections = {}
|
||||
self.cookies = cookiejar.CookieJar()
|
||||
self.last_url = None
|
||||
self.redirect_count = 0
|
||||
|
||||
self.basedir = os.path.join(str(pathlib.Path.home()), '.financials-extension')
|
||||
os.makedirs(self.basedir, exist_ok=True)
|
||||
|
||||
user_agents = [
|
||||
'Mozilla/5.0 (X11; Linux x86_64; rv:57.0) Gecko/20100101 Firefox/57.0',
|
||||
'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:55.0) Gecko/20100101 Firefox/55.0'
|
||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36',
|
||||
'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36'
|
||||
'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:133.0) Gecko/20100101 Firefox/133.0',
|
||||
'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0',
|
||||
'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:135.0) Gecko/20100101 Firefox/135.0',
|
||||
'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0',
|
||||
'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:137.0) Gecko/20100101 Firefox/137.0',
|
||||
]
|
||||
|
||||
self.default_headers = {
|
||||
'User-Agent': random.sample(user_agents, 1)[0],
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
|
||||
'Accept-Encoding': 'gzip, deflate',
|
||||
'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8'
|
||||
}
|
||||
if curl_cffi_present:
|
||||
self.session = requests.Session()
|
||||
if logger.isEnabledFor(logging.DEBUG) and self.session.curl:
|
||||
self.session.curl.debug()
|
||||
else:
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({'User-Agent': random.sample(user_agents, 1)[0],
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||
'Accept-Encoding': 'gzip, deflate',
|
||||
'Accept-Language': 'en-US,en;q=0.5',
|
||||
'Connection': 'keep-alive',
|
||||
'Cache-Control': 'max-age=0',
|
||||
})
|
||||
|
||||
def request(self, method: str, url: str, data=None, headers={}, **kwargs):
|
||||
self.session.max_redirects = 5
|
||||
|
||||
_headers = self.default_headers.copy()
|
||||
for key, value in headers.items():
|
||||
_headers[key] = value
|
||||
def urlopen(self, url, data=None):
|
||||
|
||||
connection = None
|
||||
self.last_url = None
|
||||
|
||||
scheme, _, host, path = url.split('/', 3)
|
||||
resp = self.session.request('POST' if data else 'GET', url, data=data)
|
||||
|
||||
if (scheme, host) in self.connections:
|
||||
connection = self.connections.get((scheme, host))
|
||||
if 400 <= resp.status_code < 500:
|
||||
if resp.headers.get('X-Cache') == 'Error from cloudfront':
|
||||
resp = self.session.request('POST' if data else 'GET', url, data=data)
|
||||
|
||||
if connection and select.select([connection.sock], [], [], 0)[0]:
|
||||
connection.close()
|
||||
connection = None
|
||||
if resp.status_code >= 400:
|
||||
logger.warning("url='%s' status=%s reason='%s' headers=%s", resp.url,
|
||||
resp.status_code, resp.reason,
|
||||
'\n'.join(sorted(resp.headers.__str__().splitlines(), key=lambda l: l.lower())))
|
||||
raise HttpException(url, resp)
|
||||
|
||||
if not connection:
|
||||
log('Creating HTTP connection --------- ----------------------------------------')
|
||||
connection = HTTPConnection(host, **kwargs) if scheme == 'http:' else HTTPSConnection(host, **kwargs)
|
||||
self.redirect_count = len(resp.history)
|
||||
self.last_url = resp.url
|
||||
|
||||
log('Creating HTTP request ------------ ----------------------------------------')
|
||||
log(url)
|
||||
return resp.text
|
||||
|
||||
# generate and add cookie headers
|
||||
request = urllib.request.Request(url)
|
||||
def get_ticker(self):
|
||||
|
||||
self.cookies.add_cookie_header(request)
|
||||
if request.get_header('Cookie'):
|
||||
_headers['Cookie'] = request.get_header('Cookie')
|
||||
tick = {}
|
||||
|
||||
for key, value in _headers.items():
|
||||
log('{}: {}'.format(key, value))
|
||||
tick[Datacode.ADJ_CLOSE] = None
|
||||
tick[Datacode.ASKSIZE] = None
|
||||
tick[Datacode.ASK] = None
|
||||
tick[Datacode.AVG_DAILY_VOL_3MONTH] = None
|
||||
tick[Datacode.BETA] = None
|
||||
tick[Datacode.BIDSIZE] = None
|
||||
tick[Datacode.BID] = None
|
||||
tick[Datacode.CHANGE] = None
|
||||
tick[Datacode.CHANGE_IN_PERCENT] = None
|
||||
tick[Datacode.CURRENCY] = None
|
||||
tick[Datacode.DIV] = None
|
||||
tick[Datacode.DIV_YIELD] = None
|
||||
tick[Datacode.EPS] = None
|
||||
tick[Datacode.EXCHANGE] = None
|
||||
tick[Datacode.EXPIRY_DATE] = None
|
||||
tick[Datacode.EX_DIV_DATE] = None
|
||||
tick[Datacode.FREE_FLOAT] = None
|
||||
tick[Datacode.SETTLEMENT_DATE] = None
|
||||
tick[Datacode.HIGH] = None
|
||||
tick[Datacode.HIGH_52_WEEK] = None
|
||||
tick[Datacode.INDUSTRY] = None
|
||||
tick[Datacode.LAST_PRICE] = None
|
||||
tick[Datacode.LAST_PRICE_DATE] = None
|
||||
tick[Datacode.LAST_PRICE_TIME] = None
|
||||
tick[Datacode.LOW] = None
|
||||
tick[Datacode.LOW_52_WEEK] = None
|
||||
tick[Datacode.MARKET_CAP] = None
|
||||
tick[Datacode.NAME] = None
|
||||
tick[Datacode.OPEN] = None
|
||||
tick[Datacode.PAYOUT_RATIO] = None
|
||||
tick[Datacode.PE_RATIO] = None
|
||||
tick[Datacode.PREV_CLOSE] = None
|
||||
tick[Datacode.SECTOR] = None
|
||||
tick[Datacode.SHARES_OUT] = None
|
||||
tick[Datacode.TICKER] = None
|
||||
tick[Datacode.TIMEZONE] = None
|
||||
tick[Datacode.VOLUME] = None
|
||||
|
||||
# request
|
||||
connection.request(method, '/' + path, data, _headers)
|
||||
response = connection.getresponse()
|
||||
tick[Datacode.TIMESTAMP] = None
|
||||
|
||||
log('Processing HTTP response --------- ----------------------------------------')
|
||||
|
||||
# log('response.status={}'.format(response.status))
|
||||
for key, value in response.getheaders():
|
||||
log('{}: {}'.format(key, value))
|
||||
|
||||
self.cookies.extract_cookies(response, request)
|
||||
self.connections[(scheme, host)] = connection
|
||||
|
||||
return response
|
||||
|
||||
def urlopen(self, url, data=None, headers={}, **kwargs):
|
||||
|
||||
response = self.request('POST' if data else 'GET', url, data, headers, **kwargs)
|
||||
text = response.read()
|
||||
|
||||
if 300 <= response.status < 400:
|
||||
|
||||
scheme, _, host, path = url.split('/', 3)
|
||||
redirect_to = response.getheader('Location')
|
||||
if host not in redirect_to:
|
||||
redirect_to = scheme + '//' + host + redirect_to
|
||||
|
||||
if response.getheader('Location'):
|
||||
response = self.request('POST' if data else 'GET', redirect_to, data, headers, **kwargs)
|
||||
text = response.read()
|
||||
|
||||
assert response.status < 400, \
|
||||
'HTTP Status={} Reason={} url={}'.format(response.status, response.reason, url)
|
||||
|
||||
if response.getheader('Content-Encoding') == 'gzip':
|
||||
text = gzip.decompress(text)
|
||||
|
||||
content_type = response.headers.get_content_charset()
|
||||
if content_type is None:
|
||||
content_type = 'utf-8'
|
||||
text = codecs.decode(text, encoding=content_type, errors='ignore')
|
||||
|
||||
return text
|
||||
return tick
|
||||
|
||||
def _return_value(self, data: dict, datacode: int):
|
||||
|
||||
@@ -136,6 +164,9 @@ class BaseClient:
|
||||
:return: value or None
|
||||
"""
|
||||
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
if datacode == Datacode.PREV_CLOSE.value and Datacode.PREV_CLOSE in data:
|
||||
return data[Datacode.PREV_CLOSE]
|
||||
@@ -147,10 +178,16 @@ class BaseClient:
|
||||
return data[Datacode.CHANGE]
|
||||
|
||||
elif datacode == Datacode.LAST_PRICE_DATE.value and Datacode.LAST_PRICE_DATE in data:
|
||||
return data[Datacode.LAST_PRICE_DATE].isoformat()
|
||||
if data[Datacode.LAST_PRICE_DATE]:
|
||||
return data[Datacode.LAST_PRICE_DATE].isoformat()
|
||||
else:
|
||||
return data[Datacode.LAST_PRICE_DATE]
|
||||
|
||||
elif datacode == Datacode.LAST_PRICE_TIME.value and Datacode.LAST_PRICE_TIME in data:
|
||||
return data[Datacode.LAST_PRICE_TIME].isoformat()
|
||||
if data[Datacode.LAST_PRICE_TIME]:
|
||||
return data[Datacode.LAST_PRICE_TIME].isoformat()
|
||||
else:
|
||||
return data[Datacode.LAST_PRICE_TIME]
|
||||
|
||||
elif datacode == Datacode.CHANGE_IN_PERCENT.value and Datacode.CHANGE_IN_PERCENT in data:
|
||||
return data[Datacode.CHANGE_IN_PERCENT]
|
||||
@@ -164,11 +201,74 @@ class BaseClient:
|
||||
elif datacode == Datacode.LAST_PRICE.value and Datacode.LAST_PRICE in data:
|
||||
return data[Datacode.LAST_PRICE]
|
||||
|
||||
elif datacode == Datacode.BID.value and Datacode.BID in data:
|
||||
return data[Datacode.BID]
|
||||
|
||||
elif datacode == Datacode.ASK.value and Datacode.ASK in data:
|
||||
return data[Datacode.ASK]
|
||||
|
||||
elif datacode == Datacode.BIDSIZE.value and Datacode.BIDSIZE in data:
|
||||
return data[Datacode.BIDSIZE]
|
||||
|
||||
elif datacode == Datacode.ASKSIZE.value and Datacode.ASKSIZE in data:
|
||||
return data[Datacode.ASKSIZE]
|
||||
|
||||
elif datacode == Datacode.LOW_52_WEEK.value and Datacode.LOW_52_WEEK in data:
|
||||
return data[Datacode.LOW_52_WEEK]
|
||||
|
||||
elif datacode == Datacode.HIGH_52_WEEK.value and Datacode.HIGH_52_WEEK in data:
|
||||
return data[Datacode.HIGH_52_WEEK]
|
||||
|
||||
elif datacode == Datacode.MARKET_CAP.value and Datacode.MARKET_CAP in data:
|
||||
return data[Datacode.MARKET_CAP]
|
||||
|
||||
elif datacode == Datacode.VOLUME.value and Datacode.VOLUME in data:
|
||||
return data[Datacode.VOLUME]
|
||||
|
||||
elif datacode == Datacode.AVG_DAILY_VOL_3MOMTH.value and Datacode.AVG_DAILY_VOL_3MOMTH in data:
|
||||
return data[Datacode.AVG_DAILY_VOL_3MOMTH]
|
||||
elif datacode == Datacode.AVG_DAILY_VOL_3MONTH.value and Datacode.AVG_DAILY_VOL_3MONTH in data:
|
||||
return data[Datacode.AVG_DAILY_VOL_3MONTH]
|
||||
|
||||
elif datacode == Datacode.BETA.value and Datacode.BETA in data:
|
||||
return data[Datacode.BETA]
|
||||
|
||||
elif datacode == Datacode.EPS.value and Datacode.EPS in data:
|
||||
return data[Datacode.EPS]
|
||||
|
||||
elif datacode == Datacode.PE_RATIO.value and Datacode.PE_RATIO in data:
|
||||
return data[Datacode.PE_RATIO]
|
||||
|
||||
elif datacode == Datacode.DIV.value and Datacode.DIV in data:
|
||||
return data[Datacode.DIV]
|
||||
|
||||
elif datacode == Datacode.DIV_YIELD.value and Datacode.DIV_YIELD in data:
|
||||
return data[Datacode.DIV_YIELD]
|
||||
|
||||
elif datacode == Datacode.EX_DIV_DATE.value and Datacode.EX_DIV_DATE in data:
|
||||
if data[Datacode.EX_DIV_DATE]:
|
||||
return data[Datacode.EX_DIV_DATE].isoformat()
|
||||
else:
|
||||
return data[Datacode.EX_DIV_DATE]
|
||||
|
||||
elif datacode == Datacode.PAYOUT_RATIO.value and Datacode.PAYOUT_RATIO in data:
|
||||
return data[Datacode.PAYOUT_RATIO]
|
||||
|
||||
elif datacode == Datacode.EXPIRY_DATE.value and Datacode.EXPIRY_DATE in data:
|
||||
if data[Datacode.EXPIRY_DATE]:
|
||||
return data[Datacode.EXPIRY_DATE].isoformat()
|
||||
else:
|
||||
return data[Datacode.EXPIRY_DATE]
|
||||
|
||||
elif datacode == Datacode.FREE_FLOAT.value and Datacode.FREE_FLOAT in data:
|
||||
return data[Datacode.FREE_FLOAT]
|
||||
|
||||
elif datacode == Datacode.SETTLEMENT_DATE.value and Datacode.SETTLEMENT_DATE in data:
|
||||
if data[Datacode.SETTLEMENT_DATE]:
|
||||
return data[Datacode.SETTLEMENT_DATE].isoformat()
|
||||
else:
|
||||
return data[Datacode.SETTLEMENT_DATE]
|
||||
|
||||
elif datacode == Datacode.SHARES_OUT.value and Datacode.SHARES_OUT in data:
|
||||
return data[Datacode.SHARES_OUT]
|
||||
|
||||
elif datacode == Datacode.CLOSE.value and Datacode.CLOSE in data:
|
||||
return data[Datacode.CLOSE]
|
||||
@@ -176,22 +276,50 @@ class BaseClient:
|
||||
elif datacode == Datacode.ADJ_CLOSE.value and Datacode.ADJ_CLOSE in data:
|
||||
return data[Datacode.ADJ_CLOSE]
|
||||
|
||||
elif datacode == Datacode.SECTOR.value and Datacode.SECTOR in data:
|
||||
return data[Datacode.SECTOR]
|
||||
|
||||
elif datacode == Datacode.INDUSTRY.value and Datacode.INDUSTRY in data:
|
||||
return data[Datacode.INDUSTRY]
|
||||
|
||||
elif datacode == Datacode.TICKER.value and Datacode.TICKER in data:
|
||||
return data[Datacode.TICKER]
|
||||
|
||||
elif datacode == Datacode.EXCHANGE.value and data[Datacode.EXCHANGE]:
|
||||
elif datacode == Datacode.EXCHANGE.value and Datacode.EXCHANGE in data:
|
||||
return data[Datacode.EXCHANGE]
|
||||
|
||||
elif datacode == Datacode.CURRENCY.value and Datacode.CURRENCY in data:
|
||||
return data[Datacode.CURRENCY]
|
||||
|
||||
elif datacode == Datacode.NAME.value and data[Datacode.NAME]:
|
||||
elif datacode == Datacode.NAME.value and Datacode.NAME in data:
|
||||
return data[Datacode.NAME]
|
||||
|
||||
elif datacode == Datacode.TIMEZONE.value and data[Datacode.TIMEZONE]:
|
||||
return str(data[Datacode.TIMEZONE])
|
||||
elif datacode == Datacode.TIMEZONE.value and Datacode.TIMEZONE in data:
|
||||
if data[Datacode.TIMEZONE] is not None and type(data[Datacode.TIMEZONE]) != str:
|
||||
return str(data[Datacode.TIMEZONE])
|
||||
else:
|
||||
return data[Datacode.TIMEZONE]
|
||||
|
||||
except BaseException as e:
|
||||
return 'BaseClient.return_value(\'{}\', {}) - {}'.format(data, datacode, e)
|
||||
|
||||
return "Data doesn't exist - {}".format(datacode)
|
||||
|
||||
def save_wrapper(self, f):
|
||||
try:
|
||||
value = f()
|
||||
logger.debug(value)
|
||||
return value
|
||||
except BaseException as e:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def version(self):
|
||||
return requests_name + "_" + requests_version
|
||||
|
||||
def curl(self):
|
||||
return curl_version
|
||||
|
||||
def close(self):
|
||||
self.session.close()
|
||||
|
||||
+26
-4
@@ -24,12 +24,36 @@ class Datacode(Enum):
|
||||
|
||||
LAST_PRICE = 21
|
||||
|
||||
BID = 22
|
||||
ASK = 25
|
||||
BIDSIZE = 30
|
||||
ASKSIZE = 31
|
||||
|
||||
HIGH_52_WEEK = 24
|
||||
LOW_52_WEEK = 26
|
||||
MARKET_CAP = 27
|
||||
|
||||
VOLUME = 35
|
||||
AVG_DAILY_VOL_3MOMTH = 39
|
||||
AVG_DAILY_VOL_3MONTH = 39
|
||||
|
||||
BETA = 67
|
||||
EPS = 68
|
||||
PE_RATIO = 69
|
||||
DIV = 70
|
||||
DIV_YIELD = 71
|
||||
EX_DIV_DATE = 72
|
||||
PAYOUT_RATIO = 73
|
||||
EXPIRY_DATE = 74
|
||||
SHARES_OUT = 75
|
||||
FREE_FLOAT = 76
|
||||
SETTLEMENT_DATE = 77
|
||||
|
||||
CLOSE = 90
|
||||
ADJ_CLOSE = 91
|
||||
|
||||
SECTOR = 98
|
||||
INDUSTRY = 99
|
||||
|
||||
TICKER = 101
|
||||
EXCHANGE = 102
|
||||
CURRENCY = 103
|
||||
@@ -38,8 +62,6 @@ class Datacode(Enum):
|
||||
|
||||
TIMESTAMP = 999
|
||||
|
||||
# TODO YAHOO fundInceptionDate
|
||||
|
||||
@classmethod
|
||||
def has_value(cls, value):
|
||||
return (any(value == item.value for item in cls))
|
||||
return any(value == item.value for item in cls)
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<description xmlns="http://openoffice.org/extensions/description/2006"
|
||||
xmlns:d="http://openoffice.org/extensions/description/2006"
|
||||
xmlns:l="http://libreoffice.org/extensions/description/2011"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
|
||||
<!-- only used for testing -->
|
||||
|
||||
<dependencies>
|
||||
<l:LibreOffice-minimal-version value="5.0" d:name="LibreOffice 5.0" />
|
||||
</dependencies>
|
||||
|
||||
<identifier value="com.financials.getinfo" />
|
||||
<version value="3.0.0" />
|
||||
<display-name><name lang="en">Financial Market Extension</name></display-name>
|
||||
<publisher><name xlink:href="https://github.com/cmallwitz/Financials-Extension" lang="en">The Publisher</name></publisher>
|
||||
<extension-description><src xlink:href="description-en-US.txt" lang="en" /></extension-description>
|
||||
|
||||
</description>
|
||||
+134
-29
@@ -8,42 +8,100 @@
|
||||
# version 3 of the License, or (at your option) any later version.
|
||||
|
||||
import datetime
|
||||
import dateutil.parser
|
||||
import inspect
|
||||
import locale
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pathlib
|
||||
import platform
|
||||
import ssl
|
||||
import sys
|
||||
import time
|
||||
from functools import wraps
|
||||
from importlib import util
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
import unohelper
|
||||
|
||||
from com.financials.getinfo import Financials
|
||||
|
||||
basedir = os.path.join(str(pathlib.Path.home()), '.financials-extension')
|
||||
os.makedirs(basedir, exist_ok=True)
|
||||
|
||||
logging.basicConfig(
|
||||
handlers=[logging.FileHandler(filename=os.path.join(basedir, 'extension.log'), encoding='utf-8', mode='a+')],
|
||||
format="%(asctime)s %(name)s %(levelname)s %(message)s",
|
||||
level=logging.INFO)
|
||||
|
||||
# Add current directory to import path
|
||||
current_dir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
|
||||
if current_dir not in sys.path:
|
||||
sys.path.insert(0, current_dir)
|
||||
|
||||
dateutil_missing = util.find_spec("dateutil") is None
|
||||
pyparsing_missing = util.find_spec("pyparsing") is None
|
||||
pytz_missing = util.find_spec("pytz") is None
|
||||
|
||||
if dateutil_missing or pyparsing_missing or pytz_missing:
|
||||
msg = ''
|
||||
msg += ' dateutil' if dateutil_missing else ''
|
||||
msg += ' pyparsing' if pyparsing_missing else ''
|
||||
msg += ' pytz' if pytz_missing else ''
|
||||
raise Exception("THIS EXTENSION NEEDS THE FOLLOWING PYTHON 3 LIBRARIES INSTALLED:" + msg)
|
||||
|
||||
import dateutil.parser
|
||||
import pytz
|
||||
import pyparsing
|
||||
import six
|
||||
|
||||
from datacode import Datacode
|
||||
import google
|
||||
import yahoo
|
||||
import financials_yahoo as yahoo
|
||||
import financials_coinbase as coinbase
|
||||
import financials_ft as ft
|
||||
|
||||
implementation_name = "com.financials.getinfo.python.FinancialsImpl" # as defined in Financials.xcu
|
||||
implementation_services = ("com.sun.star.sheet.AddIn",)
|
||||
|
||||
# Disabling SSL certificate validation as Python setup on MacOS seems to be broken
|
||||
# Only reading public data so this should be safe
|
||||
|
||||
try:
|
||||
_create_unverified_https_context = ssl._create_unverified_context
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
ssl._create_default_https_context = _create_unverified_https_context
|
||||
|
||||
|
||||
def profile(fn):
|
||||
@wraps(fn)
|
||||
def with_profiling(*args, **kwargs):
|
||||
start = time.perf_counter()
|
||||
r = fn(*args, **kwargs)
|
||||
elapsed = time.perf_counter() - start
|
||||
|
||||
with open(os.path.join(basedir, 'trace.log'), "a+", encoding="utf-8") as text_file:
|
||||
print(
|
||||
f"{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')} {fn.__name__} *args={args[1:]} r='{r}' {(1000 * elapsed):.3f} ms",
|
||||
file=text_file)
|
||||
|
||||
return r
|
||||
|
||||
return with_profiling
|
||||
|
||||
|
||||
class FinancialsImpl(unohelper.Base, Financials):
|
||||
"""Define the main class for the Financials extension """
|
||||
|
||||
def __init__(self, ctx):
|
||||
self.ctx = ctx
|
||||
self.google = google.createInstance(ctx)
|
||||
self.yahoo = yahoo.createInstance(ctx)
|
||||
self.coinbase = coinbase.createInstance(ctx)
|
||||
self.ft = ft.createInstance(ctx)
|
||||
|
||||
@profile
|
||||
def getRealtime(self, ticker, datacode=None, source=None):
|
||||
|
||||
if ticker == 'SUPPORT':
|
||||
if ticker == 'SUPPORT' or ticker == 'support':
|
||||
return self.support(datacode)
|
||||
|
||||
try:
|
||||
@@ -63,21 +121,27 @@ class FinancialsImpl(unohelper.Base, Financials):
|
||||
return 'Datacode is empty'
|
||||
|
||||
try:
|
||||
datacode = int(float(str(datacode).strip()))
|
||||
dc = str(datacode).strip().upper()
|
||||
if dc in Datacode.__members__:
|
||||
dc = Datacode[dc].value
|
||||
datacode = int(float(dc))
|
||||
except:
|
||||
return 'Datacode is not a number'
|
||||
return 'Datacode is invalid'
|
||||
|
||||
if not Datacode.has_value(datacode):
|
||||
return 'Datacode {} not supported'.format(datacode)
|
||||
|
||||
source = source.upper()
|
||||
ticker = str(ticker).strip()
|
||||
source = str(source).upper()
|
||||
|
||||
if source == 'GOOGLE':
|
||||
s = self.google.getRealtime(str(ticker).strip(), datacode)
|
||||
elif source == 'YAHOO':
|
||||
s = self.yahoo.getRealtime(str(ticker).strip(), datacode)
|
||||
if source == 'YAHOO':
|
||||
s = self.yahoo.getRealtime(ticker, datacode)
|
||||
elif source == 'FT':
|
||||
s = self.ft.getRealtime(ticker, datacode)
|
||||
elif source == 'COINBASE':
|
||||
s = self.coinbase.getRealtime(ticker, datacode)
|
||||
else:
|
||||
s = 'getRealtime:Source \'{}\' not supported'.format(source)
|
||||
s = 'Source \'{}\' not supported'.format(source)
|
||||
|
||||
except Exception as ex:
|
||||
return str(ex)
|
||||
@@ -89,6 +153,7 @@ class FinancialsImpl(unohelper.Base, Financials):
|
||||
|
||||
return x
|
||||
|
||||
@profile
|
||||
def getHistoric(self, ticker, datacode=None, date=None, source=None):
|
||||
|
||||
if ticker == 'SUPPORT':
|
||||
@@ -117,9 +182,12 @@ class FinancialsImpl(unohelper.Base, Financials):
|
||||
return 'Date is empty'
|
||||
|
||||
try:
|
||||
datacode = int(float(str(datacode).strip()))
|
||||
dc = str(datacode).strip().upper()
|
||||
if dc in Datacode.__members__:
|
||||
dc = Datacode[dc].value
|
||||
datacode = int(float(dc))
|
||||
except:
|
||||
return 'Datacode {} is not a number'.format(datacode)
|
||||
return 'Datacode {} is invalid'.format(datacode)
|
||||
|
||||
if not Datacode.has_value(datacode):
|
||||
return 'Datacode {} not supported'.format(datacode)
|
||||
@@ -127,8 +195,8 @@ class FinancialsImpl(unohelper.Base, Financials):
|
||||
if type(date) == float or type(date) == int:
|
||||
|
||||
try:
|
||||
offset = int(date) # offset for 1899-12-30
|
||||
d = dateutil.parser.parse('1899-12-30') + datetime.timedelta(days=offset)
|
||||
offset = int(date) # offset for 1899-12-30
|
||||
d = dateutil.parser.parse('1899-12-30', yearfirst=True, dayfirst=False) + datetime.timedelta(days=offset)
|
||||
d = d.date().isoformat()
|
||||
except:
|
||||
return 'Date format not supported: {}'.format(date)
|
||||
@@ -137,19 +205,20 @@ class FinancialsImpl(unohelper.Base, Financials):
|
||||
elif type(date) == str:
|
||||
|
||||
try:
|
||||
int(dateutil.parser.parse(date).strftime('%s'))
|
||||
int(dateutil.parser.parse(date, yearfirst=True, dayfirst=False).timestamp())
|
||||
except:
|
||||
return 'Date format not supported: \'{}\''.format(date)
|
||||
|
||||
else:
|
||||
return 'Date type not supported: {} \'{}\''.format(type(date), date)
|
||||
|
||||
source = source.upper()
|
||||
ticker = str(ticker).strip()
|
||||
source = str(source).upper()
|
||||
|
||||
if source == 'YAHOO':
|
||||
s = self.yahoo.getHistoric(str(ticker).strip(), datacode, date)
|
||||
else:
|
||||
s = 'getHistoric: Source \'{}\' not supported'.format(source)
|
||||
s = 'Source \'{}\' not supported'.format(source)
|
||||
|
||||
except Exception as ex:
|
||||
return str(ex)
|
||||
@@ -161,18 +230,48 @@ class FinancialsImpl(unohelper.Base, Financials):
|
||||
|
||||
return x
|
||||
|
||||
@profile
|
||||
def support(self, datacode):
|
||||
|
||||
s = 'ctx={}\nid(self)={}\npid={}\nuname={}\nsys.executable={}\nsys.version={}\nhome={}'.format(
|
||||
version = '0.0.0'
|
||||
|
||||
description_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'description.xml')
|
||||
for e in ET.parse(description_file).getroot():
|
||||
if e.tag.endswith('version'):
|
||||
version = e.attrib['value']
|
||||
|
||||
s = ('ctx={}\nid(self)={}\nversion={}\nfile={}\ncwd={}\nhome={}\nuname={}\npid={}\nsys.executable={}\nsys.version={}\nsys.path={}\n' +
|
||||
'locale={}\ndefaultlocale={}\ndateutil={}\npytz={}\npyparsing={}\nsix={}\nrequests={}').format(
|
||||
self.ctx,
|
||||
id(self),
|
||||
os.getpid(),
|
||||
version,
|
||||
os.path.realpath(__file__),
|
||||
os.path.realpath(os.getcwd()),
|
||||
str(pathlib.Path.home()),
|
||||
' '.join(platform.uname()),
|
||||
os.getpid(),
|
||||
sys.executable,
|
||||
sys.version.replace("\n", " "),
|
||||
str(pathlib.Path.home()),
|
||||
type(datacode),
|
||||
str(datacode))
|
||||
sys.path,
|
||||
locale.getlocale(),
|
||||
locale.getdefaultlocale(),
|
||||
dateutil.__version__,
|
||||
pytz.__version__,
|
||||
pyparsing.__version__,
|
||||
six.__version__,
|
||||
self.ft.version()
|
||||
)
|
||||
|
||||
ld_preload = os.environ.get('LD_PRELOAD')
|
||||
if ld_preload:
|
||||
s += f"\nLD_PRELOAD={ld_preload}"
|
||||
|
||||
curl_impersonate = os.environ.get('CURL_IMPERSONATE')
|
||||
if curl_impersonate:
|
||||
s += f"\nCURL_IMPERSONATE={curl_impersonate}"
|
||||
|
||||
if 'curl_cffi' in self.ft.version():
|
||||
s += f"\ncurl_version=\"{self.ft.session.curl.version().decode()}\""
|
||||
|
||||
if datacode:
|
||||
s = '{}\ntype(datacode)={}\nstr(datacode)={}'.format(
|
||||
@@ -182,10 +281,16 @@ class FinancialsImpl(unohelper.Base, Financials):
|
||||
|
||||
return s
|
||||
|
||||
def close(self):
|
||||
if self.yahoo: self.yahoo.close()
|
||||
if self.coinbase: self.coinbase.close()
|
||||
if self.ft: self.ft.close()
|
||||
|
||||
|
||||
def createInstance(ctx):
|
||||
return FinancialsImpl(ctx)
|
||||
|
||||
|
||||
# pythonloader looks for a static g_ImplementationHelper variable
|
||||
# python loader looks for a static g_ImplementationHelper variable
|
||||
g_ImplementationHelper = unohelper.ImplementationHelper()
|
||||
g_ImplementationHelper.addImplementation(createInstance, implementation_name, implementation_services, )
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
# financials_coinbase.py
|
||||
#
|
||||
# license: GNU LGPL
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 3 of the License, or (at your option) any later version.
|
||||
|
||||
|
||||
import csv
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import pprint
|
||||
import re
|
||||
import time
|
||||
import json
|
||||
|
||||
import dateutil.parser
|
||||
import pytz
|
||||
|
||||
from baseclient import BaseClient, HttpException
|
||||
from datacode import Datacode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# logger.setLevel(logging.DEBUG)
|
||||
|
||||
class Coinbase(BaseClient):
|
||||
def __init__(self, ctx):
|
||||
super().__init__()
|
||||
|
||||
self.crumb = None
|
||||
self.realtime = {}
|
||||
|
||||
def getRealtime(self, ticker, datacode):
|
||||
|
||||
"""
|
||||
Retrieve realtime data for ticker from Coinbase and cache it for further lookups
|
||||
|
||||
:param ticker: the ticker symbol e.g. ETH-EUR
|
||||
:param datacode: the requested datacode, not all are supported
|
||||
:return:
|
||||
"""
|
||||
|
||||
# remove white space
|
||||
ticker = "".join(ticker.split())
|
||||
|
||||
# use cached value for up to 60 seconds
|
||||
if ticker in self.realtime:
|
||||
tick = self.realtime[ticker]
|
||||
if Datacode.TIMESTAMP in tick and type(tick[Datacode.TIMESTAMP]) == float and time.time() - 60 < tick[Datacode.TIMESTAMP]:
|
||||
return self._return_value(tick, datacode)
|
||||
else:
|
||||
del self.realtime[ticker]
|
||||
|
||||
url = 'https://api.exchange.coinbase.com/products/{}/stats'.format(ticker)
|
||||
|
||||
try:
|
||||
text = self.urlopen(url)
|
||||
except BaseException as e:
|
||||
logger.exception("BaseException ticker=%s datacode=%s last_url=%s redirect_count=%s", ticker, datacode, self.last_url, self.redirect_count)
|
||||
del self.realtime[ticker]
|
||||
return 'Coinbase.getRealtime({}, {}) - urlopen: {}'.format(ticker, datacode, e)
|
||||
|
||||
try:
|
||||
with open(os.path.join(self.basedir, 'coinbase-{}.json'.format(ticker)), "w", encoding="utf-8") as text_file:
|
||||
print(f"<!-- '{self.last_url}' -->\r\n\r\n{text}", file=text_file)
|
||||
except BaseException:
|
||||
logger.exception("BaseException open/write ticker=%s datacode=%s", ticker, datacode)
|
||||
|
||||
try:
|
||||
results = json.loads(text)
|
||||
|
||||
except BaseException as e:
|
||||
logger.exception("BaseException ticker=%s datacode=%s", ticker, datacode)
|
||||
del self.realtime[ticker]
|
||||
return 'Coinbase.getRealtime({}, {}) - exception: {}'.format(ticker, datacode, e)
|
||||
|
||||
try:
|
||||
price = results['last']
|
||||
|
||||
if not price:
|
||||
return 'Could not find price for \'{}\''.format(ticker)
|
||||
|
||||
if ticker not in self.realtime:
|
||||
self.realtime[ticker] = self.get_ticker()
|
||||
|
||||
tick = self.realtime[ticker]
|
||||
|
||||
tick[Datacode.TIMESTAMP] = time.time()
|
||||
tick[Datacode.LAST_PRICE] = float(price)
|
||||
tick[Datacode.OPEN] = float(results['open'])
|
||||
tick[Datacode.HIGH] = float(results['high'])
|
||||
tick[Datacode.LOW] = float(results['low'])
|
||||
tick[Datacode.VOLUME] = float(results['volume'])
|
||||
tick[Datacode.TICKER] = ticker.split('-', 1)[0]
|
||||
tick[Datacode.CURRENCY] = ticker.split('-', 1)[1]
|
||||
|
||||
except BaseException as e:
|
||||
logger.exception("BaseException ticker=%s datacode=%s", ticker, datacode)
|
||||
del self.realtime[ticker]
|
||||
return 'Coinbase.getRealtime({}, {}) - process: {}'.format(ticker, datacode, e)
|
||||
|
||||
return self._return_value(self.realtime[ticker], datacode)
|
||||
|
||||
def createInstance(ctx):
|
||||
return Coinbase(ctx)
|
||||
@@ -0,0 +1,330 @@
|
||||
# financials_ft.py
|
||||
#
|
||||
# license: GNU LGPL
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 3 of the License, or (at your option) any later version.
|
||||
|
||||
import html
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import urllib.parse
|
||||
|
||||
import dateutil.parser
|
||||
|
||||
from baseclient import BaseClient
|
||||
from datacode import Datacode
|
||||
from tz import whois_timezone_info
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
def handle_abbreviations(s):
|
||||
s = str(s).strip().replace(',', '')
|
||||
if s.endswith('k'):
|
||||
return float(s[:-1]) * 1000
|
||||
elif s.endswith('m'):
|
||||
return float(s[:-1]) * 1000000
|
||||
elif s.endswith('bn'):
|
||||
return float(s[:-2]) * 1000000000
|
||||
elif s.endswith('tn'):
|
||||
return float(s[:-2]) * 1000000000000
|
||||
return float(s)
|
||||
|
||||
|
||||
class FT(BaseClient):
|
||||
|
||||
def __init__(self, ctx):
|
||||
super().__init__()
|
||||
|
||||
self.crumb = None
|
||||
self.realtime = {}
|
||||
self.historicdata = {}
|
||||
|
||||
def getRealtime(self, ticker: str, datacode: int):
|
||||
|
||||
"""
|
||||
Retrieve data for ticker from Financial Times and cache it for further lookups
|
||||
|
||||
:param ticker: the ticker symbol e.g. VOD:LSE
|
||||
:param datacode: the requested datacode
|
||||
:return:
|
||||
"""
|
||||
|
||||
# use cached value for up to 60 seconds
|
||||
if ticker in self.realtime:
|
||||
tick = self.realtime[ticker]
|
||||
if Datacode.TIMESTAMP in tick and type(tick[Datacode.TIMESTAMP]) == float and time.time() - 60 < tick[Datacode.TIMESTAMP]:
|
||||
return self._return_value(tick, datacode)
|
||||
else:
|
||||
del self.realtime[ticker]
|
||||
|
||||
if ticker not in self.realtime:
|
||||
self.realtime[ticker] = self.get_ticker()
|
||||
|
||||
tick = self.realtime[ticker]
|
||||
|
||||
asset_class = self.guess_asset_class(ticker)
|
||||
|
||||
url = f'https://markets.ft.com/data/{asset_class}/tearsheet/summary?s={urllib.parse.quote_plus(ticker)}'
|
||||
|
||||
try:
|
||||
text = self.urlopen(url)
|
||||
except BaseException as e:
|
||||
logger.exception("BaseException ticker=%s datacode=%s last_url=%s redirect_count=%s", ticker, datacode, self.last_url, self.redirect_count)
|
||||
del self.realtime[ticker]
|
||||
return f'FT.getRealtime({ticker}, {datacode}) - urlopen endpoint: {str(e)}'
|
||||
|
||||
try:
|
||||
temp = ticker.replace(':', '_') # Windows can't have ':' in file names
|
||||
with open(os.path.join(self.basedir, f'ft-{temp}.html'), "w", encoding="utf-8") as text_file:
|
||||
print(f"<!-- '{self.last_url}' -->\r\n\r\n{text}", file=text_file)
|
||||
except BaseException:
|
||||
logger.exception("BaseException open/write ticker=%s datacode=%s %s", ticker, datacode)
|
||||
|
||||
tick[Datacode.TIMESTAMP] = time.time()
|
||||
|
||||
try:
|
||||
r = '<h1 class="mod-tearsheet-overview__header__name mod-tearsheet-overview__header__name--large">(.*?)</h1>'
|
||||
match = re.compile(r, flags=re.DOTALL).search(text)
|
||||
if not match:
|
||||
return None
|
||||
start = match.span(0)[1]
|
||||
|
||||
tick[Datacode.NAME] = self.save_wrapper(
|
||||
lambda: html.unescape(match.group(1)).strip())
|
||||
|
||||
r = '<div class="mod-tearsheet-overview__header__symbol">(?:<div [^>]*>)?<span *[^>]*>(.*?)<'
|
||||
match = re.compile(r, flags=re.DOTALL).search(text, start)
|
||||
if match:
|
||||
start = match.span(0)[1]
|
||||
tick[Datacode.TICKER] = self.save_wrapper(
|
||||
lambda: html.unescape(match.group(1)).strip())
|
||||
|
||||
r = '<div class="mod-tearsheet-overview__esi">(.*?)<i.*?</i>(.*?)<'
|
||||
match = re.compile(r, flags=re.DOTALL).search(text, start)
|
||||
if match:
|
||||
start = match.span(0)[1]
|
||||
tick[Datacode.SECTOR] = self.save_wrapper(
|
||||
lambda: html.unescape(match.group(1)).strip())
|
||||
tick[Datacode.INDUSTRY] = self.save_wrapper(
|
||||
lambda: html.unescape(match.group(2)).strip())
|
||||
|
||||
r = r'<span [^>]*>Price \(([A-Za-z]{3}|--)\)</span><span [^>]*>([0-9,\.]+)</span>'
|
||||
match = re.compile(r, flags=re.DOTALL).search(text, start)
|
||||
if match:
|
||||
start = match.span(0)[1]
|
||||
if match.group(1) != '--':
|
||||
tick[Datacode.CURRENCY] = self.save_wrapper(
|
||||
lambda: html.unescape(match.group(1)).strip())
|
||||
tick[Datacode.LAST_PRICE] = self.save_wrapper(
|
||||
lambda: float(html.unescape(match.group(2)).replace(',', '').strip()))
|
||||
|
||||
r = r'<span[^>]*>Today\'s Change</span><span[^>]*><span[^>]*>(?:<i[^>]*></i>)?([0-9,\.-]+) */ *([0-9,\.-]+)%</span>'
|
||||
match = re.compile(r, flags=re.DOTALL).search(text, start)
|
||||
if match:
|
||||
start = match.span(0)[1]
|
||||
tick[Datacode.CHANGE] = self.save_wrapper(
|
||||
lambda: float(html.unescape(match.group(1)).replace(',', '').strip()))
|
||||
tick[Datacode.CHANGE_IN_PERCENT] = self.save_wrapper(
|
||||
lambda: float(html.unescape(match.group(2)).replace(',', '').strip()))
|
||||
|
||||
r = r'<span[^>]*>Shares traded</span><span[^>]*>([0-9mk,\.]+)</span>'
|
||||
match = re.compile(r, flags=re.DOTALL).search(text, start)
|
||||
if match:
|
||||
start = match.span(0)[1]
|
||||
tick[Datacode.VOLUME] = self.save_wrapper(
|
||||
lambda: handle_abbreviations(html.unescape(match.group(1))))
|
||||
|
||||
r = r'<span[^>]*>Beta</span><span[^>]*>([0-9,\.]+)</span>'
|
||||
match = re.compile(r, flags=re.DOTALL).search(text, start)
|
||||
if match:
|
||||
start = match.span(0)[1]
|
||||
tick[Datacode.BETA] = self.save_wrapper(
|
||||
lambda: handle_abbreviations(html.unescape(match.group(1))))
|
||||
|
||||
r = r'<span[^>]*>52 week range</span><span[^>]*>([0-9,\.]+) *- *([0-9,\.]+)</span>'
|
||||
match = re.compile(r, flags=re.DOTALL).search(text, start)
|
||||
if match:
|
||||
start = match.span(0)[1]
|
||||
tick[Datacode.LOW_52_WEEK] = self.save_wrapper(
|
||||
lambda: float(html.unescape(match.group(1)).replace(',', '').strip()))
|
||||
tick[Datacode.HIGH_52_WEEK] = self.save_wrapper(
|
||||
lambda: float(html.unescape(match.group(2)).replace(',', '').strip()))
|
||||
|
||||
r = r'<div class="mod-disclaimer">.+?as of (.+?)\.?</div>'
|
||||
match = re.compile(r, flags=re.DOTALL).search(text, start)
|
||||
if match:
|
||||
start = match.span(0)[1]
|
||||
|
||||
try:
|
||||
value = html.unescape(match.group(1)).strip()
|
||||
dt = dateutil.parser.parse(value, yearfirst=True, dayfirst=False, tzinfos=whois_timezone_info)
|
||||
tick[Datacode.LAST_PRICE_DATE] = dt.date()
|
||||
tick[Datacode.LAST_PRICE_TIME] = dt.time()
|
||||
|
||||
time_bits = value.split(' ')
|
||||
if len(time_bits) >= 4:
|
||||
tick[Datacode.TIMEZONE] = time_bits[-1]
|
||||
|
||||
except BaseException:
|
||||
pass
|
||||
|
||||
# second attempt at 52 week range
|
||||
if Datacode.LOW_52_WEEK not in tick or not tick[Datacode.LOW_52_WEEK]:
|
||||
r = r'<span class="mod-ui-range-bar__container__label--lo"><span[^>]*>([0-9,\.]+)</span>'
|
||||
match = re.compile(r, flags=re.DOTALL).search(text, start)
|
||||
if match:
|
||||
tick[Datacode.LOW_52_WEEK] = self.save_wrapper(
|
||||
lambda: float(html.unescape(match.group(1)).replace(',', '').strip()))
|
||||
|
||||
if Datacode.HIGH_52_WEEK not in tick or not tick[Datacode.HIGH_52_WEEK]:
|
||||
r = r'<span class="mod-ui-range-bar__container__label--hi"><span[^>]*>([0-9,\.]+)</span>'
|
||||
match = re.compile(r, flags=re.DOTALL).search(text, start)
|
||||
if match:
|
||||
tick[Datacode.HIGH_52_WEEK] = self.save_wrapper(
|
||||
lambda: float(html.unescape(match.group(1)).replace(',', '').strip()))
|
||||
|
||||
# just moving forward to data table
|
||||
r = '<div class="mod-tearsheet-key-stats__data__table">'
|
||||
match = re.compile(r, flags=re.DOTALL).search(text, start)
|
||||
if match:
|
||||
start = match.span(0)[1]
|
||||
|
||||
r = r'<th>Open</th><td>([0-9,\.]+)</td>'
|
||||
match = re.compile(r, flags=re.DOTALL).search(text, start)
|
||||
if match:
|
||||
tick[Datacode.OPEN] = self.save_wrapper(
|
||||
lambda: float(html.unescape(match.group(1)).replace(',', '').strip()))
|
||||
|
||||
r = r'<th>High</th><td>([0-9,\.]+)</td>'
|
||||
match = re.compile(r, flags=re.DOTALL).search(text, start)
|
||||
if match:
|
||||
tick[Datacode.HIGH] = self.save_wrapper(
|
||||
lambda: float(html.unescape(match.group(1)).replace(',', '').strip()))
|
||||
|
||||
r = r'<th>Low</th><td>([0-9,\.]+)</td>'
|
||||
match = re.compile(r, flags=re.DOTALL).search(text, start)
|
||||
if match:
|
||||
tick[Datacode.LOW] = self.save_wrapper(
|
||||
lambda: float(html.unescape(match.group(1)).replace(',', '').strip()))
|
||||
|
||||
r = r'<th>\s*Bid\s*</th><td>([0-9,\.]+)</td>'
|
||||
match = re.compile(r, flags=re.DOTALL).search(text, start)
|
||||
if match:
|
||||
tick[Datacode.BID] = self.save_wrapper(
|
||||
lambda: float(html.unescape(match.group(1)).replace(',', '').strip()))
|
||||
|
||||
r = r'<th>\s*Offer\s*</th><td>([0-9,\.]+)</td>'
|
||||
match = re.compile(r, flags=re.DOTALL).search(text, start)
|
||||
if match:
|
||||
tick[Datacode.ASK] = self.save_wrapper(
|
||||
lambda: float(html.unescape(match.group(1)).replace(',', '').strip()))
|
||||
|
||||
r = r'<th>\s*Previous close\s*</th><td>\s*([0-9,\.]+)\s*</td>'
|
||||
match = re.compile(r, flags=re.DOTALL).search(text, start)
|
||||
if match:
|
||||
tick[Datacode.PREV_CLOSE] = self.save_wrapper(
|
||||
lambda: float(html.unescape(match.group(1)).replace(',', '').strip()))
|
||||
|
||||
r = r'<th>\s*Average volume\s*</th><td>\s*([0-9,\.btnmk]+)\s*</td>'
|
||||
match = re.compile(r, flags=re.DOTALL).search(text, start)
|
||||
if match:
|
||||
tick[Datacode.AVG_DAILY_VOL_3MONTH] = self.save_wrapper(
|
||||
lambda: handle_abbreviations(html.unescape(match.group(1))))
|
||||
|
||||
r = r'<th>\s*Shares outstanding\s*</th><td>\s*([0-9,\.btnmk]+)\s*</td>'
|
||||
match = re.compile(r, flags=re.DOTALL).search(text, start)
|
||||
if match:
|
||||
tick[Datacode.SHARES_OUT] = self.save_wrapper(
|
||||
lambda: handle_abbreviations(html.unescape(match.group(1))))
|
||||
|
||||
r = r'<th>\s*Free float\s*</th><td>\s*([0-9,\.btnmk]+)\s*</td>'
|
||||
match = re.compile(r, flags=re.DOTALL).search(text, start)
|
||||
if match:
|
||||
tick[Datacode.FREE_FLOAT] = self.save_wrapper(
|
||||
lambda: handle_abbreviations(html.unescape(match.group(1))))
|
||||
|
||||
r = r'<th>\s*P/E.*?</th><td>\s*([0-9,\.\-]+)\s*<'
|
||||
match = re.compile(r, flags=re.DOTALL).search(text, start)
|
||||
if match:
|
||||
value = html.unescape(match.group(1))
|
||||
if value == '--':
|
||||
tick[Datacode.PE_RATIO] = 0.0
|
||||
else:
|
||||
tick[Datacode.PE_RATIO] = self.save_wrapper(
|
||||
lambda: float(value))
|
||||
|
||||
r = r'<th>\s*Market cap\s*</th><td>\s*([0-9,\.btnmk]+)\s*<'
|
||||
match = re.compile(r, flags=re.DOTALL).search(text, start)
|
||||
if match:
|
||||
tick[Datacode.MARKET_CAP] = self.save_wrapper(
|
||||
lambda: handle_abbreviations(html.unescape(match.group(1))))
|
||||
|
||||
r = r'<th>\s*EPS.*?</th><td>\s*([0-9,\.\-]+)\s*<'
|
||||
match = re.compile(r, flags=re.DOTALL).search(text, start)
|
||||
if match:
|
||||
tick[Datacode.EPS] = self.save_wrapper(
|
||||
lambda: float(html.unescape(match.group(1))))
|
||||
|
||||
r = r'<th>\s*Annual div.*?</th><td>\s*([0-9,\.]+)\s*<'
|
||||
match = re.compile(r, flags=re.DOTALL).search(text, start)
|
||||
if match:
|
||||
tick[Datacode.DIV] = self.save_wrapper(
|
||||
lambda: float(html.unescape(match.group(1))))
|
||||
|
||||
r = r'<th>\s*Annual div yield.*?</th><td>\s*([0-9,\.]+)%\s*<'
|
||||
match = re.compile(r, flags=re.DOTALL).search(text, start)
|
||||
if match:
|
||||
tick[Datacode.DIV_YIELD] = self.save_wrapper(
|
||||
lambda: float(html.unescape(match.group(1))))
|
||||
|
||||
r = r'<th>\s*Div ex-date\s*</th><td><span[^>]*>(.*?)</span><'
|
||||
match = re.compile(r, flags=re.DOTALL).search(text, start)
|
||||
if match:
|
||||
try:
|
||||
value = html.unescape(match.group(1)).strip()
|
||||
dt = dateutil.parser.parse(value, yearfirst=True, dayfirst=False, tzinfos=whois_timezone_info)
|
||||
tick[Datacode.EX_DIV_DATE] = dt.date()
|
||||
except BaseException:
|
||||
pass
|
||||
|
||||
except BaseException as e:
|
||||
logger.exception("BaseException ticker=%s datacode=%s", ticker, datacode)
|
||||
del self.realtime[ticker]
|
||||
return f'FT.getRealtime({ticker}, {datacode}) - process: {str(e)}'
|
||||
|
||||
logger.debug(tick)
|
||||
|
||||
return self._return_value(self.realtime[ticker], datacode)
|
||||
|
||||
def getHistoric(self, ticker, datacode, date):
|
||||
return 'FT.getHistoric: Historic Data not implemented.'
|
||||
|
||||
def guess_asset_class(self, ticker):
|
||||
|
||||
if len(ticker) == 6:
|
||||
if ticker[0:2] in ['USD', 'EUR', 'GBP', 'JPY', 'CHF']:
|
||||
return 'currencies'
|
||||
if ticker[3:5] in ['USD', 'EUR', 'GBP', 'JPY', 'CHF']:
|
||||
return 'currencies'
|
||||
|
||||
colon_count = ticker.count(':')
|
||||
|
||||
if colon_count == 0:
|
||||
return 'funds'
|
||||
elif colon_count == 3:
|
||||
return 'etfs'
|
||||
|
||||
return 'equities'
|
||||
|
||||
|
||||
def createInstance(ctx):
|
||||
return FT(ctx)
|
||||
@@ -0,0 +1,467 @@
|
||||
# financials_yahoo.py
|
||||
#
|
||||
# license: GNU LGPL
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 3 of the License, or (at your option) any later version.
|
||||
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pytz
|
||||
import re
|
||||
import time
|
||||
import urllib.parse
|
||||
|
||||
import dateutil.parser
|
||||
|
||||
from baseclient import BaseClient, HttpException
|
||||
from datacode import Datacode
|
||||
from naivehtmlparser import NaiveHTMLParser
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
def default(obj, prop, fallback=''):
|
||||
try:
|
||||
if obj is None or property is None:
|
||||
return fallback
|
||||
|
||||
v = None
|
||||
if hasattr(obj, prop):
|
||||
v = getattr(obj, prop)
|
||||
elif prop in obj:
|
||||
v = obj[prop]
|
||||
return v if v is not None else fallback
|
||||
except:
|
||||
pass
|
||||
return fallback
|
||||
|
||||
|
||||
def handle_abbreviations(s):
|
||||
s = str(s).strip()
|
||||
if s.endswith('M'):
|
||||
return float(s[:-1]) * 1000000
|
||||
elif s.endswith('B'):
|
||||
return float(s[:-1]) * 1000000000
|
||||
elif s.endswith('T'):
|
||||
return float(s[:-1]) * 1000000000000
|
||||
return float(s)
|
||||
|
||||
|
||||
class Yahoo(BaseClient):
|
||||
def __init__(self, ctx):
|
||||
super().__init__()
|
||||
|
||||
self.crumb = None
|
||||
self.realtime = {}
|
||||
self.historicdata = {}
|
||||
|
||||
def _read_ticker_json_file(self, ticker):
|
||||
|
||||
fn = os.path.join(self.basedir, 'yahoo-hist-{}.json'.format(ticker))
|
||||
|
||||
if not os.path.isfile(fn):
|
||||
return
|
||||
|
||||
with open(fn, newline='', encoding="utf-8") as jsonfile:
|
||||
js = jsonfile.read()
|
||||
|
||||
parsed = json.loads(js)
|
||||
parsed = parsed['chart']['result'][0]
|
||||
|
||||
price_hint = 2
|
||||
if 'priceHint' in parsed['meta']:
|
||||
price_hint = str(parsed['meta']['priceHint'])
|
||||
if price_hint and price_hint.isnumeric():
|
||||
price_hint = int(price_hint)
|
||||
else:
|
||||
price_hint = 2
|
||||
|
||||
tz = datetime.timezone(datetime.timedelta(seconds=parsed['meta']['gmtoffset']), parsed['meta']['exchangeTimezoneName'])
|
||||
|
||||
rows = list(
|
||||
zip((datetime.datetime.fromtimestamp(ts, tz).date() for ts in parsed['timestamp']),
|
||||
parsed['indicators']['quote'][0]['open'],
|
||||
parsed['indicators']['quote'][0]['low'],
|
||||
parsed['indicators']['quote'][0]['high'],
|
||||
parsed['indicators']['quote'][0]['volume'],
|
||||
parsed['indicators']['quote'][0]['close'],
|
||||
parsed['indicators']['adjclose'][0]['adjclose']))
|
||||
|
||||
ticks = {}
|
||||
|
||||
for row in rows:
|
||||
tick = self.get_ticker()
|
||||
try:
|
||||
tick[Datacode.OPEN] = round(float(row[1]), price_hint)
|
||||
tick[Datacode.LOW] = round(float(row[2]), price_hint)
|
||||
tick[Datacode.HIGH] = round(float(row[3]), price_hint)
|
||||
tick[Datacode.VOLUME] = round(float(row[4]), price_hint)
|
||||
tick[Datacode.CLOSE] = round(float(row[5]), price_hint)
|
||||
tick[Datacode.ADJ_CLOSE] = round(float(row[6]), price_hint)
|
||||
except:
|
||||
pass
|
||||
|
||||
if len(tick) > 0:
|
||||
ticks[str(row[0])] = tick # Date
|
||||
|
||||
self.historicdata[ticker] = ticks
|
||||
|
||||
|
||||
def handleCookiesAndConsent(self, url, ticker, datacode, html_file):
|
||||
|
||||
try:
|
||||
text = self.urlopen(url)
|
||||
except BaseException as e:
|
||||
logger.exception("BaseException (1) ticker=%s datacode=%s last_url=%s redirect_count=%s %s",
|
||||
ticker, datacode, self.last_url, self.redirect_count, e)
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(os.path.join(self.basedir, html_file), "w", encoding="utf-8") as text_file:
|
||||
print(f"<!-- '{self.last_url}' -->\r\n\r\n{text}", file=text_file)
|
||||
except BaseException as e:
|
||||
logger.exception("BaseException (2) ticker=%s datacode=%s %s", ticker, datacode, e)
|
||||
|
||||
if not text:
|
||||
return None
|
||||
|
||||
try:
|
||||
parser = NaiveHTMLParser()
|
||||
root = parser.feed(text)
|
||||
parser.close()
|
||||
except BaseException as e:
|
||||
logger.exception("BaseException (3) ticker=%s datacode=%s - HTML parsing - %s", ticker, datacode, e)
|
||||
return None
|
||||
|
||||
form = root.find(f".//form[@class='consent-form']")
|
||||
|
||||
if form:
|
||||
inputs = form.findall(f".//input")
|
||||
|
||||
if inputs:
|
||||
|
||||
data = {'reject': 'reject'}
|
||||
for d in inputs:
|
||||
if 'name' in d.attrib and 'value' in d.attrib:
|
||||
data[d.attrib['name']] = d.attrib['value']
|
||||
|
||||
try:
|
||||
text = self.urlopen(self.last_url, data=data)
|
||||
except BaseException as e:
|
||||
logger.exception("BaseException (4) ticker=%s datacode=%s last_url=%s redirect_count=%s %s",
|
||||
ticker, datacode, self.last_url, self.redirect_count, e)
|
||||
|
||||
try:
|
||||
with open(os.path.join(self.basedir, html_file), "w", encoding="utf-8") as text_file:
|
||||
print(f"<!-- '{self.last_url}' (after consent handling) -->\r\n\r\n{text}", file=text_file)
|
||||
except BaseException as e:
|
||||
logger.exception("BaseException (5) ticker=%s datacode=%s %s", ticker, datacode, e)
|
||||
|
||||
return text
|
||||
|
||||
def fetch_crumb(self):
|
||||
"""Refreshes the crumb using the current session."""
|
||||
try:
|
||||
# Grab cookies
|
||||
self.session.get("https://finance.yahoo.com/quote/SPY", timeout=10)
|
||||
|
||||
# Request crumb endpoint
|
||||
response = self.session.get("https://query1.finance.yahoo.com/v1/test/getcrumb", timeout=10)
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.text.strip()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to fetch crumb: {e}")
|
||||
return None
|
||||
|
||||
def getRealtime(self, ticker, datacode):
|
||||
|
||||
"""
|
||||
Retrieve realtime data for ticker from Yahoo Finance and cache it for further lookups
|
||||
|
||||
:param ticker: the ticker symbol e.g. VOD.L
|
||||
:param datacode: the requested datacode
|
||||
:return:
|
||||
"""
|
||||
|
||||
# remove white space
|
||||
ticker = "".join(ticker.split())
|
||||
|
||||
# use cached value for up to 60 seconds
|
||||
if ticker in self.realtime:
|
||||
tick = self.realtime[ticker]
|
||||
if Datacode.TIMESTAMP in tick and type(tick[Datacode.TIMESTAMP]) == float and time.time() - 60 < tick[Datacode.TIMESTAMP]:
|
||||
return self._return_value(tick, datacode)
|
||||
else:
|
||||
del self.realtime[ticker]
|
||||
|
||||
if ticker not in self.realtime:
|
||||
self.realtime[ticker] = self.get_ticker()
|
||||
|
||||
tick = self.realtime[ticker]
|
||||
|
||||
if not self.crumb:
|
||||
new_crumb = self.fetch_crumb()
|
||||
if new_crumb:
|
||||
self.crumb = new_crumb
|
||||
logger.debug(f"Crumb successfully set: {self.crumb}")
|
||||
else:
|
||||
del self.realtime[ticker]
|
||||
return f'Yahoo.getRealtime({ticker}, {datacode}) - failed to fetch crumb'
|
||||
|
||||
if not self.crumb:
|
||||
return 'Yahoo.getRealtime({}, {}) - crumb missing'.format(ticker, datacode)
|
||||
|
||||
try:
|
||||
|
||||
url = 'https://query1.finance.yahoo.com/v10/finance/quoteSummary/{}?formatted=true&' \
|
||||
'modules=summaryProfile,financialData,quoteType,recommendationTrend,earnings,equityPerformance,summaryDetail,defaultKeyStatistics,calendarEvents,esgScores,price,pageViews,financialsTemplate&' \
|
||||
'lang=en-US®ion=US&crumb={}' \
|
||||
.format(ticker, urllib.parse.quote_plus(self.crumb))
|
||||
|
||||
js = self.urlopen(url)
|
||||
|
||||
except HttpException as e:
|
||||
logger.exception("HttpException querying ticker=%s datacode=%s", ticker, datacode)
|
||||
del self.realtime[ticker]
|
||||
return None
|
||||
|
||||
try:
|
||||
|
||||
with open(os.path.join(self.basedir, 'yahoo-{}.json'.format(ticker)), "w", encoding="utf-8") as json_file:
|
||||
print(f"<!-- '{self.last_url}' -->\r\n\r\n{js}", file=json_file)
|
||||
|
||||
parsed = json.loads(js)
|
||||
parsed = parsed['quoteSummary']['result'][0]
|
||||
|
||||
summaryDetail = dict()
|
||||
if 'summaryDetail' in parsed:
|
||||
summaryDetail = dict(sorted(parsed['summaryDetail'].items()))
|
||||
|
||||
price = dict(sorted(parsed['price'].items()))
|
||||
|
||||
if 'defaultKeyStatistics' in parsed:
|
||||
defaultKeyStatistics = dict(sorted(parsed['defaultKeyStatistics'].items()))
|
||||
else:
|
||||
defaultKeyStatistics = {}
|
||||
|
||||
if 'summaryProfile' in parsed:
|
||||
summaryProfile = dict(sorted(parsed['summaryProfile'].items()))
|
||||
else:
|
||||
summaryProfile = {}
|
||||
|
||||
quoteType = dict(sorted(parsed['quoteType'].items()))
|
||||
|
||||
except BaseException as e:
|
||||
logger.exception("BaseException parsing ticker=%s datacode=%s", ticker, datacode)
|
||||
del self.realtime[ticker]
|
||||
return 'Yahoo.getRealtimeSummary({}, {}) - exception: {}'.format(ticker, datacode, e)
|
||||
|
||||
try:
|
||||
|
||||
tick[Datacode.TICKER] = ticker
|
||||
tick[Datacode.TIMESTAMP] = time.time()
|
||||
|
||||
if 'regularMarketPrice' not in price:
|
||||
return None
|
||||
|
||||
tick[Datacode.PREV_CLOSE] = self.save_wrapper(lambda: float(price['regularMarketPreviousClose']['raw']))
|
||||
tick[Datacode.OPEN] = self.save_wrapper(lambda: float(price['regularMarketOpen']['raw']))
|
||||
tick[Datacode.CHANGE] = self.save_wrapper(lambda: float(price['regularMarketChange']['raw']))
|
||||
tick[Datacode.CHANGE_IN_PERCENT] = self.save_wrapper(lambda: float(price['regularMarketChangePercent']['raw']))
|
||||
|
||||
tick[Datacode.LOW] = self.save_wrapper(lambda: float(price['regularMarketDayLow']['raw']))
|
||||
tick[Datacode.HIGH] = self.save_wrapper(lambda: float(price['regularMarketDayHigh']['raw']))
|
||||
|
||||
tick[Datacode.LAST_PRICE] = self.save_wrapper(lambda: float(price['regularMarketPrice']['raw']))
|
||||
tick[Datacode.VOLUME] = self.save_wrapper(lambda: float(price['regularMarketVolume']['raw']))
|
||||
tick[Datacode.AVG_DAILY_VOL_3MONTH] = self.save_wrapper(lambda: float(price['averageDailyVolume3Month']['raw']))
|
||||
tick[Datacode.BETA] = self.save_wrapper(lambda: float(defaultKeyStatistics['beta']['raw']))
|
||||
tick[Datacode.EPS] = self.save_wrapper(lambda: float(defaultKeyStatistics['trailingEps']['raw']))
|
||||
tick[Datacode.PE_RATIO] = self.save_wrapper(lambda: float(summaryDetail['trailingPE']['raw']))
|
||||
|
||||
tick[Datacode.EX_DIV_DATE] = self.save_wrapper(
|
||||
lambda: dateutil.parser.parse(summaryDetail['exDividendDate']['fmt'], yearfirst=True, dayfirst=False).date())
|
||||
|
||||
tick[Datacode.LOW_52_WEEK] = self.save_wrapper(lambda: float(summaryDetail['fiftyTwoWeekLow']['raw']))
|
||||
tick[Datacode.HIGH_52_WEEK] = self.save_wrapper(lambda: float(summaryDetail['fiftyTwoWeekHigh']['raw']))
|
||||
|
||||
tick[Datacode.MARKET_CAP] = self.save_wrapper(lambda: float(price['marketCap']['raw']))
|
||||
|
||||
tick[Datacode.BID] = self.save_wrapper(lambda: float(summaryDetail['bid']['raw']))
|
||||
tick[Datacode.BIDSIZE] = self.save_wrapper(lambda: float(summaryDetail['bidSize']['raw']))
|
||||
|
||||
tick[Datacode.ASK] = self.save_wrapper(lambda: float(summaryDetail['ask']['raw']))
|
||||
tick[Datacode.ASKSIZE] = self.save_wrapper(lambda: float(summaryDetail['askSize']['raw']))
|
||||
|
||||
if quoteType:
|
||||
t = int(price['regularMarketTime'])
|
||||
tz = pytz.timezone(quoteType['timeZoneFullName'])
|
||||
|
||||
tick[Datacode.TIMEZONE] = tz
|
||||
dt = datetime.datetime.fromtimestamp(t, tz)
|
||||
|
||||
tick[Datacode.LAST_PRICE_DATE] = dt.date()
|
||||
tick[Datacode.LAST_PRICE_TIME] = dt.time()
|
||||
|
||||
tick[Datacode.EXCHANGE] = self.save_wrapper(lambda: price['exchangeName'])
|
||||
tick[Datacode.CURRENCY] = self.save_wrapper(lambda: price['currency'])
|
||||
|
||||
tick[Datacode.DIV] = self.save_wrapper(lambda: float(summaryDetail['dividendRate']['raw']))
|
||||
tick[Datacode.DIV_YIELD] = self.save_wrapper(lambda: float(summaryDetail['dividendYield']['raw']))
|
||||
|
||||
# fallback to last dividend on mutual funds and ETFs
|
||||
if not tick[Datacode.DIV]:
|
||||
tick[Datacode.DIV] = self.save_wrapper(lambda: float(defaultKeyStatistics['lastDividendValue']['raw']))
|
||||
|
||||
if default(price, 'quoteType') == 'FUTURE':
|
||||
tick[Datacode.TICKER] = self.save_wrapper(lambda: price['underlyingSymbol'])
|
||||
tick[Datacode.NAME] = self.save_wrapper(lambda: price['shortName'])
|
||||
tick[Datacode.SETTLEMENT_DATE] = self.save_wrapper(
|
||||
lambda: dateutil.parser.parse(summaryDetail['expireDate']['fmt'], yearfirst=True, dayfirst=False).date())
|
||||
else:
|
||||
tick[Datacode.NAME] = self.save_wrapper(lambda: price['longName'])
|
||||
tick[Datacode.EXPIRY_DATE] = self.save_wrapper(
|
||||
lambda: dateutil.parser.parse(summaryDetail['expireDate']['fmt'], yearfirst=True, dayfirst=False).date())
|
||||
tick[Datacode.SETTLEMENT_DATE] = None
|
||||
|
||||
if not tick[Datacode.NAME]:
|
||||
tick[Datacode.NAME] = tick[Datacode.TICKER]
|
||||
|
||||
tick[Datacode.SECTOR] = self.save_wrapper(lambda: summaryProfile['sector'])
|
||||
tick[Datacode.INDUSTRY] = self.save_wrapper(lambda: summaryProfile['industry'])
|
||||
|
||||
tick[Datacode.SHARES_OUT] = self.save_wrapper(lambda: float(defaultKeyStatistics['sharesOutstanding']['raw']))
|
||||
tick[Datacode.FREE_FLOAT] = self.save_wrapper(lambda: float(defaultKeyStatistics['floatShares']['raw']))
|
||||
tick[Datacode.PAYOUT_RATIO] = self.save_wrapper(lambda: float(summaryDetail['payoutRatio']['raw']))
|
||||
|
||||
except BaseException as e:
|
||||
logger.exception("BaseException ticker=%s datacode=%s", ticker, datacode)
|
||||
del self.realtime[ticker]
|
||||
return 'Yahoo.getRealtime({}, {}) - process: {}'.format(ticker, datacode, e)
|
||||
|
||||
return self._return_value(self.realtime[ticker], datacode)
|
||||
|
||||
def getHistoric(self, ticker: str, datacode: int, date):
|
||||
|
||||
"""
|
||||
Retrieve historic data for ticker from Yahoo Finance and cache it for further lookups
|
||||
|
||||
:param ticker: the ticker symbol e.g. VOD.L
|
||||
:param datacode: the requested datacode
|
||||
:param date: the requested date
|
||||
:return:
|
||||
"""
|
||||
|
||||
# remove white space
|
||||
ticker = "".join(ticker.split())
|
||||
min_tick_date = None
|
||||
|
||||
# dividend and splits will change past adjusted prices
|
||||
# the moment we are asked for ADJ_CLOSE we ignore the ticker cache to refresh
|
||||
|
||||
if Datacode.ADJ_CLOSE != datacode and ticker not in self.historicdata:
|
||||
self._read_ticker_json_file(ticker)
|
||||
|
||||
try:
|
||||
date_as_dt = dateutil.parser.parse(date, yearfirst=True, dayfirst=False)
|
||||
except BaseException as e:
|
||||
logger.exception("BaseException ticker=%s datacode=%s last_url=%s redirect_count=%s", ticker, datacode, self.last_url, self.redirect_count)
|
||||
return 'Yahoo.getHistoric({}, {}, {}) - date_as_dt: {}'.format(ticker, datacode, date, e)
|
||||
|
||||
if ticker in self.historicdata:
|
||||
ticks = self.historicdata[ticker]
|
||||
|
||||
if date in ticks:
|
||||
return self._return_value(ticks[date], datacode)
|
||||
|
||||
# weekend, trading holiday or as yet un-fetched
|
||||
if min(ticks) <= date <= max(ticks):
|
||||
return 'Not a trading day \'{}\''.format(date)
|
||||
|
||||
# (potentially) future date
|
||||
if date > max(ticks):
|
||||
t1 = int(date_as_dt.timestamp())
|
||||
t2 = int(time.time())
|
||||
if t1 > t2:
|
||||
return 'Future date \'{}\''.format(date)
|
||||
|
||||
min_tick_date = int(dateutil.parser.parse(min(ticks), yearfirst=True, dayfirst=False).timestamp()) # remember current earliest date
|
||||
|
||||
if not self.crumb:
|
||||
self.getRealtime(ticker, Datacode.LAST_PRICE)
|
||||
|
||||
if not self.crumb:
|
||||
return 'Yahoo.getHistoric({}, {}, {}) - crumb missing'.format(ticker, datacode, date)
|
||||
|
||||
try:
|
||||
t1 = int(date_as_dt.timestamp())
|
||||
t2 = int(time.time())
|
||||
|
||||
if min_tick_date:
|
||||
t1 = min_tick_date
|
||||
|
||||
if t1 >= t2:
|
||||
return 'Future date \'{}\''.format(date)
|
||||
|
||||
if t1 < int(dateutil.parser.parse('2000-01-01', yearfirst=True, dayfirst=False).timestamp()):
|
||||
return 'Date before 2000 \'{}\''.format(date)
|
||||
|
||||
t1 = t1 - 2682000 # pad with extra month
|
||||
|
||||
except BaseException as e:
|
||||
logger.exception("BaseException ticker=%s datacode=%s", ticker, datacode)
|
||||
return 'Yahoo.getHistoric({}, {}, {}) - date: {}'.format(ticker, datacode, date, e)
|
||||
|
||||
try:
|
||||
|
||||
url = 'https://query1.finance.yahoo.com/v8/finance/chart/{}' \
|
||||
'?period1={}&period2={}&interval=1d&events=history&crumb={}' \
|
||||
.format(ticker, t1, t2, urllib.parse.quote_plus(self.crumb))
|
||||
|
||||
text = self.urlopen(url)
|
||||
|
||||
with open(os.path.join(self.basedir, 'yahoo-hist-{}.json'.format(ticker)), "w", encoding="utf-8") as csv_file:
|
||||
print(text, file=csv_file)
|
||||
|
||||
self._read_ticker_json_file(ticker)
|
||||
|
||||
except HttpException:
|
||||
logger.exception("HttpException ticker=%s datacode=%s date=%s", ticker, datacode, date)
|
||||
return None
|
||||
|
||||
except BaseException as e:
|
||||
logger.exception("BaseException ticker=%s datacode=%s date=%s", ticker, datacode, date)
|
||||
return 'Yahoo.getHistoric({}, {}, {}) - urlopen: {}'.format(ticker, datacode, date, e)
|
||||
|
||||
try:
|
||||
if ticker in self.historicdata:
|
||||
ticks = self.historicdata[ticker]
|
||||
|
||||
if date in ticks:
|
||||
return self._return_value(ticks[date], datacode)
|
||||
|
||||
# future date
|
||||
if date > max(ticks):
|
||||
return 'Future date \'{}\''.format(date)
|
||||
|
||||
# weekend or trading holiday
|
||||
return 'Not a trading day \'{}\''.format(date)
|
||||
|
||||
except BaseException as e:
|
||||
logger.exception("BaseException ticker=%s datacode=%s", ticker, datacode)
|
||||
return 'Yahoo.getHistoric({}, {}, {}) - process: {}'.format(ticker, datacode, date, e)
|
||||
|
||||
return None
|
||||
|
||||
def createInstance(ctx):
|
||||
return Yahoo(ctx)
|
||||
+24
-14
@@ -10,15 +10,17 @@
|
||||
#
|
||||
|
||||
import os
|
||||
|
||||
cur_dir = os.getcwd()
|
||||
|
||||
addin_id = "com.financials.getinfo"
|
||||
addin_version = "0.0.3"
|
||||
addin_version = "3.8.2"
|
||||
addin_displayname = "Financial Market Extension"
|
||||
addin_publisher_link = "https://github.com/cmallwitz/Financials-Extension"
|
||||
addin_publisher_name = "The Publisher"
|
||||
|
||||
print ("Generating extension files for version", addin_version)
|
||||
print("Generating extension files for version", addin_version)
|
||||
|
||||
|
||||
################################################################################
|
||||
# description.xml
|
||||
@@ -35,15 +37,17 @@ desc_xml.write(' <l:LibreOffice-minimal-version value="5.0" d:name="LibreOffi
|
||||
desc_xml.write('</dependencies> \n')
|
||||
desc_xml.write('\n')
|
||||
desc_xml.write('<identifier value="' + addin_id + '" /> \n')
|
||||
desc_xml.write('<version value="' + addin_version + '" />\n')
|
||||
desc_xml.write('<version value="' + addin_version + '" />\n')
|
||||
desc_xml.write('<display-name><name lang="en">' + addin_displayname + '</name></display-name>\n')
|
||||
desc_xml.write('<publisher><name xlink:href="' + addin_publisher_link + '" lang="en">' + addin_publisher_name + '</name></publisher>\n')
|
||||
desc_xml.write(
|
||||
'<publisher><name xlink:href="' + addin_publisher_link + '" lang="en">' + addin_publisher_name + '</name></publisher>\n')
|
||||
desc_xml.write('<extension-description><src xlink:href="description-en-US.txt" lang="en" /></extension-description>')
|
||||
desc_xml.write('\n \n')
|
||||
desc_xml.write('</description> \n')
|
||||
|
||||
desc_xml.close()
|
||||
|
||||
|
||||
################################################################################
|
||||
# manifest.xml
|
||||
# List of files in package and their types.
|
||||
@@ -53,9 +57,10 @@ def add_manifest_entry(xml_file, file_type, file_name):
|
||||
xml_file.write('<manifest:file-entry manifest:media-type="application/vnd.sun.star.' + file_type + '" \n')
|
||||
xml_file.write(' manifest:full-path="' + file_name + '"/> \n')
|
||||
|
||||
|
||||
manifest_xml = open(cur_dir + '/build/META-INF/manifest.xml', 'w')
|
||||
|
||||
manifest_xml.write('<manifest:manifest>\n');
|
||||
manifest_xml.write('<manifest:manifest>\n')
|
||||
add_manifest_entry(manifest_xml, 'uno-typelibrary;type=RDB', 'XFinancials.rdb')
|
||||
add_manifest_entry(manifest_xml, 'configuration-data', 'Financials.xcu')
|
||||
add_manifest_entry(manifest_xml, 'uno-component;type=Python', 'financials.py')
|
||||
@@ -63,6 +68,7 @@ manifest_xml.write('</manifest:manifest> \n')
|
||||
|
||||
manifest_xml.close()
|
||||
|
||||
|
||||
################################################################################
|
||||
# Financials.xcu
|
||||
|
||||
@@ -71,12 +77,13 @@ def define_function(xml_file, function_name, description, parameters):
|
||||
xml_file.write(' <prop oor:name="DisplayName"><value xml:lang="en">' + function_name + '</value></prop>\n')
|
||||
xml_file.write(' <prop oor:name="Description"><value xml:lang="en">' + description + '</value></prop>\n')
|
||||
xml_file.write(' <prop oor:name="Category"><value>Add-In</value></prop>\n')
|
||||
xml_file.write(' <prop oor:name="CompatibilityName"><value xml:lang="en">AutoAddIn.Financials.' + function_name + '</value></prop>\n')
|
||||
xml_file.write(
|
||||
' <prop oor:name="CompatibilityName"><value xml:lang="en">AutoAddIn.Financials.' + function_name + '</value></prop>\n')
|
||||
xml_file.write(' <node oor:name="Parameters">\n')
|
||||
|
||||
for p, desc in parameters:
|
||||
# Optional parameters will have a displayname enclosed in square brackets.
|
||||
p_name = p.strip("[]")
|
||||
# Optional parameters will have a display name enclosed in square brackets.
|
||||
p_name = p.strip("[]")
|
||||
xml_file.write(' <node oor:name="' + p_name + '" oor:op="replace">\n')
|
||||
xml_file.write(' <prop oor:name="DisplayName"><value xml:lang="en">' + p_name + '</value></prop>\n')
|
||||
xml_file.write(' <prop oor:name="Description"><value xml:lang="en">' + desc + '</value></prop>\n')
|
||||
@@ -85,6 +92,7 @@ def define_function(xml_file, function_name, description, parameters):
|
||||
xml_file.write(' </node>\n')
|
||||
xml_file.write(' </node>\n')
|
||||
|
||||
|
||||
# instance_id references the named UNO component instantiated by Python code (that is my understanding at least).
|
||||
implementation_name = "com.financials.getinfo.python.FinancialsImpl"
|
||||
|
||||
@@ -94,17 +102,19 @@ excel_addin_name = "Financials.xlam"
|
||||
financials_xml = open(cur_dir + '/build/Financials.xcu', 'w')
|
||||
|
||||
financials_xml.write('<?xml version="1.0" encoding="UTF-8"?>\n')
|
||||
financials_xml.write('<oor:component-data xmlns:oor="http://openoffice.org/2001/registry" xmlns:xs="http://www.w3.org/2001/XMLSchema" oor:name="CalcAddIns" oor:package="org.openoffice.Office">\n')
|
||||
financials_xml.write(
|
||||
'<oor:component-data xmlns:oor="http://openoffice.org/2001/registry" xmlns:xs="http://www.w3.org/2001/XMLSchema" oor:name="CalcAddIns" oor:package="org.openoffice.Office">\n')
|
||||
financials_xml.write('<node oor:name="AddInInfo">\n')
|
||||
financials_xml.write('<node oor:name="' + implementation_name + '" oor:op="replace">\n')
|
||||
financials_xml.write('<node oor:name="AddInFunctions">\n')
|
||||
|
||||
define_function(financials_xml, \
|
||||
'getRealtime', 'Fetches Realtime Financial Data.', \
|
||||
define_function(financials_xml,
|
||||
'getRealtime', 'Fetches Realtime Financial Data.',
|
||||
[('ticker', 'The ticker symbol.'), ('datacode', 'The data code.'), ('source', 'The source.')])
|
||||
define_function(financials_xml, \
|
||||
'getHistoric', 'Fetches Historic Financial Data.', \
|
||||
[('ticker', 'The ticker symbol.'), ('datacode', 'The data code.'), ('date', 'The date.'), ('source', 'The source.')])
|
||||
define_function(financials_xml,
|
||||
'getHistoric', 'Fetches Historic Financial Data.',
|
||||
[('ticker', 'The ticker symbol.'), ('datacode', 'The data code.'), ('date', 'The date.'),
|
||||
('source', 'The source.')])
|
||||
|
||||
financials_xml.write('</node>\n')
|
||||
financials_xml.write('</node>\n')
|
||||
|
||||
-168
@@ -1,168 +0,0 @@
|
||||
# google.py
|
||||
#
|
||||
# license: GNU LGPL
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 3 of the License, or (at your option) any later version.
|
||||
|
||||
|
||||
import datetime
|
||||
import locale
|
||||
import html
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
|
||||
import urllib.parse
|
||||
|
||||
from datacode import Datacode
|
||||
from baseclient import BaseClient
|
||||
|
||||
|
||||
def log(str):
|
||||
# print(str, file=sys.stderr)
|
||||
pass
|
||||
|
||||
# TODO migrate to:
|
||||
# https://www.google.com/search?q=NYSE:IBM&tbm=fin
|
||||
# https://www.google.com/search?q=NASDAQ:INTC&tbm=fin
|
||||
# https://www.google.com/search?q=LON:VOD&tbm=fin
|
||||
# https://www.google.com/search?q=EURGBP
|
||||
# https://www.google.com/search?q=INDEXSP:.INX
|
||||
|
||||
|
||||
class Google(BaseClient):
|
||||
def __init__(self, ctx):
|
||||
super().__init__()
|
||||
|
||||
self.realtime = {}
|
||||
|
||||
def getRealtime(self, ticker: str, datacode: int):
|
||||
|
||||
"""
|
||||
Retrieve realtime data for ticker from Google Finance and cache it for further lookups
|
||||
|
||||
:param ticker: the ticker symbol e.g. VOD.L or LON:VOD
|
||||
:param datacode: the requested datacode
|
||||
:return:
|
||||
"""
|
||||
|
||||
# remove white space
|
||||
ticker = "".join(ticker.split())
|
||||
|
||||
# use cached value for up to 60 seconds
|
||||
if ticker in self.realtime:
|
||||
tick = self.realtime[ticker]
|
||||
if time.time() - 60 < tick[Datacode.TIMESTAMP]:
|
||||
return self._return_value(tick, datacode)
|
||||
else:
|
||||
del self.realtime[ticker]
|
||||
|
||||
url = 'https://finance.google.com/finance?{}'.format(urllib.parse.urlencode({'q': ticker}))
|
||||
|
||||
try:
|
||||
text = self.urlopen(url)
|
||||
except BaseException as e:
|
||||
log(traceback.format_exc())
|
||||
return 'Google.getRealtime(\'{}\', {}) - read: {}'.format(ticker, datacode, e)
|
||||
|
||||
try:
|
||||
r = '<meta\s*itemprop="([^"]+)"\s*content="([^"]+)"\s*/>'
|
||||
pattern = re.compile(r)
|
||||
result = re.findall(pattern, text)
|
||||
|
||||
if len(result) == 0:
|
||||
return 'Data for \'{}\' not found'.format(ticker)
|
||||
|
||||
if ticker not in self.realtime:
|
||||
self.realtime[ticker] = {}
|
||||
|
||||
tick = self.realtime[ticker]
|
||||
|
||||
for key, value in result:
|
||||
|
||||
if key == 'exchangeTimezone':
|
||||
try:
|
||||
tick[Datacode.TIMEZONE] = str(value)
|
||||
except:
|
||||
pass
|
||||
|
||||
elif key == 'priceChange':
|
||||
try:
|
||||
tick[Datacode.CHANGE] = float(value)
|
||||
except:
|
||||
pass
|
||||
|
||||
elif key == 'quoteTime':
|
||||
try:
|
||||
dt = datetime.datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ")
|
||||
tick[Datacode.LAST_PRICE_DATE] = dt.date()
|
||||
tick[Datacode.LAST_PRICE_TIME] = dt.time()
|
||||
except:
|
||||
pass
|
||||
|
||||
elif key == 'priceChangePercent':
|
||||
try:
|
||||
tick[Datacode.CHANGE_IN_PERCENT] = float(value)
|
||||
except:
|
||||
pass
|
||||
|
||||
elif key == 'price':
|
||||
try:
|
||||
locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
|
||||
tick[Datacode.LAST_PRICE] = locale.atof(str(value))
|
||||
except:
|
||||
pass
|
||||
|
||||
elif key == 'priceCurrency':
|
||||
try:
|
||||
tick[Datacode.CURRENCY] = str(value)
|
||||
except:
|
||||
pass
|
||||
|
||||
elif key == 'priceCurrency':
|
||||
pass
|
||||
|
||||
elif key == 'exchange':
|
||||
try:
|
||||
tick[Datacode.EXCHANGE] = str(value)
|
||||
except:
|
||||
pass
|
||||
|
||||
elif key == 'name':
|
||||
try:
|
||||
tick[Datacode.NAME] = html.unescape(str(value))
|
||||
except:
|
||||
pass
|
||||
|
||||
elif key == 'tickerSymbol':
|
||||
try:
|
||||
tick[Datacode.TICKER] = str(value)
|
||||
except:
|
||||
pass
|
||||
|
||||
else:
|
||||
log('ignored {} {}'.format(key, value))
|
||||
|
||||
tick[Datacode.TIMESTAMP] = time.time()
|
||||
|
||||
if tick[Datacode.EXCHANGE] == 'CURRENCY' and Datacode.CURRENCY not in tick:
|
||||
tick[Datacode.CURRENCY] = ''
|
||||
|
||||
log(tick)
|
||||
|
||||
except BaseException as e:
|
||||
log(traceback.format_exc())
|
||||
return 'Google.getRealtime({}, {}) - process: {}'.format(ticker, datacode, e)
|
||||
|
||||
return self._return_value(self.realtime[ticker], datacode)
|
||||
|
||||
def getHistoric(self, ticker, datacode, date):
|
||||
return 'Google.getHistoric: Historic Data not implemented.'
|
||||
|
||||
|
||||
def createInstance(ctx):
|
||||
return Google(ctx)
|
||||
@@ -1,118 +0,0 @@
|
||||
# jsonParser.py
|
||||
#
|
||||
# Implementation of a simple JSON parser, returning a hierarchical
|
||||
# ParseResults object support both list- and dict-style data access.
|
||||
#
|
||||
# Copyright 2006, by Paul McGuire
|
||||
#
|
||||
# Updated 8 Jan 2007 - fixed dict grouping bug, and made elements and
|
||||
# members optional in array and object collections
|
||||
#
|
||||
json_bnf = """
|
||||
object
|
||||
{ members }
|
||||
{}
|
||||
members
|
||||
string : value
|
||||
members , string : value
|
||||
array
|
||||
[ elements ]
|
||||
[]
|
||||
elements
|
||||
value
|
||||
elements , value
|
||||
value
|
||||
string
|
||||
number
|
||||
object
|
||||
array
|
||||
true
|
||||
false
|
||||
null
|
||||
"""
|
||||
|
||||
from pyparsing import *
|
||||
|
||||
TRUE = Keyword("true").setParseAction(replaceWith(True))
|
||||
FALSE = Keyword("false").setParseAction(replaceWith(False))
|
||||
NULL = Keyword("null").setParseAction(replaceWith(None))
|
||||
|
||||
jsonString = dblQuotedString.setParseAction(removeQuotes)
|
||||
jsonNumber = Combine(Optional('-') + ('0' | Word('123456789', nums)) +
|
||||
Optional('.' + Word(nums)) +
|
||||
Optional(Word('eE', exact=1) + Word(nums + '+-', nums)))
|
||||
|
||||
jsonObject = Forward()
|
||||
jsonValue = Forward()
|
||||
jsonElements = delimitedList(jsonValue)
|
||||
jsonArray = Group(Suppress('[') + Optional(jsonElements) + Suppress(']'))
|
||||
jsonValue << (jsonString | jsonNumber | Group(jsonObject) | jsonArray | TRUE | FALSE | NULL)
|
||||
memberDef = Group(jsonString + Suppress(':') + jsonValue)
|
||||
jsonMembers = delimitedList(memberDef)
|
||||
jsonObject << Dict(Suppress('{') + Optional(jsonMembers) + Suppress('}'))
|
||||
|
||||
jsonComment = cppStyleComment
|
||||
jsonObject.ignore(jsonComment)
|
||||
|
||||
|
||||
def convertNumbers(s, l, toks):
|
||||
n = toks[0]
|
||||
try:
|
||||
return int(n)
|
||||
except ValueError as ve:
|
||||
return float(n)
|
||||
|
||||
|
||||
jsonNumber.setParseAction(convertNumbers)
|
||||
|
||||
if __name__ == "__main__":
|
||||
testdata = """
|
||||
{
|
||||
"glossary": {
|
||||
"title": "example glossary",
|
||||
"GlossDiv": {
|
||||
"title": "S",
|
||||
"GlossList":
|
||||
{
|
||||
"ID": "SGML",
|
||||
"SortAs": "SGML",
|
||||
"GlossTerm": "Standard Generalized Markup Language",
|
||||
"TrueValue": true,
|
||||
"FalseValue": false,
|
||||
"Gravity": -9.8,
|
||||
"LargestPrimeLessThan100": 97,
|
||||
"AvogadroNumber": 6.02E23,
|
||||
"EvenPrimesGreaterThan2": null,
|
||||
"PrimesLessThan10" : [2,3,5,7],
|
||||
"Acronym": "SGML",
|
||||
"Abbrev": "ISO 8879:1986",
|
||||
"GlossDef": "A meta-markup language, used to create markup languages such as DocBook.",
|
||||
"GlossSeeAlso": ["GML", "XML", "markup"],
|
||||
"EmptyDict" : {},
|
||||
"EmptyList" : []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
import pprint
|
||||
|
||||
results = jsonObject.parseString(testdata)
|
||||
pprint.pprint(results.asList())
|
||||
print
|
||||
|
||||
|
||||
def testPrint(x):
|
||||
print
|
||||
type(x), repr(x)
|
||||
|
||||
|
||||
print
|
||||
results.glossary.GlossDiv.GlossList.keys()
|
||||
testPrint(results.glossary.title)
|
||||
testPrint(results.glossary.GlossDiv.GlossList.ID)
|
||||
testPrint(results.glossary.GlossDiv.GlossList.FalseValue)
|
||||
testPrint(results.glossary.GlossDiv.GlossList.Acronym)
|
||||
testPrint(results.glossary.GlossDiv.GlossList.EvenPrimesGreaterThan2)
|
||||
testPrint(results.glossary.GlossDiv.GlossList.PrimesLessThan10)
|
||||
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# https://github.com/marmelo/python-htmlparser - revision cbe9633 on 25 Dec 2013
|
||||
# Copyright by Rafael Marmelo
|
||||
|
||||
"""
|
||||
Python 3.x HTMLParser extension with ElementTree support.
|
||||
"""
|
||||
|
||||
from html.parser import HTMLParser
|
||||
from xml.etree import ElementTree
|
||||
|
||||
|
||||
class NaiveHTMLParser(HTMLParser):
|
||||
"""
|
||||
Python 3.x HTMLParser extension with ElementTree support.
|
||||
@see https://github.com/marmelo/python-htmlparser
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.root = None
|
||||
self.tree = []
|
||||
HTMLParser.__init__(self)
|
||||
|
||||
def feed(self, data):
|
||||
HTMLParser.feed(self, data)
|
||||
return self.root
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
if len(self.tree) == 0:
|
||||
element = ElementTree.Element(tag, dict(self.__filter_attrs(attrs)))
|
||||
self.tree.append(element)
|
||||
self.root = element
|
||||
else:
|
||||
element = ElementTree.SubElement(self.tree[-1], tag, dict(self.__filter_attrs(attrs)))
|
||||
self.tree.append(element)
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
self.tree.pop()
|
||||
|
||||
def handle_startendtag(self, tag, attrs):
|
||||
self.handle_starttag(tag, attrs)
|
||||
self.handle_endtag(tag)
|
||||
pass
|
||||
|
||||
def handle_data(self, data):
|
||||
if self.tree:
|
||||
self.tree[-1].text = data
|
||||
|
||||
def get_root_element(self):
|
||||
return self.root
|
||||
|
||||
def __filter_attrs(self, attrs):
|
||||
return filter(lambda x: x[0] and x[1], attrs) if attrs else []
|
||||
|
||||
|
||||
# example usage
|
||||
if __name__ == "__main__":
|
||||
|
||||
html = """
|
||||
<html>
|
||||
<head>
|
||||
<title>GitHub</title>
|
||||
</head>
|
||||
<body>
|
||||
<a href="https://github.com/marmelo">GitHub</a>
|
||||
<a href="https://github.com/marmelo/python-htmlparser">GitHub Project</a>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
parser = NaiveHTMLParser()
|
||||
root = parser.feed(html)
|
||||
parser.close()
|
||||
|
||||
# root is an xml.etree.Element and supports the ElementTree API
|
||||
# (e.g. you may use its limited support for XPath expressions)
|
||||
|
||||
# get title
|
||||
print(root.find('head/title').text)
|
||||
|
||||
# get all anchors
|
||||
for a in root.findall('.//a'):
|
||||
print(a.get('href'))
|
||||
|
||||
# for more information, see:
|
||||
# http://docs.python.org/2/library/xml.etree.elementtree.html
|
||||
# http://docs.python.org/2/library/xml.etree.elementtree.html#xpath-support
|
||||
@@ -0,0 +1,42 @@
|
||||
# test_yahoo.py
|
||||
#
|
||||
# license: GNU LGPL
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 3 of the License, or (at your option) any later version.
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
logging.basicConfig(level=logging.ERROR, format="%(asctime)s %(name)s %(levelname)s %(message)s")
|
||||
|
||||
import financials
|
||||
from datacode import Datacode
|
||||
import testutils
|
||||
|
||||
financials = financials.createInstance(None)
|
||||
|
||||
|
||||
class Test(unittest.TestCase):
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
# this avoids "ResourceWarning: unclosed..." on cached socket connections
|
||||
financials.close()
|
||||
|
||||
def test_currency(self):
|
||||
s = financials.getRealtime('ETH-EUR', Datacode.LAST_PRICE.value, 'COINBASE')
|
||||
self.assertEqual(float, type(s), 'test_currency LAST_PRICE')
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('unittest_args', nargs='*')
|
||||
args = parser.parse_args()
|
||||
unit_argv = [sys.argv[0]] + args.unittest_args
|
||||
unittest.main(argv=unit_argv)
|
||||
+435
@@ -0,0 +1,435 @@
|
||||
# test_yahoo.py
|
||||
#
|
||||
# license: GNU LGPL
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 3 of the License, or (at your option) any later version.
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
logging.basicConfig(level=logging.ERROR, format="%(asctime)s %(name)s %(levelname)s %(message)s")
|
||||
|
||||
import financials
|
||||
from datacode import Datacode
|
||||
import testutils
|
||||
|
||||
financials = financials.createInstance(None)
|
||||
|
||||
|
||||
class Test(unittest.TestCase):
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
# this avoids "ResourceWarning: unclosed..." on cached socket connections
|
||||
financials.close()
|
||||
|
||||
def test_currency(self):
|
||||
s = financials.getRealtime('EURGBP', 'LAST_PRICE', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_currency LAST_PRICE')
|
||||
|
||||
s = financials.getRealtime('EURGBP', 'CURRENCY', 'FT')
|
||||
self.assertEqual(str, type(s), 'test_currency CURRENCY')
|
||||
|
||||
def test_US_equity(self):
|
||||
s = financials.getRealtime('INTC:NSQ', 'CHANGE', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_US_equity CHANGE {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('INTC:NSQ', 'CHANGE_IN_PERCENT', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_US_equity CHANGE_IN_PERCENT {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('INTC:NSQ', 'AVG_DAILY_VOL_3MONTH', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_US_equity AVG_DAILY_VOL_3MONTH {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('INTC:NSQ', 'MARKET_CAP', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_US_equity MARKET_CAP {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM:NYQ', 'TICKER', 'FT')
|
||||
self.assertEqual(str, type(s), 'test_US_equity TICKER {}'.format(s))
|
||||
self.assertEqual('IBM:NYQ', s, 'test_US_equity TICKER {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM:NYQ', 'PREV_CLOSE', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_US_equity PREV_CLOSE {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM:NYQ', 'OPEN', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_US_equity OPEN {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM:NYQ', 'LAST_PRICE', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_US_equity LAST_PRICE {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM:NYQ', 'LOW', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_US_equity LOW {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM:NYQ', 'HIGH', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_US_equity HIGH {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM:NYQ', 'VOLUME', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_US_equity VOLUME {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM:NYQ', 'BETA', 'FT')
|
||||
self.assertTrue(testutils.is_positive_float(s), 'test_US_equity BETA {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM:NYQ', 'EPS', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_US_equity EPS {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM:NYQ', 'SHARES_OUT', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_US_equity SHARES_OUT {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM:NYQ', 'FREE_FLOAT', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_US_equity FREE_FLOAT {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM:NYQ', 'PE_RATIO', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_US_equity PE_RATIO {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM:NYQ', 'DIV', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_US_equity DIV {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM:NYQ', 'DIV_YIELD', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_US_equity DIV_YIELD {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM:NYQ', 'EX_DIV_DATE', 'FT')
|
||||
self.assertEqual(str, type(s), 'test_US_equity EX_DIV_DATE {}'.format(s))
|
||||
self.assertTrue(testutils.is_date(s), 'test_US_equity EX_DIV_DATE {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM:NYQ', 'PAYOUT_RATIO', 'FT')
|
||||
self.assertIsNone(s, 'test_US_equity PAYOUT_RATIO {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM:NYQ', 'NAME', 'FT')
|
||||
self.assertEqual(str, type(s), 'test_US_equity NAME {}'.format(s))
|
||||
self.assertEqual(s, 'International Business Machines Corp', 'test_US_equity NAME {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM:NYQ', 'SECTOR', 'FT')
|
||||
self.assertEqual(str, type(s), 'test_US_equity SECTOR {}'.format(s))
|
||||
self.assertEqual(s, 'Technology', 'test_US_equity SECTOR {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM:NYQ', 'INDUSTRY', 'FT')
|
||||
self.assertEqual(str, type(s), 'test_US_equity INDUSTRY {}'.format(s))
|
||||
self.assertEqual(s, 'Technology', 'test_US_equity INDUSTRY {}'.format(s))
|
||||
|
||||
# may fail (s is None) on weekends when date/time displayed doesn't have time component with TZ
|
||||
s = financials.getRealtime('IBM:NYQ', 'TIMEZONE', 'FT')
|
||||
self.assertEqual(str, type(s), 'test_US_equity TIMEZONE {}'.format(s))
|
||||
|
||||
def test_US_mutuals(self):
|
||||
s = financials.getRealtime('VGSLX', 'LAST_PRICE', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_US_mutuals LAST_PRICE {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('VGSLX', 'NAME', 'FT')
|
||||
self.assertEqual("Vanguard Real Estate Index Fund Admiral Shares", s, 'test_US_mutuals NAME {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('VGSLX', 'CURRENCY', 'FT')
|
||||
self.assertEqual('USD', s, 'test_US_mutuals CURRENCY {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('VGSLX', 'CHANGE', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_US_mutuals CHANGE {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('VGSLX', 'CHANGE_IN_PERCENT', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_US_mutuals CHANGE_IN_PERCENT {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('VFIAX', 'LAST_PRICE', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_US_mutuals LAST_PRICE {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('VFIAX', 'LAST_PRICE_DATE', 'FT')
|
||||
self.assertEqual(str, type(s), 'test_US_mutuals LAST_PRICE_DATE {}'.format(s))
|
||||
self.assertTrue(testutils.is_date(s), 'test_US_mutuals LAST_PRICE_DATE {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('VFIAX', 'LAST_PRICE_TIME', 'FT')
|
||||
self.assertEqual(str, type(s), 'test_US_mutuals LAST_PRICE_TIME {}'.format(s))
|
||||
|
||||
def test_US_futures(self):
|
||||
|
||||
# https://markets.ft.com/data/commodities/tearsheet/summary?s=823439664 ESH26:IOM - EMINI S&P MAR26
|
||||
|
||||
s = financials.getRealtime('823439664', Datacode.NAME.value, 'FT')
|
||||
self.assertEqual(str, type(s), 'test_realtime_US_futures NAME {}'.format(s))
|
||||
self.assertEqual('EMINI S&P MAR26', s, 'test_US_futures NAME {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('823439664', Datacode.LAST_PRICE.value, 'FT')
|
||||
self.assertEqual(float, type(s), 'test_US_futures LAST_PRICE {}'.format(s))
|
||||
|
||||
# s = financials.getRealtime('775326843', Datacode.OPEN.value, 'FT')
|
||||
# self.assertEqual(float, type(s), 'test_US_futures OPEN {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('823439664', Datacode.VOLUME.value, 'FT')
|
||||
self.assertEqual(float, type(s), 'test_US_futures VOLUME {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('823439664', Datacode.LOW_52_WEEK.value, 'FT')
|
||||
self.assertEqual(float, type(s), 'test_US_futures LOW_52_WEEK {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('823439664', Datacode.HIGH_52_WEEK.value, 'FT')
|
||||
self.assertEqual(float, type(s), 'test_US_futures HIGH_52_WEEK {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('823439664', Datacode.CHANGE.value, 'FT')
|
||||
self.assertEqual(float, type(s), 'test_US_futures CHANGE {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('823439664', Datacode.CHANGE_IN_PERCENT.value, 'FT')
|
||||
self.assertEqual(float, type(s), 'test_US_futures CHANGE_IN_PERCENT {}'.format(s))
|
||||
|
||||
def test_UK_ETF(self):
|
||||
s = financials.getRealtime('CSP1:LSE:GBX', 'NAME', 'FT')
|
||||
self.assertEqual(str, type(s), 'test_UK_ETF NAME {}'.format(s))
|
||||
self.assertEqual('iShares Core S&P 500 UCITS ETF USD (Acc)', s, 'test_UK_ETF NAME {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('VERX:LSE:GBP', 'LAST_PRICE', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_UK_ETF LAST_PRICE {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('VERX:LSE:GBP', 'TIMEZONE', 'FT')
|
||||
self.assertEqual(str, type(s), 'test_UK_ETF TIMEZONE {}'.format(s))
|
||||
|
||||
def test_UK_equity(self):
|
||||
s = financials.getRealtime('VOD:LSE', 'NAME', 'FT')
|
||||
self.assertEqual(str, type(s), 'test_UK_equity NAME {}'.format(s))
|
||||
self.assertEqual('Vodafone Group PLC', s, 'test_UK_ETF NAME {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('VOD:LSE', 'BID', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_UK_equity BID {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('VOD:LSE', 'ASK', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_UK_equity ASK {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('VOD:LSE', 'LAST_PRICE', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_UK_equity LAST_PRICE {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('VOD:LSE', 'TIMEZONE', 'FT')
|
||||
self.assertEqual(str, type(s), 'test_UK_equity TIMEZONE {}'.format(s))
|
||||
|
||||
def test_DE_equity(self):
|
||||
s = financials.getRealtime('SAPX:GER', 'NAME', 'FT')
|
||||
self.assertEqual('SAP SE', s, 'test_DE_equity NAME {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('SAPX:GER', 'TICKER', 'FT')
|
||||
self.assertEqual('SAPX:GER', s, 'test_DE_equity TICKER {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('SAPX:GER', 'CURRENCY', 'FT')
|
||||
self.assertEqual('EUR', s, 'test_DE_equity CURRENCY {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('SAPX:GER', 'LAST_PRICE', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_DE_equity LAST_PRICE {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('SAPX:GER', 'CHANGE', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_DE_equity CHANGE {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('SAPX:GER', 'CHANGE_IN_PERCENT', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_DE_equity CHANGE_IN_PERCENT {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('SAPX:GER', 'VOLUME', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_DE_equity VOLUME {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('SAPX:GER', 'OPEN', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_DE_equity OPEN {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('SAPX:GER', 'HIGH', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_DE_equity HIGH {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('SAPX:GER', 'LOW', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_DE_equity LOW {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('SAPX:GER', 'PREV_CLOSE', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_DE_equity PREV_CLOSE {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('SAPX:GER', 'MARKET_CAP', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_DE_equity MARKET_CAP {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('SAPX:GER', 'SECTOR', 'FT')
|
||||
self.assertEqual(str, type(s), 'test_DE_equity SECTOR {}'.format(s))
|
||||
self.assertEqual(s, 'Technology', 'test_DE_equity SECTOR {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('SAPX:GER', 'INDUSTRY', 'FT')
|
||||
self.assertEqual(str, type(s), 'test_DE_equity INDUSTRY {}'.format(s))
|
||||
self.assertEqual('Technology', s, 'test_DE_equity INDUSTRY {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('SAPX:GER', 'LAST_PRICE_DATE', 'FT')
|
||||
self.assertEqual(str, type(s), 'test_DE_equity LAST_PRICE_DATE {}'.format(s))
|
||||
self.assertTrue(testutils.is_date(s), 'test_DE_equity LAST_PRICE_DATE {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('SAPX:GER', 'TIMEZONE', 'FT')
|
||||
self.assertEqual(str, type(s), 'test_DE_equity TIMEZONE {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('SAPX:GER', 'BETA', 'FT')
|
||||
self.assertTrue(testutils.is_positive_float(s), 'test_DE_equity BETA {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('SAPX:GER', 'EPS', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_DE_equity EPS {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('SAPX:GER', 'SHARES_OUT', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_DE_equity SHARES_OUT {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('SAPX:GER', 'FREE_FLOAT', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_DE_equity FREE_FLOAT {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('SAPX:GER', 'PE_RATIO', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_DE_equity PE_RATIO {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('SAPX:GER', 'DIV', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_DE_equity DIV {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('SAPX:GER', 'DIV_YIELD', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_DE_equity DIV_YIELD {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('SAPX:GER', 'EX_DIV_DATE', 'FT')
|
||||
self.assertEqual(str, type(s), 'test_DE_equity EX_DIV_DATE {}'.format(s))
|
||||
self.assertTrue(testutils.is_date(s), 'test_DE_equity EX_DIV_DATE {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('ISHAX:GER', 'NAME', 'FT')
|
||||
self.assertEqual('Intershop Communications AG', s, 'test_DE_equity NAME {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('ISHAX:GER', 'BETA', 'FT')
|
||||
self.assertTrue(testutils.is_positive_float(s), 'test_DE_equity BETA {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('ISHAX:GER', 'MARKET_CAP', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_DE_equity MARKET_CAP {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('ISHAX:GER', 'EPS', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_DE_equity EPS {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('ISHAX:GER', 'PE_RATIO', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_DE_equity PE_RATIO {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('ISHAX:GER', 'DIV', 'FT')
|
||||
self.assertIsNone(s, 'test_DE_equity DIV {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('ISHAX:GER', 'DIV_YIELD', 'FT')
|
||||
self.assertIsNone(s, 'test_DE_equity DIV_YIELD {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('ISHAX:GER', 'EX_DIV_DATE', 'FT')
|
||||
self.assertIsNone(s, 'test_DE_equity EX_DIV_DATE {}'.format(s))
|
||||
|
||||
def test_DK_equity(self):
|
||||
s = financials.getRealtime('NOVO B:CPH', 'name', 'FT')
|
||||
self.assertEqual('Novo Nordisk A/S', s, 'test_DK_equity NAME {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('NOVO B:CPH', 'currency', 'FT')
|
||||
self.assertEqual('DKK', s, 'test_DK_equity CURRENCY {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('NOVO B:CPH', 'industry', 'FT')
|
||||
self.assertEqual(str, type(s), 'test_DK_equity INDUSTRY {}'.format(s))
|
||||
self.assertEqual('Pharmaceuticals and Biotechnology', s, 'test_DK_equity INDUSTRY {}'.format(s))
|
||||
|
||||
def test_SE_equity(self):
|
||||
s = financials.getRealtime('ACRI A:STO', 'name', 'FT')
|
||||
self.assertEqual('Acrinova AB (publ)', s, 'test_SE_equity NAME {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('SE0015660014', 'name', 'FT')
|
||||
self.assertEqual('Acrinova AB (publ)', s, 'test_SE_equity NAME {}'.format(s))
|
||||
|
||||
def test_TY_equity(self):
|
||||
s = financials.getRealtime('6503:TYO', 'OPEN', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_TY_equity OPEN {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('6503:TYO', 'LOW', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_TY_equity LOW {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('6503:TYO', 'HIGH', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_TY_equity HIGH {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('6503:TYO', 'LOW_52_WEEK', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_TY_equity LOW_52_WEEK {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('6503:TYO', 'HIGH_52_WEEK', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_TY_equity HIGH_52_WEEK {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('6503:TYO', 'MARKET_CAP', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_TY_equity MARKET_CAP {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('6503:TYO', 'VOLUME', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_TY_equity VOLUME {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('6503:TYO', 'CURRENCY', 'FT')
|
||||
self.assertEqual('JPY', s, 'test_TY_equity CURRENCY')
|
||||
|
||||
s = financials.getRealtime('6503:TYO', 'SECTOR', 'FT')
|
||||
self.assertEqual(str, type(s), 'test_TY_equity SECTOR {}'.format(s))
|
||||
self.assertEqual('Industrials', s, 'test_TY_equity SECTOR {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('6503:TYO', 'INDUSTRY', 'FT')
|
||||
self.assertEqual(str, type(s), 'test_TY_equity INDUSTRY {}'.format(s))
|
||||
self.assertEqual('General Industrials', s, 'test_TY_equity INDUSTRY {}'.format(s))
|
||||
|
||||
def test_TLV_equity(self):
|
||||
s = financials.getRealtime('LUMI:TLV', 'LAST_PRICE', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_TLV_equity LAST_PRICE {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('LUMI:TLV', 'OPEN', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_TLV_equity OPEN {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('LUMI:TLV', 'LOW', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_TLV_equity LOW {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('LUMI:TLV', 'HIGH', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_TLV_equity HIGH {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('LUMI:TLV', 'LOW_52_WEEK', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_TLV_equity LOW_52_WEEK {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('LUMI:TLV', 'HIGH_52_WEEK', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_TLV_equity HIGH_52_WEEK {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('LUMI:TLV', 'MARKET_CAP', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_TLV_equity MARKET_CAP {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('LUMI:TLV', 'VOLUME', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_TLV_equity VOLUME {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('LUMI:TLV', 'CURRENCY', 'FT')
|
||||
self.assertEqual('ILa', s, 'test_TLV_equity CURRENCY')
|
||||
|
||||
s = financials.getRealtime('LUMI:TLV', 'SECTOR', 'FT')
|
||||
self.assertEqual(str, type(s), 'test_TLV_equity SECTOR {}'.format(s))
|
||||
self.assertEqual('Financials', s, 'test_TY_equity SECTOR {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('LUMI:TLV', 'INDUSTRY', 'FT')
|
||||
self.assertEqual(str, type(s), 'test_TLV_equity INDUSTRY {}'.format(s))
|
||||
self.assertEqual('Banks', s, 'test_TY_equity INDUSTRY {}'.format(s))
|
||||
|
||||
def test_index(self):
|
||||
s = financials.getRealtime('INX:IOM', 'ticker', 'FT')
|
||||
self.assertEqual('INX:IOM', s, 'test_index TICKER {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('DAXX:GER', 'ticker', 'FT')
|
||||
self.assertEqual('DAXX:GER', s, 'test_index TICKER {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('DAXX:GER', 'last_price', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_index LAST_PRICE {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('DAXX:GER', 'volume', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_DE_equity VOLUME {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('DAXX:GER', 'low_52_week', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_DE_equity LOW_52_WEEK {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('DAXX:GER', 'high_52_week', 'FT')
|
||||
self.assertEqual(float, type(s), 'test_DE_equity HIGH_52_WEEK {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('DAXX:GER', 'open', 'FT')
|
||||
self.assertIsNone(s, 'test_DE_equity OPEN {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('DAXX:GER', 'high', 'FT')
|
||||
self.assertIsNone(s, 'test_DE_equity HIGH {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('DAXX:GER', 'low', 'FT')
|
||||
self.assertIsNone(s, 'test_DE_equity LOW {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('DAXX:GER', 'prev_close', 'FT')
|
||||
self.assertIsNone(s, 'test_DE_equity PREV_CLOSE {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('DAXX:GER', 'market_cap', 'FT')
|
||||
self.assertIsNone(s, 'test_DE_equity MARKET_CAP {}'.format(s))
|
||||
|
||||
def test_errors(self):
|
||||
s = financials.getRealtime('NO_NAME', 'LAST_PRICE', 'FT')
|
||||
self.assertIsNone(s, 'test_errors LAST_PRICE {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('DAXX:GER', 'Foo', 'FT')
|
||||
self.assertEqual('Datacode is invalid', s, 'test_errors')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('unittest_args', nargs='*')
|
||||
args = parser.parse_args()
|
||||
unit_argv = [sys.argv[0]] + args.unittest_args
|
||||
unittest.main(argv=unit_argv)
|
||||
@@ -1,209 +0,0 @@
|
||||
# test_google.py
|
||||
#
|
||||
# license: GNU LGPL
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 3 of the License, or (at your option) any later version.
|
||||
|
||||
import unittest
|
||||
|
||||
import financials
|
||||
from datacode import Datacode
|
||||
|
||||
financials = financials.createInstance(None)
|
||||
|
||||
|
||||
class TestGoogle(unittest.TestCase):
|
||||
|
||||
def test_currency(self):
|
||||
s = financials.getRealtime('EURGBP', Datacode.LAST_PRICE.value, 'GOOGLE')
|
||||
self.assertEqual(type(s), float, 'test_currency LAST_PRICE')
|
||||
|
||||
s = financials.getRealtime('EURGBP', Datacode.CURRENCY.value, 'GOOGLE')
|
||||
self.assertEqual(type(s), str, 'test_currency CURRENCY')
|
||||
self.assertEqual(s, '', 'test_currency CURRENCY')
|
||||
|
||||
def test_UK_equity(self):
|
||||
s = financials.getRealtime('EURGBP', Datacode.LAST_PRICE.value, 'GOOGLE')
|
||||
self.assertEqual(type(s), float, 'test_UK_equity LAST_PRICE')
|
||||
|
||||
s = financials.getRealtime('LON:VOD', Datacode.LAST_PRICE.value, 'GOOGLE')
|
||||
self.assertEqual(type(s), float, 'test_UK_equity LAST_PRICE')
|
||||
|
||||
s = financials.getRealtime('VOD.L', Datacode.LAST_PRICE.value, 'GOOGLE')
|
||||
self.assertEqual(type(s), float, 'test_UK_equity LAST_PRICE')
|
||||
|
||||
s = financials.getRealtime('VOD.L', Datacode.TICKER.value, 'GOOGLE')
|
||||
self.assertEqual(s, 'VOD', 'test_UK_equity TICKER')
|
||||
|
||||
s = financials.getRealtime('VOD.L', Datacode.NAME.value, 'GOOGLE')
|
||||
self.assertEqual(type(s), str, 'test_UK_equity NAME')
|
||||
|
||||
def test_UK_ETF(self):
|
||||
s = financials.getRealtime('LON:CSP1', Datacode.LAST_PRICE.value, 'GOOGLE')
|
||||
self.assertEqual(type(s), float, 'test_UK_ETF LAST_PRICE')
|
||||
|
||||
s = financials.getRealtime('LON:CSP1', Datacode.CURRENCY.value, 'GOOGLE')
|
||||
self.assertEqual(s, 'GBX', 'test_UK_ETF CURRENCY')
|
||||
|
||||
s = financials.getRealtime('LON:FTAL', Datacode.LAST_PRICE.value, 'GOOGLE')
|
||||
self.assertEqual(type(s), float, 'test_UK_ETF LAST_PRICE')
|
||||
|
||||
s = financials.getRealtime('LON:FTAL', Datacode.CURRENCY.value, 'GOOGLE')
|
||||
self.assertEqual(s, 'GBP', 'test_UK_ETF CURRENCY')
|
||||
|
||||
s = financials.getRealtime('LON:FTAL', Datacode.NAME.value, 'GOOGLE')
|
||||
self.assertEqual(type(s), str, 'test_UK_ETF NAME')
|
||||
|
||||
def test_DE_equity(self):
|
||||
s = financials.getRealtime('FRA:SAP', 7, 'GOOGLE')
|
||||
self.assertEqual(type(s), float, 'test_DE_equity 7')
|
||||
|
||||
s = financials.getRealtime('FRA:SAP', '7', 'GOOGLE')
|
||||
self.assertEqual(type(s), float, 'test_DE_equity \'7\'')
|
||||
|
||||
s = financials.getRealtime('FRA:SAP', 8, 'GOOGLE')
|
||||
self.assertEqual(type(s), str, 'test_DE_equity 8')
|
||||
|
||||
s = financials.getRealtime('FRA:SAP', 8.1, 'GOOGLE')
|
||||
self.assertEqual(type(s), str, 'test_DE_equity 8.1')
|
||||
|
||||
s = financials.getRealtime('FRA:SAP', '8.1', 'GOOGLE')
|
||||
self.assertEqual(type(s), str, 'test_DE_equity \'8.1\'')
|
||||
|
||||
s = financials.getRealtime('FRA:SAP', 10, 'GOOGLE')
|
||||
self.assertEqual(type(s), str, 'test_DE_equity 10')
|
||||
|
||||
s = financials.getRealtime('FRA:SAP', '11', 'GOOGLE')
|
||||
self.assertEqual(type(s), float, 'test_DE_equity \'11\'')
|
||||
|
||||
s = financials.getRealtime('FRA:SAP', '21', 'GOOGLE')
|
||||
self.assertEqual(type(s), float, 'test_DE_equity \'21\'')
|
||||
|
||||
s = financials.getRealtime('FRA:SAP', Datacode.TIMEZONE.value, 'GOOGLE')
|
||||
self.assertEqual(s, 'Europe/Berlin', 'test_DE_equity TIMEZONE')
|
||||
|
||||
def test_DE_ETF(self):
|
||||
s = financials.getRealtime('FRA:C060', Datacode.LAST_PRICE.value, 'GOOGLE')
|
||||
self.assertEqual(type(s), float, 'test_DE_ETF LAST_PRICE')
|
||||
|
||||
s = financials.getRealtime('FRA:C060', Datacode.CURRENCY.value, 'GOOGLE')
|
||||
self.assertEqual(s, 'EUR', 'test_DE_ETF CURRENCY')
|
||||
|
||||
s = financials.getRealtime('C060.de', Datacode.LAST_PRICE.value, 'GOOGLE')
|
||||
self.assertEqual(type(s), float, 'test_DE_ETF LAST_PRICE')
|
||||
|
||||
s = financials.getRealtime('C060.de', Datacode.TICKER.value, 'GOOGLE')
|
||||
self.assertEqual(s, 'C060', 'test_DE_ETF CURRENCY')
|
||||
|
||||
s = financials.getRealtime('C060.de', Datacode.EXCHANGE.value, 'GOOGLE')
|
||||
self.assertEqual(s, 'FRA', 'test_DE_ETF CURRENCY')
|
||||
|
||||
s = financials.getRealtime('C060.de', Datacode.CURRENCY.value, 'GOOGLE')
|
||||
self.assertEqual(s, 'EUR', 'test_DE_ETF CURRENCY')
|
||||
|
||||
def test_US_equity(self):
|
||||
s = financials.getRealtime(' NASDAQ : AAPL ', Datacode.LAST_PRICE.value, 'GOOGLE')
|
||||
self.assertEqual(type(s), float, 'test_US_equity LAST_PRICE')
|
||||
|
||||
s = financials.getRealtime(' NASDAQ : AAPL ', Datacode.TICKER.value, 'GOOGLE')
|
||||
self.assertEqual(s, 'AAPL', 'test_US_equity TICKER')
|
||||
|
||||
s = financials.getRealtime(' NASDAQ : AAPL ', Datacode.EXCHANGE.value, 'GOOGLE')
|
||||
self.assertEqual(s, 'NASDAQ', 'test_US_equity EXCHANGE')
|
||||
|
||||
s = financials.getRealtime(' NASDAQ : AAPL ', Datacode.CURRENCY.value, 'GOOGLE')
|
||||
self.assertEqual(s, 'USD', 'test_US_equity CURRENCY')
|
||||
|
||||
s = financials.getRealtime('NYSE:IBM', Datacode.LAST_PRICE.value, 'GOOGLE')
|
||||
self.assertEqual(type(s), float, 'test_US_equity LAST_PRICE')
|
||||
|
||||
s = financials.getRealtime('NYSE:IBM', Datacode.TICKER.value, 'GOOGLE')
|
||||
self.assertEqual(s, 'IBM', 'test_US_equity TICKER')
|
||||
|
||||
s = financials.getRealtime('NYSE:IBM', Datacode.EXCHANGE.value, 'GOOGLE')
|
||||
self.assertEqual(s, 'NYSE', 'test_US_equity EXCHANGE')
|
||||
|
||||
s = financials.getRealtime('NYSE:IBM', Datacode.CURRENCY.value, 'GOOGLE')
|
||||
self.assertEqual(s, 'USD', 'test_US_equity CURRENCY')
|
||||
|
||||
s = financials.getRealtime('NYSE:IBM', Datacode.NAME.value, 'GOOGLE')
|
||||
self.assertEqual(type(s), str, 'test_US_equity NAME')
|
||||
|
||||
s = financials.getRealtime('NYSE:IBM', Datacode.NAME.value, 'GOOGLE')
|
||||
self.assertEqual(type(s), str, 'test_US_equity NAME')
|
||||
|
||||
s = financials.getRealtime('NYSE:IBM', Datacode.TIMESTAMP.value, 'GOOGLE')
|
||||
self.assertEqual(s, 'Data doesn\'t exist - 999', 'test_US_equity TIMESTAMP')
|
||||
|
||||
s = financials.getRealtime('NYSE:IBM', Datacode.TIMEZONE.value, 'GOOGLE')
|
||||
self.assertEqual(s, 'America/New_York', 'test_US_equity TIMEZONE')
|
||||
|
||||
def test_US_mutuals(self):
|
||||
s = financials.getRealtime('MUTF:VFIAX', Datacode.LAST_PRICE.value, 'GOOGLE')
|
||||
self.assertEqual(type(s), float, 'test_US_mutuals LAST_PRICE')
|
||||
|
||||
s = financials.getRealtime('MUTF:VFIAX', Datacode.CURRENCY.value, 'GOOGLE')
|
||||
self.assertEqual(s, 'USD', 'test_US_mutuals CURRENCY')
|
||||
|
||||
def test_errors(self):
|
||||
s = financials.getRealtime(None, Datacode.LAST_PRICE.value, 'GOOGLE')
|
||||
self.assertEqual(s, 'Ticker is empty', 'test_errors')
|
||||
|
||||
s = financials.getRealtime('NYS:IBM', None, 'GOOGLE')
|
||||
self.assertEqual(s, 'Datacode is empty', 'test_errors')
|
||||
|
||||
s = financials.getRealtime('DOES_NOT_EXISTS', Datacode.LAST_PRICE.value, 'GOOGLE')
|
||||
self.assertEqual(s, 'Data for \'DOES_NOT_EXISTS\' not found', 'test_errors')
|
||||
|
||||
s = financials.getRealtime('NYS:IBM', 'Foo', 'GOOGLE')
|
||||
self.assertEqual(s, 'Datacode is not a number', 'test_errors')
|
||||
|
||||
# Historic data not supported on GOOGLE
|
||||
|
||||
s = financials.getHistoric('NYS:IBM', Datacode.LAST_PRICE.value, '2017-01-01', 'GOOGLE')
|
||||
self.assertEqual(s, 'getHistoric: Source \'GOOGLE\' not supported', 'test_errors')
|
||||
|
||||
def test_errors_cell_range_passed(self):
|
||||
cell_range = ((1, 2), ('3', '4'), (5.0, 6.0))
|
||||
|
||||
s = financials.getRealtime(cell_range, Datacode.LAST_PRICE.value, 'GOOGLE')
|
||||
self.assertEqual(s, 'Cell range not allowed for ticker', 'test_errors')
|
||||
|
||||
s = financials.getRealtime('NYS:IBM', cell_range, 'GOOGLE')
|
||||
self.assertEqual(s, 'Cell range not allowed for datacode', 'test_errors')
|
||||
|
||||
s = financials.getRealtime('NYS:IBM', Datacode.LAST_PRICE.value, cell_range)
|
||||
self.assertEqual(s, 'Cell range not allowed for source', 'test_errors')
|
||||
|
||||
def test_support(self):
|
||||
cell_range = ((1, 2), ('3', '4'), (5.0, 6.0))
|
||||
|
||||
s = financials.getRealtime('SUPPORT')
|
||||
self.assertTrue(s.startswith("ctx="), 'test_errors SUPPORT {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('SUPPORT', 1)
|
||||
self.assertTrue(s.startswith("ctx="), 'test_errors SUPPORT {}'.format(s))
|
||||
self.assertTrue("type(datacode)=<class 'int'>" in s, 'test_errors SUPPORT {}'.format(s))
|
||||
self.assertTrue("str(datacode)=1" in s, 'test_errors SUPPORT {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('SUPPORT', 1.0)
|
||||
self.assertTrue(s.startswith("ctx="), 'test_errors SUPPORT {}'.format(s))
|
||||
self.assertTrue("type(datacode)=<class 'float'>" in s, 'test_errors SUPPORT {}'.format(s))
|
||||
self.assertTrue("str(datacode)=1.0" in s, 'test_errors SUPPORT {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('SUPPORT', '1')
|
||||
self.assertTrue(s.startswith("ctx="), 'test_errors SUPPORT {}'.format(s))
|
||||
self.assertTrue("type(datacode)=<class 'str'>" in s, 'test_errors SUPPORT {}'.format(s))
|
||||
self.assertTrue("str(datacode)=1" in s, 'test_errors SUPPORT {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('SUPPORT', cell_range)
|
||||
self.assertTrue(s.startswith("ctx="), 'test_errors SUPPORT {}'.format(s))
|
||||
self.assertTrue("type(datacode)=<class 'tuple'>" in s, 'test_errors SUPPORT {}'.format(s))
|
||||
self.assertTrue("str(datacode)=((1, 2), ('3', '4'), (5.0, 6.0))" in s, 'test_errors SUPPORT {}'.format(s))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,57 @@
|
||||
# test_support.py
|
||||
#
|
||||
# license: GNU LGPL
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 3 of the License, or (at your option) any later version.
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
logging.basicConfig(level=logging.ERROR, format="%(asctime)s %(name)s %(levelname)s %(message)s")
|
||||
|
||||
import financials
|
||||
|
||||
financials = financials.createInstance(None)
|
||||
|
||||
|
||||
class Test(unittest.TestCase):
|
||||
|
||||
def test_support(self):
|
||||
cell_range = ((1, 2), ('3', '4'), (5.0, 6.0))
|
||||
|
||||
s = financials.getRealtime('SUPPORT')
|
||||
self.assertTrue(s.startswith("ctx="), 'test_errors SUPPORT {}'.format(s))
|
||||
self.assertTrue("version=3.0.0" in s, 'test_errors SUPPORT {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('SUPPORT', 1)
|
||||
self.assertTrue(s.startswith("ctx="), 'test_errors SUPPORT {}'.format(s))
|
||||
self.assertTrue("type(datacode)=<class 'int'>" in s, 'test_errors SUPPORT {}'.format(s))
|
||||
self.assertTrue("str(datacode)=1" in s, 'test_errors SUPPORT {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('SUPPORT', 1.0)
|
||||
self.assertTrue(s.startswith("ctx="), 'test_errors SUPPORT {}'.format(s))
|
||||
self.assertTrue("type(datacode)=<class 'float'>" in s, 'test_errors SUPPORT {}'.format(s))
|
||||
self.assertTrue("str(datacode)=1.0" in s, 'test_errors SUPPORT {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('SUPPORT', '1')
|
||||
self.assertTrue(s.startswith("ctx="), 'test_errors SUPPORT {}'.format(s))
|
||||
self.assertTrue("type(datacode)=<class 'str'>" in s, 'test_errors SUPPORT {}'.format(s))
|
||||
self.assertTrue("str(datacode)=1" in s, 'test_errors SUPPORT {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('SUPPORT', cell_range)
|
||||
self.assertTrue(s.startswith("ctx="), 'test_errors SUPPORT {}'.format(s))
|
||||
self.assertTrue("type(datacode)=<class 'tuple'>" in s, 'test_errors SUPPORT {}'.format(s))
|
||||
self.assertTrue("str(datacode)=((1, 2), ('3', '4'), (5.0, 6.0))" in s, 'test_errors SUPPORT {}'.format(s))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('unittest_args', nargs='*')
|
||||
args = parser.parse_args()
|
||||
unit_argv = [sys.argv[0]] + args.unittest_args
|
||||
unittest.main(argv=unit_argv)
|
||||
+386
-61
@@ -7,103 +7,414 @@
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 3 of the License, or (at your option) any later version.
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
logging.basicConfig(level=logging.ERROR, format="%(asctime)s %(name)s %(levelname)s %(message)s")
|
||||
|
||||
import baseclient
|
||||
import financials
|
||||
from datacode import Datacode
|
||||
import testutils
|
||||
|
||||
financials = financials.createInstance(None)
|
||||
|
||||
|
||||
class TestYahoo(unittest.TestCase):
|
||||
def urlopen_fail(self, url, data=None):
|
||||
raise baseclient.HttpException(url, 'ERROR: simulated urlopen() failed')
|
||||
|
||||
|
||||
class Test(unittest.TestCase):
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
# this avoids "ResourceWarning: unclosed..." on cached socket connections
|
||||
financials.close()
|
||||
|
||||
def test_recovery_from_urlopen_error_issue(self):
|
||||
|
||||
financials.yahoo.last_url = 'blank'
|
||||
|
||||
urlopen_saved = baseclient.BaseClient.urlopen
|
||||
baseclient.BaseClient.urlopen = urlopen_fail
|
||||
|
||||
s = financials.getRealtime('U1IH.F', Datacode.LAST_PRICE.value, 'YAHOO')
|
||||
|
||||
baseclient.BaseClient.urlopen = urlopen_saved
|
||||
|
||||
s = financials.getRealtime('U1IH.F', Datacode.PREV_CLOSE.value, 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_recovery_from_urlopen_error_issue PREV_CLOSE {}'.format(s))
|
||||
|
||||
def test_currency(self):
|
||||
s = financials.getRealtime('EURGBP=X', Datacode.CURRENCY.value, 'YAHOO')
|
||||
self.assertEqual(str, type(s), 'test_currency CURRENCY')
|
||||
|
||||
s = financials.getRealtime('EURGBP=X', Datacode.LAST_PRICE.value, 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_currency LAST_PRICE')
|
||||
|
||||
def test_realtime_US_ZVZZT(self):
|
||||
|
||||
s = financials.getRealtime('ZVZZT', Datacode.PAYOUT_RATIO.value, 'YAHOO')
|
||||
self.assertIsNone(s, 'test_realtime_US_ZVZZT PAYOUT_RATIO {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('ZVZZT', Datacode.SECTOR.value, 'YAHOO')
|
||||
self.assertIsNone(s, 'test_realtime_US_ZVZZT SECTOR {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('ZVZZT', Datacode.LAST_PRICE.value, 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_realtime_US_ZVZZT LAST_PRICE {}'.format(s))
|
||||
|
||||
def test_realtime_US_equity(self):
|
||||
|
||||
s = financials.getRealtime('^GSPC', Datacode.NAME.value, 'YAHOO')
|
||||
self.assertEqual(type(s), str, 'test_realtime_US_equity NAME {}'.format(s))
|
||||
self.assertEqual(str, type(s), 'test_realtime_US_equity NAME {}'.format(s))
|
||||
self.assertIn('500', s, 'test_realtime_US_equity NAME {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM', Datacode.PREV_CLOSE.value, 'YAHOO')
|
||||
self.assertEqual(type(s), float, 'test_realtime_US_equity PREV_CLOSE {}'.format(s))
|
||||
self.assertEqual(float, type(s), 'test_realtime_US_equity PREV_CLOSE {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM', Datacode.OPEN.value, 'YAHOO')
|
||||
self.assertEqual(type(s), float, 'test_realtime_US_equity OPEN {}'.format(s))
|
||||
self.assertEqual(float, type(s), 'test_realtime_US_equity OPEN {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM', Datacode.LAST_PRICE.value, 'YAHOO')
|
||||
self.assertEqual(type(s), float, 'test_realtime_US_equity LAST_PRICE {}'.format(s))
|
||||
self.assertEqual(float, type(s), 'test_realtime_US_equity LAST_PRICE {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM', Datacode.LOW.value, 'YAHOO')
|
||||
self.assertEqual(type(s), float, 'test_realtime_US_equity LOW {}'.format(s))
|
||||
self.assertEqual(float, type(s), 'test_realtime_US_equity LOW {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM', Datacode.HIGH.value, 'YAHOO')
|
||||
self.assertEqual(type(s), float, 'test_realtime_US_equity HIGH {}'.format(s))
|
||||
self.assertEqual(float, type(s), 'test_realtime_US_equity HIGH {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM', Datacode.HIGH_52_WEEK.value, 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_realtime_US_equity HIGH_52_WEEK {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM', Datacode.LOW_52_WEEK.value, 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_realtime_US_equity LOW_52_WEEK {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM', Datacode.MARKET_CAP.value, 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_realtime_US_equity MARKET_CAP {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM', Datacode.VOLUME.value, 'YAHOO')
|
||||
self.assertEqual(type(s), float, 'test_realtime_US_equity VOLUME {}'.format(s))
|
||||
self.assertEqual(float, type(s), 'test_realtime_US_equity VOLUME {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM', Datacode.AVG_DAILY_VOL_3MOMTH.value, 'YAHOO')
|
||||
self.assertEqual(type(s), float, 'test_realtime_US_equity AVG_DAILY_VOL_3MOMTH {}'.format(s))
|
||||
s = financials.getRealtime('IBM', Datacode.AVG_DAILY_VOL_3MONTH.value, 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_realtime_US_equity AVG_DAILY_VOL_3MONTH {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM', Datacode.NAME.value, 'YAHOO')
|
||||
self.assertEqual(type(s), str, 'test_realtime_US_equity NAME {}'.format(s))
|
||||
self.assertEqual(str, type(s), 'test_realtime_US_equity NAME {}'.format(s))
|
||||
self.assertEqual(s, 'International Business Machines Corporation',
|
||||
'test_realtime_US_equity NAME {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM', 'SECTOR', 'YAHOO')
|
||||
self.assertEqual(str, type(s), 'test_realtime_US_equity SECTOR {}'.format(s))
|
||||
self.assertEqual(s, 'Technology', 'test_realtime_US_equity SECTOR {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM', 'INDUSTRY', 'YAHOO')
|
||||
self.assertEqual(str, type(s), 'test_realtime_US_equity INDUSTRY {}'.format(s))
|
||||
self.assertEqual(s, 'Information Technology Services', 'test_realtime_US_equity INDUSTRY {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM', Datacode.TIMEZONE.value, 'YAHOO')
|
||||
self.assertEqual(s, 'America/New_York', 'test_realtime_US_equity TIMEZONE {}'.format(s))
|
||||
self.assertTrue(s == 'America/New_York', 'test_realtime_US_equity TIMEZONE: {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM', Datacode.BETA.value, 'YAHOO')
|
||||
self.assertTrue(testutils.is_positive_float(s), 'test_realtime_US_equity BETA {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM', Datacode.EPS.value, 'YAHOO')
|
||||
self.assertTrue(testutils.is_positive_float(s), 'test_realtime_US_equity EPS {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM', Datacode.PE_RATIO.value, 'YAHOO')
|
||||
self.assertTrue(testutils.is_positive_float(s), 'test_realtime_US_equity PE_RATIO {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM', Datacode.DIV.value, 'YAHOO')
|
||||
self.assertTrue(testutils.is_positive_float(s), 'test_realtime_US_equity DIV {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM', Datacode.DIV_YIELD.value, 'YAHOO')
|
||||
self.assertTrue(testutils.is_positive_float(s), 'test_realtime_US_equity DIV_YIELD {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM', Datacode.EX_DIV_DATE.value, 'YAHOO')
|
||||
self.assertEqual(str, type(s), 'test_realtime_US_equity EX_DIV_DATE {}'.format(s))
|
||||
self.assertTrue(testutils.is_date(s), 'test_realtime_US_equity EX_DIV_DATE {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM', 'PAYOUT_RATIO', 'YAHOO')
|
||||
self.assertTrue(testutils.is_positive_float(s), 'test_realtime_US_equity PAYOUT_RATIO {}'.format(s))
|
||||
# self.assertEqual(None, s, 'test_realtime_US_equity PAYOUT_RATIO {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM', 'SHARES_OUT', 'YAHOO')
|
||||
self.assertTrue(testutils.is_positive_float(s), 'test_realtime_US_equity SHARES_OUT {}'.format(s))
|
||||
# self.assertEqual(None, s, 'test_realtime_US_equity SHARES_OUT {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM', 'FREE_FLOAT', 'YAHOO')
|
||||
self.assertTrue(testutils.is_positive_float(s), 'test_realtime_US_equity FREE_FLOAT {}'.format(s))
|
||||
# self.assertEqual(None, s, 'test_realtime_US_equity FREE_FLOAT {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM', Datacode.EXCHANGE.value, 'YAHOO')
|
||||
self.assertEqual(s, 'NYSE', 'test_realtime_US_equity EXCHANGE')
|
||||
|
||||
def test_realtime_US_mutuals(self):
|
||||
|
||||
s = financials.getRealtime('VGSLX', Datacode.LAST_PRICE.value, 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_realtime_US_mutuals LAST_PRICE {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('VFIAX', Datacode.LAST_PRICE.value, 'YAHOO')
|
||||
self.assertEqual(type(s), float, 'test_realtime_US_mutuals LAST_PRICE {}'.format(s))
|
||||
self.assertEqual(float, type(s), 'test_realtime_US_mutuals LAST_PRICE {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('VFIAX', Datacode.LAST_PRICE_DATE.value, 'YAHOO')
|
||||
self.assertEqual(type(s), str, 'test_realtime_US_mutuals LAST_PRICE_DATE {}'.format(s))
|
||||
s = financials.getRealtime('VFIAX', Datacode.DIV.value, 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_realtime_US_mutuals DIV {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('VFIAX', Datacode.LAST_PRICE_TIME.value, 'YAHOO')
|
||||
self.assertEqual(type(s), str, 'test_realtime_US_mutuals LAST_PRICE_TIME {}'.format(s))
|
||||
s = financials.getRealtime('VFIAX', Datacode.DIV_YIELD.value, 'YAHOO')
|
||||
self.assertIsNone(s, 'test_realtime_US_mutuals DIV_YIELD {}'.format(s)) # no yield
|
||||
|
||||
s = financials.getRealtime('SHRAX', Datacode.DIV.value, 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_realtime_US_mutuals DIV {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('SHRAX', Datacode.DIV_YIELD.value, 'YAHOO')
|
||||
self.assertIsNone(s, 'test_realtime_US_mutuals DIV_YIELD {}'.format(s)) # no yield
|
||||
|
||||
s = financials.getRealtime('VERX.L', Datacode.DIV.value, 'YAHOO')
|
||||
self.assertIsNone(s, 'test_realtime_US_mutuals DIV {}'.format(s)) # no dividend
|
||||
|
||||
s = financials.getRealtime('VERX.L', Datacode.DIV_YIELD.value, 'YAHOO')
|
||||
self.assertIsNone(s, 'test_realtime_US_mutuals DIV_YIELD {}'.format(s)) # no yield
|
||||
|
||||
def test_realtime_US_options(self):
|
||||
|
||||
# symbol from https://finance.yahoo.com/quote/IBM/options?p=IBM
|
||||
|
||||
s = financials.getRealtime('IBM260116C00230000', Datacode.PREV_CLOSE.value, 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_realtime_US_options PREV_CLOSE {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM260116C00230000', Datacode.NAME.value, 'YAHOO')
|
||||
self.assertEqual(str, type(s), 'test_realtime_US_options NAME {}'.format(s))
|
||||
self.assertEqual('IBM Jan 2026 230.000 call', s, 'test_realtime_US_options NAME {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM260116C00230000', Datacode.EXPIRY_DATE.value, 'YAHOO')
|
||||
self.assertEqual(str, type(s), 'test_realtime_US_options EXPIRY_DATE {}'.format(s))
|
||||
self.assertTrue(testutils.is_date(s), 'test_realtime_US_options EXPIRY_DATE {}'.format(s))
|
||||
self.assertEqual("2026-01-16", s, 'test_realtime_US_options EXPIRY_DATE {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM260116C00230000', Datacode.LAST_PRICE.value, 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_realtime_US_options LAST_PRICE {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM260116C00230000', Datacode.OPEN.value, 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_realtime_US_options OPEN {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM260116C00230000', Datacode.VOLUME.value, 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_realtime_US_options VOLUME {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM260116C00230000', Datacode.BID.value, 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_realtime_US_options BID {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM260116C00230000', Datacode.ASK.value, 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_realtime_US_options ASK {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM260116C00230000', Datacode.PAYOUT_RATIO.value, 'YAHOO')
|
||||
self.assertIsNone(s, 'test_realtime_US_options PAYOUT_RATIO {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM260116C00230000', Datacode.SECTOR.value, 'YAHOO')
|
||||
self.assertIsNone(s, 'test_realtime_US_options SECTOR {}'.format(s))
|
||||
|
||||
def test_realtime_US_futures(self):
|
||||
|
||||
s = financials.getRealtime('ES=F', Datacode.NAME.value, 'YAHOO')
|
||||
self.assertEqual(str, type(s), 'test_realtime_US_futures NAME {}'.format(s))
|
||||
self.assertEqual('E-Mini S&P 500 Jun 25', s, 'test_realtime_US_futures NAME {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('ES=F', Datacode.TICKER.value, 'YAHOO')
|
||||
self.assertEqual(str, type(s), 'test_realtime_US_futures TICKER {}'.format(s))
|
||||
self.assertEqual('ESM25.CME', s, 'test_realtime_US_futures TICKER {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('ES=F', Datacode.SETTLEMENT_DATE.value, 'YAHOO')
|
||||
self.assertEqual(str, type(s), 'test_realtime_US_futures SETTLEMENT_DATE {}'.format(s))
|
||||
self.assertTrue(testutils.is_date(s), 'test_realtime_US_futures SETTLEMENT_DATE {}'.format(s))
|
||||
self.assertEqual("2025-06-20", s, 'test_realtime_US_futures SETTLEMENT_DATE {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('ES=F', Datacode.LAST_PRICE.value, 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_realtime_US_futures LAST_PRICE {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('ES=F', Datacode.OPEN.value, 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_realtime_US_futures OPEN {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('ES=F', Datacode.VOLUME.value, 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_realtime_US_futures VOLUME {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('ES=F', Datacode.BID.value, 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_realtime_US_futures BID {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('ES=F', Datacode.ASK.value, 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_realtime_US_futures ASK {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('ES=F', Datacode.CHANGE.value, 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_realtime_US_futures CHANGE {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('ES=F', Datacode.CHANGE_IN_PERCENT.value, 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_realtime_US_futures CHANGE_IN_PERCENT {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('ES=F', Datacode.LOW.value, 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_realtime_US_futures LOW {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('ES=F', Datacode.HIGH.value, 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_realtime_US_futures HIGH {}'.format(s))
|
||||
|
||||
def test_realtime_UK_ETF(self):
|
||||
|
||||
s = financials.getRealtime('VERX.L', Datacode.LAST_PRICE.value, 'YAHOO')
|
||||
self.assertEqual(type(s), float, 'test_realtime_UK_ETF LAST_PRICE {}'.format(s))
|
||||
self.assertEqual(float, type(s), 'test_realtime_UK_ETF LAST_PRICE {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('VERX.L', Datacode.TIMEZONE.value, 'YAHOO')
|
||||
self.assertEqual(s, 'Europe/London', 'test_realtime_UK_ETF TIMEZONE {}'.format(s))
|
||||
self.assertTrue(s == 'Europe/London', 'test_realtime_UK_ETF TIMEZONE: {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('CSP1.L', Datacode.NAME.value, 'YAHOO')
|
||||
self.assertEqual(type(s), str, 'test_realtime_UK_ETF NAME {}'.format(s))
|
||||
self.assertEqual(s, 'iShares VII Public Limited Company - iShares Core S&P 500 UCITS ETF',
|
||||
'test_realtime_UK_ETF NAME {}'.format(s))
|
||||
self.assertEqual(str, type(s), 'test_realtime_UK_ETF NAME {}'.format(s))
|
||||
self.assertEqual('iShares Core S&P 500 UCITS ETF USD (Acc)', s, 'test_realtime_UK_ETF NAME {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('VERX.L', 'SECTOR', 'YAHOO')
|
||||
self.assertIsNone(s, 'test_realtime_UK_ETF SECTOR {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('VERX.L', 'INDUSTRY', 'YAHOO')
|
||||
self.assertIsNone(s, 'test_realtime_UK_ETF INDUSTRY {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('VERX.L', 'PAYOUT_RATIO', 'YAHOO')
|
||||
self.assertIsNone(s, 'test_realtime_UK_ETF PAYOUT_RATIO {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('VERX.L', 'SHARES_OUT', 'YAHOO')
|
||||
self.assertIsNone(s, 'test_realtime_UK_ETF SHARES_OUT {}'.format(s))
|
||||
|
||||
def test_realtime_DE_equity(self):
|
||||
|
||||
s = financials.getRealtime('SAP.DE', Datacode.LAST_PRICE.value, 'YAHOO')
|
||||
self.assertEqual(type(s), float, 'test_realtime_DE_equity LAST_PRICE {}'.format(s))
|
||||
self.assertEqual(float, type(s), 'test_realtime_DE_equity LAST_PRICE {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('SAP.DE', Datacode.TIMEZONE.value, 'YAHOO')
|
||||
self.assertEqual(s, 'Europe/Berlin', 'test_realtime_DE_equity TIMEZONE {}'.format(s))
|
||||
self.assertTrue(s == 'Europe/Berlin', 'test_realtime_DE_equity TIMEZONE: {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('SAP.DE', Datacode.SECTOR.value, 'YAHOO')
|
||||
self.assertEqual(str, type(s), 'test_realtime_DE_equity SECTOR {}'.format(s))
|
||||
self.assertEqual('Technology', s, 'test_realtime_DE_equity SECTOR {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('SAP.DE', Datacode.INDUSTRY.value, 'YAHOO')
|
||||
self.assertEqual(str, type(s), 'test_realtime_DE_equity INDUSTRY {}'.format(s))
|
||||
self.assertEqual(s, 'Software - Application', 'test_realtime_DE_equity INDUSTRY {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('LYY8.DE', Datacode.NAME.value, 'YAHOO')
|
||||
self.assertEqual(str, type(s), 'test_realtime_DE_equity NAME {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('LYY8.DE', Datacode.EXCHANGE.value, 'YAHOO')
|
||||
self.assertEqual(s, 'XETRA', 'test_realtime_DE_equity EXCHANGE')
|
||||
|
||||
def test_TA_equity(self):
|
||||
|
||||
s = financials.getRealtime('LUMI.TA', 'LAST_PRICE', 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_TA_equity LAST_PRICE {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('LUMI.TA', 'OPEN', 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_TA_equity OPEN {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('LUMI.TA', 'LOW', 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_TA_equity LOW {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('LUMI.TA', 'HIGH', 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_TA_equity HIGH {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('LUMI.TA', 'LOW_52_WEEK', 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_TA_equity LOW_52_WEEK {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('LUMI.TA', 'HIGH_52_WEEK', 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_TA_equity HIGH_52_WEEK {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('LUMI.TA', 'MARKET_CAP', 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_TA_equity MARKET_CAP {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('LUMI.TA', 'VOLUME', 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_TA_equity VOLUME {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('LUMI.TA', 'CURRENCY', 'YAHOO')
|
||||
self.assertEqual('ILA', s, 'test_TA_equity CURRENCY')
|
||||
|
||||
s = financials.getRealtime('LUMI.TA', 'SECTOR', 'YAHOO')
|
||||
self.assertEqual(str, type(s), 'test_TLV_equity SECTOR {}'.format(s))
|
||||
self.assertEqual('Financial Services', s, 'test_TA_equity SECTOR {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('LUMI.TA', 'INDUSTRY', 'YAHOO')
|
||||
self.assertEqual(str, type(s), 'test_TLV_equity INDUSTRY {}'.format(s))
|
||||
self.assertEqual('Banks - Regional', s, 'test_TA_equity INDUSTRY {}'.format(s))
|
||||
|
||||
def test_DK_equity(self):
|
||||
s = financials.getRealtime('NOVO-B.CO', 'last_price', 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_DK_equity LAST_PRICE {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('NOVO-B.CO', 'name', 'YAHOO')
|
||||
self.assertEqual('Novo Nordisk A/S', s, 'test_DK_equity NAME {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('NOVO-B.CO', 'currency', 'YAHOO')
|
||||
self.assertEqual('DKK', s, 'test_DK_equity CURRENCY {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('NOVO-B.CO', 'industry', 'YAHOO')
|
||||
self.assertEqual(str, type(s), 'test_DK_equity INDUSTRY {}'.format(s))
|
||||
self.assertEqual('Drug Manufacturers - General', s, 'test_DK_equity INDUSTRY {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('MAERSK-B.CO', 'currency', 'YAHOO')
|
||||
self.assertEqual('DKK', s, 'test_DK_equity CURRENCY {}'.format(s))
|
||||
|
||||
def test_realtime_TY_equity(self):
|
||||
s = financials.getRealtime('6503.T', Datacode.SECTOR.value, 'YAHOO')
|
||||
self.assertEqual(str, type(s), 'test_TY_equity SECTOR {}'.format(s))
|
||||
self.assertEqual(s, 'Industrials', 'test_TY_equity SECTOR {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('6503.T', Datacode.INDUSTRY.value, 'YAHOO')
|
||||
self.assertEqual(str, type(s), 'test_TY_equity INDUSTRY {}'.format(s))
|
||||
self.assertEqual(s, 'Electrical Equipment & Parts', 'test_TY_equity INDUSTRY {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('6503.T', Datacode.OPEN.value, 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_TY_equity OPEN {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('6503.T', Datacode.LOW.value, 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_TY_equity LOW {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('6503.T', Datacode.HIGH.value, 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_TY_equity HIGH {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('6503.T', Datacode.LOW_52_WEEK.value, 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_TY_equity LOW_52_WEEK {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('6503.T', Datacode.HIGH_52_WEEK.value, 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_TY_equity HIGH_52_WEEK {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('6503.T', Datacode.MARKET_CAP.value, 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_TY_equity MARKET_CAP {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('6503.T', Datacode.VOLUME.value, 'YAHOO')
|
||||
self.assertEqual(float, type(s), 'test_TY_equity VOLUME {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('6503.T', Datacode.CURRENCY.value, 'YAHOO')
|
||||
self.assertEqual(s, 'JPY', 'test_TY_equity CURRENCY')
|
||||
|
||||
s = financials.getRealtime('6503.T', Datacode.TIMEZONE.value, 'YAHOO')
|
||||
self.assertEqual(s, 'Asia/Tokyo', 'test_TY_equity TIMEZONE')
|
||||
|
||||
def test_historic_US_equity(self):
|
||||
|
||||
s = financials.getHistoric('IBM', Datacode.LAST_PRICE.value, '2017-01-01', 'YAHOO')
|
||||
self.assertEqual(s, 'Not a trading day \'2017-01-01\'', 'test_historic_US_equity LAST_PRICE {}'.format(s))
|
||||
self.assertEqual('Not a trading day \'2017-01-01\'', s, 'test_historic_US_equity LAST_PRICE {}'.format(s))
|
||||
|
||||
s = financials.getHistoric('IBM', Datacode.CLOSE.value, '2017-01-01', 'YAHOO')
|
||||
self.assertEqual(s, 'Not a trading day \'2017-01-01\'', 'test_historic_US_equity CLOSE {}'.format(s))
|
||||
self.assertEqual('Not a trading day \'2017-01-01\'', s, 'test_historic_US_equity CLOSE {}'.format(s))
|
||||
|
||||
s = financials.getHistoric('IBM', Datacode.LAST_PRICE.value, '2017-01-03', 'YAHOO')
|
||||
self.assertEqual(s, 'Data doesn\'t exist - 21', 'test_historic_US_equity LAST_PRICE {}'.format(s))
|
||||
self.assertIsNone(s, 'test_historic_US_equity LAST_PRICE {}'.format(s))
|
||||
|
||||
s = financials.getHistoric('IBM', Datacode.CLOSE.value, '2017-01-03', 'YAHOO')
|
||||
self.assertEqual(s, 167.190002, 'test_historic_US_equity CLOSE {}'.format(s))
|
||||
self.assertAlmostEqual(159.84, s, 2, 'test_historic_US_equity CLOSE {}'.format(s))
|
||||
|
||||
financials.yahoo.historicdata = {}
|
||||
|
||||
s = financials.getHistoric('IBM', Datacode.CLOSE.value, '2017-01-03', 'YAHOO')
|
||||
self.assertEqual(s, 167.190002, 'test_historic_US_equity CLOSE {}'.format(s))
|
||||
self.assertAlmostEqual(159.84, s, 2, 'test_historic_US_equity CLOSE {}'.format(s))
|
||||
|
||||
directory = os.path.join(str(pathlib.Path.home()), '.financials-extension')
|
||||
ibm = os.path.join(directory, 'yahoo-IBM.csv')
|
||||
ibm = os.path.join(directory, 'yahoo-hist-IBM.json')
|
||||
try:
|
||||
os.unlink(ibm)
|
||||
except:
|
||||
@@ -112,15 +423,15 @@ class TestYahoo(unittest.TestCase):
|
||||
financials.yahoo.historicdata = {}
|
||||
|
||||
s = financials.getHistoric('IBM', Datacode.CLOSE.value, '2017-01-03', 'YAHOO')
|
||||
self.assertEqual(s, 167.190002, 'test_historic_US_equity CLOSE {}'.format(s))
|
||||
self.assertAlmostEqual(159.84, s, 2, 'test_historic_US_equity CLOSE {}'.format(s))
|
||||
|
||||
s = financials.getHistoric('IBM', Datacode.ADJ_CLOSE.value, '2017-01-03', 'YAHOO')
|
||||
self.assertEqual(s, 160.947433, 'test_historic_US_equity ADJ_CLOSE {}'.format(s))
|
||||
self.assertEqual(float, type(s), 'test_historic_US_equity ADJ_CLOSE {}'.format(s))
|
||||
|
||||
def test_historic_UK_ETF(self):
|
||||
|
||||
directory = os.path.join(str(pathlib.Path.home()), '.financials-extension')
|
||||
verx = os.path.join(directory, 'yahoo-VERX.L.csv')
|
||||
verx = os.path.join(directory, 'yahoo-hist-VERX.L.json')
|
||||
try:
|
||||
os.unlink(verx)
|
||||
except:
|
||||
@@ -128,39 +439,31 @@ class TestYahoo(unittest.TestCase):
|
||||
|
||||
financials.yahoo.historicdata = {}
|
||||
|
||||
# Inception Date 2014-09-30
|
||||
s = financials.getHistoric('VERX.L', Datacode.CLOSE.value, '2014-01-06', 'YAHOO')
|
||||
self.assertEqual(s, 'Not a trading day \'2014-01-06\'', 'test_historic_UK_ETF CLOSE {}'.format(s))
|
||||
|
||||
# Inception Date 2014-09-30
|
||||
s = financials.getHistoric('VERX.L', Datacode.CLOSE.value, '2015-01-01', 'YAHOO')
|
||||
self.assertEqual(s, 'Not a trading day \'2015-01-01\'', 'test_historic_UK_ETF CLOSE {}'.format(s))
|
||||
|
||||
s = financials.getHistoric('VERX.L', Datacode.LAST_PRICE.value, '2017-01-01', 'YAHOO')
|
||||
self.assertEqual(s, 'Not a trading day \'2017-01-01\'', 'test_historic_UK_ETF LAST_PRICE {}'.format(s))
|
||||
|
||||
s = financials.getHistoric('VERX.L', Datacode.CLOSE.value, '2017-01-03', 'YAHOO')
|
||||
self.assertEqual(s, 23.24, 'test_historic_UK_ETF CLOSE {}'.format(s))
|
||||
self.assertAlmostEqual(s, 23.24, 2, 'test_historic_UK_ETF CLOSE {}'.format(s))
|
||||
|
||||
s = financials.getHistoric('VERX.L', Datacode.CLOSE.value, '2016-10-03', 'YAHOO')
|
||||
self.assertEqual(s, 22.26, 'test_historic_UK_ETF CLOSE {}'.format(s))
|
||||
self.assertAlmostEqual(s, 22.26, 2, 'test_historic_UK_ETF CLOSE {}'.format(s))
|
||||
|
||||
# Inception Date 2014-09-30
|
||||
s = financials.getHistoric('VERX.L', Datacode.CLOSE.value, '2014-01-06', 'YAHOO')
|
||||
self.assertEqual(s, 'Not a trading day \'2014-01-06\'', 'test_historic_UK_ETF CLOSE {}'.format(s))
|
||||
s = financials.getHistoric('VERX.L', Datacode.CLOSE.value, '2018-04-02', 'YAHOO')
|
||||
self.assertEqual(s, 'Not a trading day \'2018-04-02\'', 'test_historic_UK_ETF CLOSE {}'.format(s))
|
||||
|
||||
# Inception Date 2014-09-30
|
||||
s = financials.getHistoric('VERX.L', Datacode.CLOSE.value, '2015-01-01', 'YAHOO')
|
||||
self.assertEqual(s, 'Not a trading day \'2015-01-01\'', 'test_historic_UK_ETF CLOSE {}'.format(s))
|
||||
|
||||
s = financials.getHistoric('VERX.L', Datacode.CLOSE.value, 42738, 'YAHOO') # 2017-01-03
|
||||
self.assertEqual(s, 23.24, 'test_historic_UK_ETF CLOSE {}'.format(s))
|
||||
self.assertAlmostEqual(s, 23.24, 2, 'test_historic_UK_ETF CLOSE {}'.format(s))
|
||||
|
||||
s = financials.getHistoric('VERX.L', Datacode.CLOSE.value, 42738.0, 'YAHOO') # 2017-01-03
|
||||
self.assertEqual(s, 23.24, 'test_historic_UK_ETF CLOSE {}'.format(s))
|
||||
self.assertAlmostEqual(s, 23.24, 2, 'test_historic_UK_ETF CLOSE {}'.format(s))
|
||||
|
||||
s = financials.getHistoric('VERX.L', Datacode.CLOSE.value, 42646.0, 'YAHOO') # 2016-10-03
|
||||
self.assertEqual(s, 22.26, 'test_historic_UK_ETF CLOSE {}'.format(s))
|
||||
self.assertAlmostEqual(s, 22.26, 2, 'test_historic_UK_ETF CLOSE {}'.format(s))
|
||||
|
||||
def test_historic_DE_equity(self):
|
||||
|
||||
@@ -168,36 +471,54 @@ class TestYahoo(unittest.TestCase):
|
||||
self.assertEqual(s, 'Not a trading day \'2017-01-01\'', 'test_historic_DE_equity LAST_PRICE {}'.format(s))
|
||||
|
||||
s = financials.getHistoric('SAP.DE', Datacode.CLOSE.value, '2017-01-03', 'YAHOO')
|
||||
self.assertEqual(s, 82.889999, 'test_historic_DE_equity CLOSE {}'.format(s))
|
||||
self.assertAlmostEqual(s, 82.89, 2, 'test_historic_DE_equity CLOSE {}'.format(s))
|
||||
|
||||
s = financials.getHistoric('C060.DE', Datacode.CLOSE.value, '2017-01-03', 'YAHOO')
|
||||
self.assertEqual(s, 72.870003, 'test_historic_DE_equity CLOSE {}'.format(s))
|
||||
s = financials.getHistoric('LYY8.DE', Datacode.CLOSE.value, '2017-01-03', 'YAHOO')
|
||||
self.assertAlmostEqual(s, 96.01, 2, 'test_historic_DE_equity CLOSE {}'.format(s))
|
||||
|
||||
def test_errors(self):
|
||||
def test_realtime_errors(self):
|
||||
|
||||
s = financials.getRealtime('NO_NAME', Datacode.LAST_PRICE.value, 'YAHOO')
|
||||
self.assertIsNone(s, 'test_realtime_errors LAST_PRICE {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('NO_NAME', Datacode.PAYOUT_RATIO.value, 'YAHOO')
|
||||
self.assertIsNone(s, 'test_realtime_errors PAYOUT_RATIO {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('NO_NAME', Datacode.SECTOR.value, 'YAHOO')
|
||||
self.assertIsNone(s, 'test_realtime_errors SECTOR {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('LYY8.DE', -1, 'YAHOO')
|
||||
self.assertEqual('Datacode -1 not supported', s, 'test_realtime_errors -1 {}'.format(s))
|
||||
|
||||
def test_historic_errors(self):
|
||||
|
||||
s = financials.getHistoric('NO_NAME', Datacode.LAST_PRICE.value, '2018-01-08', 'YAHOO')
|
||||
self.assertIsNone(s, 'test_historic_errors LAST_PRICE {}'.format(s))
|
||||
|
||||
s = financials.getHistoric('IBM', Datacode.CLOSE.value, '2030-01-01', 'YAHOO')
|
||||
self.assertEqual(s, 'Future date \'2030-01-01\'', 'test_errors CLOSE {}'.format(s))
|
||||
self.assertEqual(s, 'Future date \'2030-01-01\'', 'test_historic_errors CLOSE {}'.format(s))
|
||||
|
||||
s = financials.getRealtime('IBM', 9999, 'YAHOO')
|
||||
self.assertEqual(s, 'Datacode 9999 not supported', 'test_errors 9999')
|
||||
self.assertEqual(s, 'Datacode 9999 not supported', 'test_historic_errors 9999')
|
||||
|
||||
s = financials.getRealtime('IBM', Datacode.ADJ_CLOSE.value, 'YAHOO')
|
||||
self.assertEqual(s, 'Data doesn\'t exist - 91', 'test_errors ADJ_CLOSE {}'.format(s))
|
||||
self.assertIsNone(s, 'test_historic_errors ADJ_CLOSE {}'.format(s))
|
||||
|
||||
s = financials.getHistoric('IBM', Datacode.CLOSE.value, '2030-01-01', 'YAHOO')
|
||||
self.assertEqual(s, 'Future date \'2030-01-01\'', 'test_errors CLOSE {}'.format(s))
|
||||
self.assertEqual(s, 'Future date \'2030-01-01\'', 'test_historic_errors CLOSE {}'.format(s))
|
||||
|
||||
s = financials.getHistoric('IBM', Datacode.CLOSE.value, '1990-01-01', 'YAHOO')
|
||||
self.assertEqual(s, 'Date before 2000 \'1990-01-01\'', 'test_errors CLOSE {}'.format(s))
|
||||
self.assertEqual(s, 'Date before 2000 \'1990-01-01\'', 'test_historic_errors CLOSE {}'.format(s))
|
||||
|
||||
s = financials.getHistoric('IBM', Datacode.CLOSE.value, 'abcdef', 'YAHOO')
|
||||
self.assertEqual(s, 'Date format not supported: \'abcdef\'', 'test_errors CLOSE {}'.format(s))
|
||||
self.assertEqual(s, 'Date format not supported: \'abcdef\'', 'test_historic_errors CLOSE {}'.format(s))
|
||||
|
||||
s = financials.getHistoric('IBM', Datacode.CLOSE.value, True, 'YAHOO')
|
||||
self.assertEqual(s, 'Date type not supported: <class \'bool\'> \'True\'', 'test_errors CLOSE {}'.format(s))
|
||||
self.assertEqual(s, 'Date type not supported: <class \'bool\'> \'True\'',
|
||||
'test_historic_errors CLOSE {}'.format(s))
|
||||
|
||||
s = financials.getHistoric('IBM', Datacode.CLOSE.value, -1000000, 'YAHOO')
|
||||
self.assertEqual(s, 'Date format not supported: -1000000', 'test_errors CLOSE {}'.format(s))
|
||||
self.assertEqual(s, 'Date format not supported: -1000000', 'test_historic_errors CLOSE {}'.format(s))
|
||||
|
||||
def test_errors_cell_range_passed(self):
|
||||
cell_range = ((1, 2), ('3', '4'), (5.0, 6.0))
|
||||
@@ -216,4 +537,8 @@ class TestYahoo(unittest.TestCase):
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('unittest_args', nargs='*')
|
||||
args = parser.parse_args()
|
||||
unit_argv = [sys.argv[0]] + args.unittest_args
|
||||
unittest.main(argv=unit_argv)
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import re
|
||||
|
||||
|
||||
def is_positive_float(f):
|
||||
return type(f) == float and f > 0.0
|
||||
|
||||
|
||||
def is_date(s):
|
||||
match = re.compile(r'^20\d\d-\d\d-\d\d$', flags=re.DOTALL).search(s)
|
||||
return match is not None
|
||||
@@ -0,0 +1,232 @@
|
||||
|
||||
# Copied from https://gist.github.com/h-j-13/e3a585796510b59601e34a07e99b386d
|
||||
|
||||
whois_timezone_info = {
|
||||
"A": 1 * 3600,
|
||||
"ACDT": 10.5 * 3600,
|
||||
"ACST": 9.5 * 3600,
|
||||
"ACT": -5 * 3600,
|
||||
"ACWST": 8.75 * 3600,
|
||||
"ADT": 4 * 3600,
|
||||
"AEDT": 11 * 3600,
|
||||
"AEST": 10 * 3600,
|
||||
"AET": 10 * 3600,
|
||||
"AFT": 4.5 * 3600,
|
||||
"AKDT": -8 * 3600,
|
||||
"AKST": -9 * 3600,
|
||||
"ALMT": 6 * 3600,
|
||||
"AMST": -3 * 3600,
|
||||
"AMT": -4 * 3600,
|
||||
"ANAST": 12 * 3600,
|
||||
"ANAT": 12 * 3600,
|
||||
"AQTT": 5 * 3600,
|
||||
"ART": -3 * 3600,
|
||||
"AST": 3 * 3600,
|
||||
"AT": -4 * 3600,
|
||||
"AWDT": 9 * 3600,
|
||||
"AWST": 8 * 3600,
|
||||
"AZOST": 0 * 3600,
|
||||
"AZOT": -1 * 3600,
|
||||
"AZST": 5 * 3600,
|
||||
"AZT": 4 * 3600,
|
||||
"AoE": -12 * 3600,
|
||||
"B": 2 * 3600,
|
||||
"BNT": 8 * 3600,
|
||||
"BOT": -4 * 3600,
|
||||
"BRST": -2 * 3600,
|
||||
"BRT": -3 * 3600,
|
||||
"BST": 6 * 3600,
|
||||
"BTT": 6 * 3600,
|
||||
"C": 3 * 3600,
|
||||
"CAST": 8 * 3600,
|
||||
"CAT": 2 * 3600,
|
||||
"CCT": 6.5 * 3600,
|
||||
"CDT": -5 * 3600,
|
||||
"CEST": 2 * 3600,
|
||||
"CET": 1 * 3600,
|
||||
"CHADT": 13.75 * 3600,
|
||||
"CHAST": 12.75 * 3600,
|
||||
"CHOST": 9 * 3600,
|
||||
"CHOT": 8 * 3600,
|
||||
"CHUT": 10 * 3600,
|
||||
"CIDST": -4 * 3600,
|
||||
"CIST": -5 * 3600,
|
||||
"CKT": -10 * 3600,
|
||||
"CLST": -3 * 3600,
|
||||
"CLT": -4 * 3600,
|
||||
"COT": -5 * 3600,
|
||||
"CST": -6 * 3600,
|
||||
"CT": -6 * 3600,
|
||||
"CVT": -1 * 3600,
|
||||
"CXT": 7 * 3600,
|
||||
"ChST": 10 * 3600,
|
||||
"D": 4 * 3600,
|
||||
"DAVT": 7 * 3600,
|
||||
"DDUT": 10 * 3600,
|
||||
"E": 5 * 3600,
|
||||
"EASST": -5 * 3600,
|
||||
"EAST": -6 * 3600,
|
||||
"EAT": 3 * 3600,
|
||||
"ECT": -5 * 3600,
|
||||
"EDT": -4 * 3600,
|
||||
"EEST": 3 * 3600,
|
||||
"EET": 2 * 3600,
|
||||
"EGST": 0 * 3600,
|
||||
"EGT": -1 * 3600,
|
||||
"EST": -5 * 3600,
|
||||
"ET": -5 * 3600,
|
||||
"F": 6 * 3600,
|
||||
"FET": 3 * 3600,
|
||||
"FJST": 13 * 3600,
|
||||
"FJT": 12 * 3600,
|
||||
"FKST": -3 * 3600,
|
||||
"FKT": -4 * 3600,
|
||||
"FNT": -2 * 3600,
|
||||
"G": 7 * 3600,
|
||||
"GALT": -6 * 3600,
|
||||
"GAMT": -9 * 3600,
|
||||
"GET": 4 * 3600,
|
||||
"GFT": -3 * 3600,
|
||||
"GILT": 12 * 3600,
|
||||
"GMT": 0 * 3600,
|
||||
"GST": 4 * 3600,
|
||||
"GYT": -4 * 3600,
|
||||
"H": 8 * 3600,
|
||||
"HDT": -9 * 3600,
|
||||
"HKT": 8 * 3600,
|
||||
"HOVST": 8 * 3600,
|
||||
"HOVT": 7 * 3600,
|
||||
"HST": -10 * 3600,
|
||||
"I": 9 * 3600,
|
||||
"ICT": 7 * 3600,
|
||||
"IDT": 3 * 3600,
|
||||
"IOT": 6 * 3600,
|
||||
"IRDT": 4.5 * 3600,
|
||||
"IRKST": 9 * 3600,
|
||||
"IRKT": 8 * 3600,
|
||||
"IRST": 3.5 * 3600,
|
||||
"IST": 5.5 * 3600,
|
||||
"JST": 9 * 3600,
|
||||
"K": 10 * 3600,
|
||||
"KGT": 6 * 3600,
|
||||
"KOST": 11 * 3600,
|
||||
"KRAST": 8 * 3600,
|
||||
"KRAT": 7 * 3600,
|
||||
"KST": 9 * 3600,
|
||||
"KUYT": 4 * 3600,
|
||||
"L": 11 * 3600,
|
||||
"LHDT": 11 * 3600,
|
||||
"LHST": 10.5 * 3600,
|
||||
"LINT": 14 * 3600,
|
||||
"M": 12 * 3600,
|
||||
"MAGST": 12 * 3600,
|
||||
"MAGT": 11 * 3600,
|
||||
"MART": 9.5 * 3600,
|
||||
"MAWT": 5 * 3600,
|
||||
"MDT": -6 * 3600,
|
||||
"MHT": 12 * 3600,
|
||||
"MMT": 6.5 * 3600,
|
||||
"MSD": 4 * 3600,
|
||||
"MSK": 3 * 3600,
|
||||
"MST": -7 * 3600,
|
||||
"MT": -7 * 3600,
|
||||
"MUT": 4 * 3600,
|
||||
"MVT": 5 * 3600,
|
||||
"MYT": 8 * 3600,
|
||||
"N": -1 * 3600,
|
||||
"NCT": 11 * 3600,
|
||||
"NDT": 2.5 * 3600,
|
||||
"NFT": 11 * 3600,
|
||||
"NOVST": 7 * 3600,
|
||||
"NOVT": 7 * 3600,
|
||||
"NPT": 5.5 * 3600,
|
||||
"NRT": 12 * 3600,
|
||||
"NST": 3.5 * 3600,
|
||||
"NUT": -11 * 3600,
|
||||
"NZDT": 13 * 3600,
|
||||
"NZST": 12 * 3600,
|
||||
"O": -2 * 3600,
|
||||
"OMSST": 7 * 3600,
|
||||
"OMST": 6 * 3600,
|
||||
"ORAT": 5 * 3600,
|
||||
"P": -3 * 3600,
|
||||
"PDT": -7 * 3600,
|
||||
"PET": -5 * 3600,
|
||||
"PETST": 12 * 3600,
|
||||
"PETT": 12 * 3600,
|
||||
"PGT": 10 * 3600,
|
||||
"PHOT": 13 * 3600,
|
||||
"PHT": 8 * 3600,
|
||||
"PKT": 5 * 3600,
|
||||
"PMDT": -2 * 3600,
|
||||
"PMST": -3 * 3600,
|
||||
"PONT": 11 * 3600,
|
||||
"PST": -8 * 3600,
|
||||
"PT": -8 * 3600,
|
||||
"PWT": 9 * 3600,
|
||||
"PYST": -3 * 3600,
|
||||
"PYT": -4 * 3600,
|
||||
"Q": -4 * 3600,
|
||||
"QYZT": 6 * 3600,
|
||||
"R": -5 * 3600,
|
||||
"RET": 4 * 3600,
|
||||
"ROTT": -3 * 3600,
|
||||
"S": -6 * 3600,
|
||||
"SAKT": 11 * 3600,
|
||||
"SAMT": 4 * 3600,
|
||||
"SAST": 2 * 3600,
|
||||
"SBT": 11 * 3600,
|
||||
"SCT": 4 * 3600,
|
||||
"SGT": 8 * 3600,
|
||||
"SRET": 11 * 3600,
|
||||
"SRT": -3 * 3600,
|
||||
"SST": -11 * 3600,
|
||||
"SYOT": 3 * 3600,
|
||||
"T": -7 * 3600,
|
||||
"TAHT": -10 * 3600,
|
||||
"TFT": 5 * 3600,
|
||||
"TJT": 5 * 3600,
|
||||
"TKT": 13 * 3600,
|
||||
"TLT": 9 * 3600,
|
||||
"TMT": 5 * 3600,
|
||||
"TOST": 14 * 3600,
|
||||
"TOT": 13 * 3600,
|
||||
"TRT": 3 * 3600,
|
||||
"TVT": 12 * 3600,
|
||||
"U": -8 * 3600,
|
||||
"ULAST": 9 * 3600,
|
||||
"ULAT": 8 * 3600,
|
||||
"UTC": 0 * 3600,
|
||||
"UYST": -2 * 3600,
|
||||
"UYT": -3 * 3600,
|
||||
"UZT": 5 * 3600,
|
||||
"V": -9 * 3600,
|
||||
"VET": -4 * 3600,
|
||||
"VLAST": 11 * 3600,
|
||||
"VLAT": 10 * 3600,
|
||||
"VOST": 6 * 3600,
|
||||
"VUT": 11 * 3600,
|
||||
"W": -10 * 3600,
|
||||
"WAKT": 12 * 3600,
|
||||
"WARST": -3 * 3600,
|
||||
"WAST": 2 * 3600,
|
||||
"WAT": 1 * 3600,
|
||||
"WEST": 1 * 3600,
|
||||
"WET": 0 * 3600,
|
||||
"WFT": 12 * 3600,
|
||||
"WGST": -2 * 3600,
|
||||
"WGT": -3 * 3600,
|
||||
"WIB": 7 * 3600,
|
||||
"WIT": 9 * 3600,
|
||||
"WITA": 8 * 3600,
|
||||
"WST": 14 * 3600,
|
||||
"WT": 0 * 3600,
|
||||
"X": -11 * 3600,
|
||||
"Y": -12 * 3600,
|
||||
"YAKST": 10 * 3600,
|
||||
"YAKT": 9 * 3600,
|
||||
"YAPT": 10 * 3600,
|
||||
"YEKST": 6 * 3600,
|
||||
"YEKT": 5 * 3600,
|
||||
"Z": 0 * 3600,
|
||||
}
|
||||
-302
@@ -1,302 +0,0 @@
|
||||
# yahoo.py
|
||||
#
|
||||
# license: GNU LGPL
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 3 of the License, or (at your option) any later version.
|
||||
|
||||
|
||||
import csv
|
||||
import datetime
|
||||
import dateutil.parser
|
||||
import html
|
||||
import os
|
||||
import pathlib
|
||||
import pprint
|
||||
import pytz
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
import urllib.parse
|
||||
|
||||
from datacode import Datacode
|
||||
import baseclient
|
||||
import jsonParser
|
||||
|
||||
|
||||
def log(str):
|
||||
# print(str, file=sys.stderr)
|
||||
pass
|
||||
|
||||
|
||||
def raw(price, key, default=0.0):
|
||||
try:
|
||||
return price[key]['raw']
|
||||
except:
|
||||
pass
|
||||
|
||||
return default
|
||||
|
||||
|
||||
class Yahoo(baseclient.BaseClient):
|
||||
def __init__(self, ctx):
|
||||
super().__init__()
|
||||
|
||||
self.crumb = None
|
||||
self.realtime = {}
|
||||
self.historicdata = {}
|
||||
self.js = jsonParser.jsonObject
|
||||
|
||||
self.basedir = os.path.join(str(pathlib.Path.home()), '.financials-extension')
|
||||
os.makedirs(self.basedir, exist_ok=True)
|
||||
|
||||
def _read_ticker_csv_file(self, ticker):
|
||||
|
||||
fn = os.path.join(self.basedir, 'yahoo-{}.csv'.format(ticker))
|
||||
|
||||
if not os.path.isfile(fn):
|
||||
return
|
||||
|
||||
with open(fn, newline='') as csvfile:
|
||||
reader = csv.DictReader(csvfile)
|
||||
|
||||
ticks = {}
|
||||
|
||||
for row in reader:
|
||||
tick = {}
|
||||
try:
|
||||
tick[Datacode.OPEN] = float(row['Open'])
|
||||
tick[Datacode.LOW] = float(row['Low'])
|
||||
tick[Datacode.HIGH] = float(row['High'])
|
||||
tick[Datacode.VOLUME] = float(row['Volume'])
|
||||
tick[Datacode.CLOSE] = float(row['Close'])
|
||||
tick[Datacode.ADJ_CLOSE] = float(row['Adj Close'])
|
||||
except:
|
||||
pass
|
||||
|
||||
if len(tick) > 0:
|
||||
ticks[row['Date']] = tick
|
||||
|
||||
self.historicdata[ticker] = ticks
|
||||
|
||||
def getRealtime(self, ticker, datacode):
|
||||
|
||||
"""
|
||||
Retrieve realtime data for ticker from Yahoo Finance and cache it for further lookups
|
||||
|
||||
:param ticker: the ticker symbol e.g. VOD.L or LON:VOD
|
||||
:param datacode: the requested datacode
|
||||
:return:
|
||||
"""
|
||||
|
||||
# remove white space
|
||||
ticker = "".join(ticker.split())
|
||||
|
||||
# use cached value for up to 60 seconds
|
||||
if ticker in self.realtime:
|
||||
tick = self.realtime[ticker]
|
||||
if time.time() - 60 < tick[Datacode.TIMESTAMP]:
|
||||
return self._return_value(tick, datacode)
|
||||
else:
|
||||
del self.realtime[ticker]
|
||||
|
||||
url = 'https://finance.yahoo.com/quote/{}?p={}'.format(ticker, ticker)
|
||||
|
||||
try:
|
||||
text = self.urlopen(url)
|
||||
except BaseException as e:
|
||||
log(traceback.format_exc())
|
||||
return 'Yahoo.getRealtime({}, {}) - urlopen: {}'.format(ticker, datacode, e)
|
||||
|
||||
try:
|
||||
text = urllib.parse.unquote(text)
|
||||
text = text.replace('\\u002F', '/')
|
||||
|
||||
r = '"CrumbStore":{"crumb":"([^"]{11})"'
|
||||
pattern = re.compile(r)
|
||||
match = re.search(pattern, text)
|
||||
|
||||
if match:
|
||||
self.crumb = match.group(1)
|
||||
else:
|
||||
with open(os.path.join(self.basedir, 'yahoo-{}.html'.format(ticker)), "w") as text_file:
|
||||
print(text, file=text_file)
|
||||
|
||||
except BaseException as e:
|
||||
log(traceback.format_exc())
|
||||
return 'Yahoo.getRealtime({}, {}) - crumb: {}'.format(ticker, datacode, e)
|
||||
|
||||
try:
|
||||
start = text.find('"QuoteSummaryStore":{')
|
||||
|
||||
if start < 0:
|
||||
with open(os.path.join(self.basedir, 'yahoo-{}.html'.format(ticker)), "w") as text_file:
|
||||
print(text, file=text_file)
|
||||
|
||||
return 'Could not find QuoteSummaryStore for \'{}\''.format(ticker)
|
||||
|
||||
start = start + len('"QuoteSummaryStore":')
|
||||
results = self.js.parseString(text[start:])
|
||||
|
||||
if not results:
|
||||
with open(os.path.join(self.basedir, 'yahoo-{}.html'.format(ticker)), "w") as text_file:
|
||||
print(text, file=text_file)
|
||||
return None
|
||||
|
||||
price = results['price']
|
||||
quoteType = results['quoteType']
|
||||
|
||||
if not price:
|
||||
return 'Could not find price for \'{}\''.format(ticker)
|
||||
|
||||
if ticker not in self.realtime:
|
||||
self.realtime[ticker] = {}
|
||||
|
||||
tick = self.realtime[ticker]
|
||||
|
||||
tick[Datacode.PREV_CLOSE] = float(raw(price, 'regularMarketPreviousClose'))
|
||||
tick[Datacode.OPEN] = float(raw(price, 'regularMarketOpen'))
|
||||
tick[Datacode.CHANGE] = float(raw(price, 'regularMarketChange'))
|
||||
tick[Datacode.CHANGE_IN_PERCENT] = 100 * float(raw(price, 'regularMarketChangePercent'))
|
||||
tick[Datacode.LOW] = float(raw(price, 'regularMarketDayLow'))
|
||||
tick[Datacode.HIGH] = float(raw(price, 'regularMarketDayHigh'))
|
||||
tick[Datacode.LAST_PRICE] = float(raw(price, 'regularMarketPrice'))
|
||||
tick[Datacode.VOLUME] = float(raw(price, 'regularMarketVolume'))
|
||||
tick[Datacode.AVG_DAILY_VOL_3MOMTH] = float(raw(price, 'averageDailyVolume3Month'))
|
||||
|
||||
if quoteType:
|
||||
t = int(price['regularMarketTime'])
|
||||
tz = pytz.timezone(quoteType['exchangeTimezoneName'])
|
||||
|
||||
tick[Datacode.TIMEZONE] = tz
|
||||
dt = datetime.datetime.fromtimestamp(t, tz)
|
||||
|
||||
tick[Datacode.LAST_PRICE_DATE] = dt.date()
|
||||
tick[Datacode.LAST_PRICE_TIME] = dt.time()
|
||||
|
||||
tick[Datacode.TICKER] = str(price['symbol'])
|
||||
tick[Datacode.EXCHANGE] = str(price['exchange'])
|
||||
tick[Datacode.CURRENCY] = str(price['currency'])
|
||||
|
||||
name = price['longName'] or price['shortName']
|
||||
if name:
|
||||
tick[Datacode.NAME] = html.unescape(str(name))
|
||||
else:
|
||||
tick[Datacode.NAME] = ''
|
||||
|
||||
tick[Datacode.TIMESTAMP] = time.time()
|
||||
|
||||
except BaseException as e:
|
||||
with open(os.path.join(self.basedir, 'yahoo-{}.js'.format(ticker)), "w") as text_file:
|
||||
pprint.pprint(results.asList(), stream=text_file)
|
||||
|
||||
log(traceback.format_exc())
|
||||
return 'Yahoo.getRealtime({}, {}) - process: {}'.format(ticker, datacode, e)
|
||||
|
||||
return self._return_value(self.realtime[ticker], datacode)
|
||||
|
||||
def getHistoric(self, ticker: str, datacode: int, date):
|
||||
|
||||
"""
|
||||
Retrieve historic data for ticker from Yahoo Finance and cache it for further lookups
|
||||
|
||||
:param ticker: the ticker symbol e.g. VOD.L or LON:VOD
|
||||
:param datacode: the requested datacode
|
||||
:param date: the requested date
|
||||
:return:
|
||||
"""
|
||||
|
||||
# remove white space
|
||||
ticker = "".join(ticker.split())
|
||||
min_tick_date = None
|
||||
|
||||
if ticker not in self.historicdata:
|
||||
self._read_ticker_csv_file(ticker)
|
||||
|
||||
if ticker in self.historicdata:
|
||||
ticks = self.historicdata[ticker]
|
||||
|
||||
if date in ticks:
|
||||
return self._return_value(ticks[date], datacode)
|
||||
|
||||
# weekend, trading holiday or as yet unfetched
|
||||
if min(ticks) <= date <= max(ticks):
|
||||
return 'Not a trading day \'{}\''.format(date)
|
||||
|
||||
# (potentially) future date
|
||||
if date > max(ticks):
|
||||
t1 = int(dateutil.parser.parse(date).strftime('%s'))
|
||||
t2 = int(time.time())
|
||||
if t1 > t2:
|
||||
return 'Future date \'{}\''.format(date)
|
||||
|
||||
min_tick_date = int(dateutil.parser.parse(min(ticks)).strftime('%s')) # remember current earliest date
|
||||
|
||||
if not self.crumb:
|
||||
self.getRealtime(ticker, datacode)
|
||||
|
||||
if not self.crumb:
|
||||
return 'Yahoo.getHistoric({}, {}, {}) - crumb'.format(ticker, datacode, date)
|
||||
|
||||
try:
|
||||
t1 = int(dateutil.parser.parse(date).strftime('%s'))
|
||||
t2 = int(time.time())
|
||||
|
||||
if min_tick_date:
|
||||
t1 = min_tick_date
|
||||
|
||||
if t1 >= t2:
|
||||
return 'Future date \'{}\''.format(date)
|
||||
|
||||
if t1 < int(dateutil.parser.parse('2000-01-01').strftime('%s')):
|
||||
return 'Date before 2000 \'{}\''.format(date)
|
||||
|
||||
t1 = t1 - 2682000 # pad with extra month
|
||||
|
||||
except BaseException as e:
|
||||
log(traceback.format_exc())
|
||||
return 'Yahoo.getHistoric({}, {}, {}) - date: {}'.format(ticker, datacode, date, e)
|
||||
|
||||
try:
|
||||
|
||||
url = 'https://query1.finance.yahoo.com/v7/finance/download/{}' \
|
||||
'?period1={}&period2={}&interval=1d&events=history&crumb={}' \
|
||||
.format(ticker, t1, t2, urllib.parse.quote_plus(self.crumb))
|
||||
|
||||
text = self.urlopen(url)
|
||||
|
||||
with open(os.path.join(self.basedir, 'yahoo-{}.csv'.format(ticker)), "w") as csv_file:
|
||||
print(text, file=csv_file)
|
||||
|
||||
self._read_ticker_csv_file(ticker)
|
||||
|
||||
except BaseException as e:
|
||||
log(traceback.format_exc())
|
||||
return 'Yahoo.getHistoric({}, {}, {}) - read: {}'.format(ticker, datacode, date, e)
|
||||
|
||||
try:
|
||||
if ticker in self.historicdata:
|
||||
ticks = self.historicdata[ticker]
|
||||
|
||||
if date in ticks:
|
||||
return self._return_value(ticks[date], datacode)
|
||||
|
||||
# future date
|
||||
if date > max(ticks):
|
||||
return 'Future date \'{}\''.format(date)
|
||||
|
||||
# weekend or trading holiday
|
||||
return 'Not a trading day \'{}\''.format(date)
|
||||
|
||||
except BaseException as e:
|
||||
log(traceback.format_exc())
|
||||
return 'Yahoo.getHistoric({}, {}, {}) - process: {}'.format(ticker, datacode, date, e)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def createInstance(ctx):
|
||||
return Yahoo(ctx)
|
||||
Reference in New Issue
Block a user