Compare commits

...
75 Commits
Author SHA1 Message Date
cmallwitzandGitHub 1ec1908948 Merge pull request #161 from Michael-C-Strobel/master
Swap to endpoint for fetching crumb
2026-03-28 16:18:16 +00:00
Michael Strobel 03770b4a2e Swap to endpoint for fetching crumb. 2026-03-07 15:26:53 -08:00
cmallwitz fef8d3e442 Package additional Python modules 2025-05-18 15:27:55 +01:00
cmallwitz af69d7f9fd Bundle Python module "requests" 2025-05-13 18:21:09 +01:00
cmallwitz b5b1ca5d09 README.md update 2025-05-12 22:06:32 +01:00
cmallwitz e50f084886 README.md update 2025-05-12 19:35:48 +01:00
cmallwitz d3e2937286 Workaround for Yahoo HTTPS fingerprinting 2025-05-12 18:11:13 +01:00
cmallwitz a0f723cfba Fix historic price rounding and missing summaryDetail 2025-04-21 16:12:10 +01:00
cmallwitz 376e0999c9 Fixed Yahoo consent form handling 2025-02-03 21:23:34 +00:00
cmallwitz 7b451bf872 [Issue 115] GETHISTORIC not working 2024-09-29 14:41:52 +01:00
cmallwitz 28ae9efa74 Fix for FT redirect to unencoded URLs 2024-09-02 20:37:37 +01:00
cmallwitz 090e604d1d Improve Yahoo crumb handling 2024-09-01 17:35:26 +01:00
cmallwitz a4432a85e6 Fix Yahoo crumb handling 2024-05-17 15:56:43 +01:00
cmallwitz a593336b08 Handle Yahoo site redesign 2024-05-01 18:35:41 +01:00
cmallwitz 06d9b91793 Update version and some cleanups 2024-03-13 12:11:42 +00:00
cmallwitzandGitHub 60d5d7b764 Merge pull request #99 from beckjake/handle-data-value
Handle the case where there is a data-value but no value
2024-03-12 17:18:17 +00:00
Jacob Beck 345f57420c Handle the case where there is a data-value but no value 2024-03-08 21:51:21 -07:00
cmallwitz 88b039d471 Yahoo: Don't re-fetch non-existing statistic/profile data 2023-12-29 22:22:28 +00:00
cmallwitz 7e751928de Rework Yahoo cookies (always fetch from scratch) 2023-12-29 18:52:19 +00:00
cmallwitz 0c07c4c3ba Update build for LibeOffice 7.4+ 2023-12-24 19:49:56 +00:00
cmallwitz 3f6249a6b6 Update dependencies for Fedora 39 2023-11-19 21:37:46 +00:00
cmallwitz d05ca9bb55 Reworked Yahoo cookie 2023-07-04 22:10:09 +01:00
cmallwitz 023fa71a81 Reworked Yahoo cookie and general network error handling 2023-01-20 16:04:44 +00:00
cmallwitz a89edcc5fc Rework Yahoo after website change (done) 2022-12-20 15:17:54 +00:00
cmallwitz c71825143e Rework Yahoo after website change (phase 1) 2022-12-18 18:58:59 +00:00
cmallwitz 587b391393 Update to README.md 2022-12-18 17:32:58 +00:00
cmallwitz 605e9ef185 Updated examples.ods for COINBASE 2022-12-13 13:52:29 +00:00
cmallwitz e0491318c3 Fixing Coinbase merge 2022-12-13 13:27:52 +00:00
cmallwitz 24b1406772 Merge branch 'TheSerapher-coinbase-and-macos' 2022-12-13 13:25:08 +00:00
cmallwitz 1d9c4e824f Merge branch 'coinbase-and-macos' of github.com:TheSerapher/Financials-Extension into TheSerapher-coinbase-and-macos 2022-12-13 13:24:24 +00:00
Sebastian Grewe c3a6d8d69a [WIP] Examples.ods 2022-12-13 10:29:00 +01:00
Sebastian Grewe d0fc0ae2bf Revert "[VERSION] 3.1.0"
This reverts commit e4d395240d.
2022-12-09 15:44:48 +01:00
Sebastian Grewe e4d395240d [VERSION] 3.1.0 2022-12-09 15:44:15 +01:00
Sebastian Grewe c3dc5c35fc [ADDED] Super simple test 2022-12-09 15:43:47 +01:00
cmallwitzandSebastian Grewe b356a1bca4 Updated Yahoo EU consent cookies 2022-12-09 15:43:47 +01:00
Sebastian Grewe befd0956f0 [FIX] Compile for MacOS 2022-12-09 15:14:14 +01:00
cmallwitz 9e842b30de Updated Yahoo EU consent cookies 2022-12-09 13:51:19 +00:00
Sebastian Grewe 6d0f56c87d [ADDED] Coinbase Exchange for Crypto symbols 2022-12-09 14:43:35 +01:00
Sebastian Grewe cd41959559 [UPDATE] added support for MacOS to compile.sh 2022-12-09 14:18:48 +01:00
cmallwitz 52e7119081 Updated Yahoo EU consent cookies 2022-11-04 13:25:40 +00:00
cmallwitz 520d2441cb Refresh Yahoo cookies 2022-06-02 21:19:49 +01:00
cmallwitz 8b370dfc73 Updated version 2022-01-07 21:42:07 +00:00
cmallwitz af82f16740 Updated Yahoo EU consent 2022-01-07 21:33:55 +00:00
cmallwitz 4e626c272e Add shares outstanding and free float information 2021-09-28 20:28:00 +01:00
cmallwitz e22dc7448f Fix SSL issue on MacOS 2021-09-11 23:46:42 +01:00
cmallwitz ab34a246e3 Update Readme 2021-08-19 19:50:41 +01:00
cmallwitz aa20845ae4 Fix yield for US mutuals and EFTs 2021-06-08 17:29:47 +01:00
cmallwitz 7fccc7c0ad Fix issue with FT avg vol 2021-05-19 21:44:06 +01:00
cmallwitz 7ddfbd084f Fix for FT symbols containing space 2021-05-15 18:31:10 +01:00
cmallwitz e22d4f46b8 Package all Python dependencies and disable Google as source 2021-04-03 11:23:45 +01:00
cmallwitz 6a3ab22801 Added BID/ASK/BIDSIZE/ASKSIZE/EXPIRY_DATE fields for FT/YAHOO and minor ccy fix 2021-02-22 21:09:03 +00:00
cmallwitz 46e6a68f6c Fix currency issue with Yahoo / Moscow symbols 2021-02-01 20:33:13 +00:00
cmallwitz ddaa4dc0aa Merge branch 'master' of github.com:cmallwitz/Financials-Extension 2021-02-01 20:29:14 +00:00
cmallwitz 9c8a80b1e3 Fix currency issue with Yahoo / Moscow symbols 2021-02-01 20:28:11 +00:00
cmallwitz e99a1109df Fix currency issue with Yahho / Moscow symbols 2021-02-01 20:27:04 +00:00
cmallwitz 372c878bad Fix typo 2021-01-03 18:37:06 +00:00
cmallwitz c3308ee9ec Historic data not working due to missing locale setting on Windows 2021-01-02 17:13:18 +00:00
cmallwitz 85bc40f6bd Fixed typo 2020-12-29 15:04:52 +00:00
cmallwitz 601b91c8c3 Version 2.4.0 and fixed build 2020-12-28 15:55:04 +00:00
cmallwitz 7b806ce527 Renamed files to avoid conflicts with system libraries 2020-12-26 21:29:06 +00:00
cmallwitz 240cfea126 Reworked file handling (now always using utf-8) and logging 2020-12-06 20:23:18 +00:00
cmallwitz 99c660f1ad Fixed FT issue 2020-10-26 09:19:31 +00:00
cmallwitz 19215e4e98 Adding additional statistic fields for Yahoo and FT 2020-09-11 13:06:25 +01:00
cmallwitz 0fc9d4335f Revert a small part of previos commit 2020-09-04 17:33:32 +01:00
cmallwitz 625a18eb02 Allow names instead of numbers to define data points 2020-09-04 16:32:09 +01:00
cmallwitz 1c10237b60 Fixed type in AVG_DAILY_VOL_3MOMTH 2020-09-04 10:30:35 +01:00
cmallwitz 8933347508 Fixed Yahoo and Google issues 2020-08-29 17:53:37 +01:00
cmallwitz 7aadaad7dc Adding sector and instustry support 2020-06-24 22:47:21 +01:00
cmallwitz 01538a13a8 Added new source FT 2020-04-26 14:53:06 +01:00
cmallwitz 9b464e22b1 Remove locale 2020-04-05 16:58:37 +01:00
cmallwitz d7a7e4f791 Updated Google Disclaimer 2020-04-04 19:17:33 +01:00
cmallwitz 12ecbfbbf8 Remove usage of locale.atof 2020-04-04 18:55:15 +01:00
cmallwitz 5783736d23 Added support check to check for missing locale on Debian 2020-04-04 17:36:29 +01:00
cmallwitz bf7f3f3c1e Fixed Google 2020-03-28 17:34:28 +00:00
christian 01ec715296 Update README 2020-03-28 15:32:23 +00:00
25 changed files with 2686 additions and 1466 deletions
+2 -1
View File
@@ -1,4 +1,5 @@
.idea
src/__pycache__
build/*
Financials-Extension.oxt
Financials-Extension.oxt
_trial_temp
Binary file not shown.
+180 -13
View File
@@ -1,35 +1,202 @@
# Financials-Extension
Extension for LibreOffice Calc to make stock, index and FX (Yahoo only) market data available in a Calc spread sheet - currently supports Yahoo and Google using web scraping.
## Overview
### System dependencies:
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.
Requires the following Python 3 packages (on top of standard libs): python3-dateutil python3-pyparsing pytz
## Latest version vs Yahoo HTTPS fingerprinting
### Usage:
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.
Have a look at the 'releases' tab above: download a prebuild **Financials-Extension.oxt** file and load it into Calc under menu item: Tools, Extension Manager...
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.
There is a file **examples.ods** there too with usage examples and possible arguments to functions.
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:
You will need the LibreOffice SDK installed.
I only ever tried building on a Linux box.
On my system I installed packages: libreoffice-dev libreoffice-java-common libreoffice-script-provider-python
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/IdeaProjects/Financials-Extension/
cd ~/tech/Financials-Extension/
python3 src/test_yahoo.py
\# Assuming curl-cffi is installed, LD_PRELOAD is not required here
python3 src/test_google.py
CURL_IMPERSONATE=chrome101 python3 -m unittest discover src
\# This builds file **Financials-Extension.oxt**
./compile.sh
### Tested with:
- Ubuntu 18.04 / LibreOffice Calc 6 / Python 3.6.7 (most recent version)
- Ubuntu 16.04 / LibreOffice Calc 5 (previous versions)
- 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.
+85 -11
View File
@@ -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,10 +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/naivehtmlparser.py "${PWD}"/build/
cp -f "${PWD}"/src/google2.py "${PWD}"/build/
cp -f "${PWD}"/src/yahoo.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/
BIN
View File
Binary file not shown.
+203 -138
View File
@@ -8,167 +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 select
from http.client import HTTPConnection, HTTPSConnection, HTTPException
from http import cookiejar
import urllib.request
from importlib import util
from datacode import Datacode
logger = logging.getLogger(__name__)
# logger.setLevel(logging.DEBUG)
class RedirectException(HTTPException):
def __init__(self, location):
self.location = location
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(HTTPException):
def __init__(self, url, status):
class HttpException(Exception):
def __init__(self, url, response):
self.url = url
self.status = status
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 (Windows NT 10.0; Win64; x64; rv:55.0) Gecko/20100101 Firefox/55.0'
'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0',
'Mozilla/5.0 (X11; Linux x86_64; rv:57.0) Gecko/20100101 Firefox/57.0',
'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:59.0) Gecko/20100101 Firefox/59.0',
'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.13; rv:59.0) Gecko/20100101 Firefox/59.0',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:59.0) Gecko/20100101 Firefox/59.0',
'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:25.0) Gecko/20100101 Firefox/59.0'
'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 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 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={}, cookies=[], **kwargs):
self.session.max_redirects = 5
_headers = self.default_headers.copy()
if headers:
for key, value in headers.items():
_headers[key] = value
def urlopen(self, url, data=None):
if cookies:
for c in cookies:
self.cookies.set_cookie(c)
self.last_url = None
connection = None
resp = self.session.request('POST' if data else 'GET', url, data=data)
scheme, _, host, path = url.split('/', 3)
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 (scheme, host) in self.connections:
connection = self.connections.get((scheme, host))
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 connection and select.select([connection.sock], [], [], 0)[0]:
connection.close()
connection = None
self.redirect_count = len(resp.history)
self.last_url = resp.url
if not connection:
logger.debug('Creating connection --------------------------------------------------')
connection = HTTPConnection(host, **kwargs) if scheme == 'http:' else HTTPSConnection(host, **kwargs)
return resp.text
logger.debug('Creating request -----------------------------------------------------')
logger.info('url=%s', url)
def get_ticker(self):
# generate and add cookie headers
request = urllib.request.Request(url)
tick = {}
self.cookies.add_cookie_header(request)
if request.get_header('Cookie'):
_headers['Cookie'] = request.get_header('Cookie')
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
for key, value in _headers.items():
logger.debug('Header: %s=%s', key, value)
tick[Datacode.TIMESTAMP] = None
# request
connection.request(method, '/' + path, data, _headers)
response = connection.getresponse()
logger.debug('Processing response --------------------------------------------------')
# logger.debug('response.status={}'.format(response.status))
for key, value in response.getheaders():
logger.debug('Header: %s=%s', key, value)
self.cookies.extract_cookies(response, request)
self.connections[(scheme, host)] = connection
return response
def urlopen(self, url, redirect=True, data=None, headers={}, cookies=[], **kwargs):
response = self.request('POST' if data else 'GET', url, data, headers, cookies, **kwargs)
text = response.read()
# Allow two redirects: used by Yahoo for some cookie based consent
if 300 <= response.status < 400:
location = response.getheader('Location')
if location and redirect:
if location.startswith('/'):
scheme, _, host, path = url.split('/', 3)
location = '{}//{}{}'.format(scheme, host, location)
response = self.request('POST' if data else 'GET', location, data, headers, cookies, **kwargs)
text = response.read()
if 300 <= response.status < 400:
location = response.getheader('Location')
if location and redirect:
if location.startswith('/'):
scheme, _, host, path = url.split('/', 3)
location = '{}//{}{}'.format(scheme, host, location)
response = self.request('POST' if data else 'GET', location, data, headers, cookies, **kwargs)
text = response.read()
else:
raise RedirectException(location)
else:
raise RedirectException(location)
if response.status >= 400:
raise HttpException(url, response.status)
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):
@@ -180,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]
@@ -191,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]
@@ -208,20 +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 and data[Datacode.MARKET_CAP]:
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]
@@ -229,20 +276,29 @@ 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 Datacode.TIMEZONE in data 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)
@@ -258,3 +314,12 @@ class BaseClient:
pass
return None
def version(self):
return requests_name + "_" + requests_version
def curl(self):
return curl_version
def close(self):
self.session.close()
+21 -3
View File
@@ -24,16 +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
@@ -42,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)
+19
View File
@@ -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>
+127 -22
View File
@@ -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 google2 as 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,9 +121,12 @@ 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)
@@ -73,10 +134,12 @@ class FinancialsImpl(unohelper.Base, Financials):
ticker = str(ticker).strip()
source = str(source).upper()
if source == 'GOOGLE':
s = self.google.getRealtime(ticker, datacode)
elif source == 'YAHOO':
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 = 'Source \'{}\' not supported'.format(source)
@@ -90,6 +153,7 @@ class FinancialsImpl(unohelper.Base, Financials):
return x
@profile
def getHistoric(self, ticker, datacode=None, date=None, source=None):
if ticker == 'SUPPORT':
@@ -118,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)
@@ -128,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)
@@ -138,7 +205,7 @@ 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)
@@ -163,16 +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()))
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, )
+110
View File
@@ -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)
+330
View File
@@ -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)
+467
View File
@@ -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&region=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
View File
@@ -10,15 +10,17 @@
#
import os
cur_dir = os.getcwd()
addin_id = "com.financials.getinfo"
addin_version = "1.0.4"
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')
-214
View File
@@ -1,214 +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 logging
import html
import re
import time
import traceback
import urllib.parse
from datacode import Datacode
from baseclient import BaseClient
logger = logging.getLogger(__name__)
# logger.setLevel(logging.DEBUG)
def handle_abbreviations(s):
s = str(s).strip()
if s.endswith('T'):
return float(s.replace('T', ''))*1000
if s.endswith('M'):
return float(s.replace('M', ''))*1000000
if s.endswith('B'):
return float(s.replace('B', ''))*1000000000
return float(s)
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 5 minutes
if ticker in self.realtime:
tick = self.realtime[ticker]
if time.time() - 5*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:
logger.error(traceback.format_exc())
return 'Google.getRealtime(\'{}\', {}) - read: {}'.format(ticker, datacode, e)
try:
r = '<meta\s*itemprop="([^"]+)"\s*content="([^"]+)"\s*/>'
pattern = re.compile(r)
result = pattern.findall(text)
if len(result) == 0:
return None
if ticker not in self.realtime:
self.realtime[ticker] = {}
tick = self.realtime[ticker]
for key, value in result:
if key == 'exchangeTimezone':
tick[Datacode.TIMEZONE] = self.save_wrapper(lambda: str(value))
elif key == 'priceChange':
tick[Datacode.CHANGE] = self.save_wrapper(lambda: float(value))
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':
tick[Datacode.CHANGE_IN_PERCENT] = self.save_wrapper(lambda: (float(value)))
elif key == 'price':
locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
tick[Datacode.LAST_PRICE] = self.save_wrapper(lambda: locale.atof(str(value)))
elif key == 'priceCurrency':
tick[Datacode.CURRENCY] = self.save_wrapper(lambda: str(value))
elif key == 'exchange':
tick[Datacode.EXCHANGE] = self.save_wrapper(lambda: str(value))
elif key == 'name':
tick[Datacode.NAME] = self.save_wrapper(lambda: html.unescape(str(value)))
elif key == 'tickerSymbol':
tick[Datacode.TICKER] = self.save_wrapper(lambda: str(value))
else:
logger.info('ignored key=%s value=%s', key, value)
start = 0
r = '<td[^>]+data-snapfield="range">[^<]+</td>\s*<td class="val">\s*([^<]+)\s*</td>'
pattern = re.compile(r, flags=re.DOTALL)
match = pattern.search(text, start)
if match:
lowhigh = self.save_wrapper(
lambda: list(map(
lambda s: float(s),
html.unescape(match.group(1))
.replace('-', '').replace(',', '').strip().split())))
if lowhigh and len(lowhigh) == 2:
tick[Datacode.LOW] = lowhigh[0]
tick[Datacode.HIGH] = lowhigh[1]
start = match.span(0)[1]
r = '<td[^>]+data-snapfield="range_52week">[^<]+</td>\s*<td class="val">\s*([^<]+)\s*</td>'
pattern = re.compile(r, flags=re.DOTALL)
match = pattern.search(text, start)
if match:
lowhigh = self.save_wrapper(
lambda: list(map(
lambda s: float(s),
html.unescape(match.group(1))
.replace('-', '').replace(',', '').strip().split())))
if lowhigh and len(lowhigh) == 2:
tick[Datacode.LOW_52_WEEK] = lowhigh[0]
tick[Datacode.HIGH_52_WEEK] = lowhigh[1]
start = match.span(0)[1]
r = '<td[^>]+data-snapfield="open">[^<]+</td>\s*<td class="val">\s*([^<]+)\s*</td>'
pattern = re.compile(r, flags=re.DOTALL)
match = pattern.search(text, start)
if match:
tick[Datacode.OPEN] = self.save_wrapper(
lambda: float(html.unescape(match.group(1)).replace(',', '').strip()))
start = match.span(0)[1]
r = '<td[^>]+data-snapfield="vol_and_avg">[^<]+</td>\s*<td class="val">\s*([^<]+)\s*</td>'
pattern = re.compile(r, flags=re.DOTALL)
match = pattern.search(text, start)
if match:
volavg = self.save_wrapper(
lambda: list(map(
lambda s: handle_abbreviations(s),
html.unescape(match.group(1)).replace('/', ' ').strip().split())))
if volavg:
if len(volavg) > 0:
tick[Datacode.VOLUME] = volavg[0]
start = match.span(0)[1]
r = '<td[^>]+data-snapfield="market_cap">[^<]+</td>\s*<td class="val">\s*([^<]+)'
pattern = re.compile(r, flags=re.DOTALL)
match = pattern.search(text, start)
if match:
mcap = self.save_wrapper(
lambda: handle_abbreviations(html.unescape(match.group(1)).replace('-', ' ').strip()))
if mcap:
tick[Datacode.MARKET_CAP] = mcap
# start = match.span(0)[1]
tick[Datacode.TIMESTAMP] = time.time()
if tick[Datacode.EXCHANGE] == 'CURRENCY' and Datacode.CURRENCY not in tick:
tick[Datacode.CURRENCY] = ''
logger.info(tick)
except BaseException as e:
logger.warning(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)
-260
View File
@@ -1,260 +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 dateutil
import locale
import logging
import html
import os
import pathlib
import re
import time
import traceback
import xml.etree.ElementTree as ET
from naivehtmlparser import NaiveHTMLParser
from datacode import Datacode
from baseclient import BaseClient, RedirectException
logger = logging.getLogger(__name__)
# logger.setLevel(logging.DEBUG)
def handle_abbreviations(s):
s = str(s).strip()
if s.endswith('T'):
return float(s.replace('T', ''))*1000
if s.endswith('M'):
return float(s.replace('M', ''))*1000000
if s.endswith('B'):
return float(s.replace('B', ''))*1000000000
return float(s)
def un_span(s):
return re.sub(r'<span [^>]*>', '', s).replace('</span>', '')
class Google(BaseClient):
def __init__(self, ctx):
super().__init__()
self.realtime = {}
self.location = None
self.basedir = os.path.join(str(pathlib.Path.home()), '.financials-extension')
os.makedirs(self.basedir, exist_ok=True)
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]
q_param = 'q=' + ticker
if not self.location:
url = 'https://www.google.com/search?hl=en&tbm=fin&' + q_param
try:
self.urlopen(url, redirect=False)
except RedirectException as e:
self.location = e.location.replace('&' + q_param, '')
except BaseException as e:
logger.error(traceback.format_exc())
return 'Google.getRealtime(\'{}\', {}) - location: {}'.format(ticker, datacode, e)
if not self.location:
url = 'https://www.google.com/search?tbm=fin&' + q_param
else:
url = self.location + '&' + q_param
try:
text = self.urlopen(url)
with open(os.path.join(self.basedir, 'google-{}.html'.format(ticker)), "w") as text_file:
print(text, file=text_file)
except BaseException as e:
logger.error(traceback.format_exc())
return 'Google.getRealtime(\'{}\', {}) - urlopen: {} {}'.format(ticker, datacode, e, url)
if ticker not in self.realtime:
self.realtime[ticker] = {}
tick = self.realtime[ticker]
try:
r = '<div [^>]+ role="heading">'
pattern = re.compile(r)
# ignore first <div ... role="heading">
match = pattern.search(text)
if not match:
return 'Google.getRealtime({}, {}) - no match'.format(ticker, datacode)
start = match.span(0)[1]
# after second <div ... role="heading"> - get name
match = pattern.search(text, start)
if not match:
return 'Google.getRealtime({}, {}) - no match'.format(ticker, datacode)
start = match.span(0)[1]
r = '<div [^>]*>(.*?)</div>'
pattern = re.compile(r)
# first div ignored
match = pattern.search(text, start)
if not match:
return 'Google.getRealtime({}, {}) - no match'.format(ticker, datacode)
start = match.span(0)[1]
# second div is NAME
match = pattern.search(text, start)
if not match:
return 'Google.getRealtime({}, {}) - no match'.format(ticker, datacode)
start = match.span(0)[1]
tick[Datacode.NAME] = self.save_wrapper(
lambda: html.unescape(un_span(match.group(1)).strip()))
# third div is TICKER
match = pattern.search(text, start)
if not match:
return 'Google.getRealtime({}, {}) - no match'.format(ticker, datacode)
ticker = self.save_wrapper(
lambda: html.unescape(match.group(1)).replace(' ', ''))
tick[Datacode.EXCHANGE] = self.save_wrapper(lambda: ticker.split(':')[0])
tick[Datacode.TICKER] = self.save_wrapper(lambda: ticker.split(':')[1])
except BaseException as e:
return 'Google.getRealtime({}, {}) - process: {}'.format(ticker, datacode, e)
try:
r = '<sticky-header [^>]*>(.*?)</sticky-header>'
pattern = re.compile(r, flags=re.DOTALL)
match = re.search(pattern, text)
if match:
text = match.group(1)
else:
return 'Data for \'{}\' not found'.format(ticker)
parser = NaiveHTMLParser()
root = parser.feed(text)
parser.close()
cards = root.findall('.//g-card-section')
if len(cards) < 4:
return 'Data for \'{}\' not found'.format(ticker)
header = cards[1]
locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
tick[Datacode.LAST_PRICE] = self.save_wrapper(
lambda: locale.atof(
html.unescape(header.find('./div[1]/span[1]/span[1]/span[1]').text).strip()))
tick[Datacode.CURRENCY] = self.save_wrapper(
lambda: html.unescape(header.find('./div[1]/span[1]/span[1]/span[2]').text).strip())
tick[Datacode.CHANGE] = self.save_wrapper(
lambda: locale.atof(
html.unescape(header.find('./div[1]/span[2]/span[1]').text).replace('', '-').strip()))
tick[Datacode.CHANGE_IN_PERCENT] = self.save_wrapper(
lambda: float(
html.unescape(header.find('./div[1]/span[2]/span[2]/span[1]').text).strip()
.replace('(', '').replace(')', '').replace('%', '')))
try:
value = html.unescape(header.find('./div[2]/span[1]/span[2]').text).replace('·', '').strip()
logger.debug(value)
dt = dateutil.parser.parse(value)
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 as e:
pass
footer = cards[3]
logger.debug(ET.tostring(footer))
# parse 'footer' for remaining fields
table = footer.find('./div[1]/div[1]/div[1]/table[1]')
tick[Datacode.OPEN] = self.save_wrapper(
lambda: float(
html.unescape(table.find('./tr[1]/td[2]').text).replace(',', '').strip()))
tick[Datacode.HIGH] = self.save_wrapper(
lambda: float(
html.unescape(table.find('./tr[2]/td[2]').text).replace(',', '').strip()))
tick[Datacode.LOW] = self.save_wrapper(
lambda: float(
html.unescape(table.find('./tr[3]/td[2]').text).replace(',', '').strip()))
tick[Datacode.MARKET_CAP] = self.save_wrapper(
lambda: handle_abbreviations(
html.unescape(table.find('./tr[4]/td[2]').text).replace(',', '').replace('-', '').strip()))
table = footer.find('./div[1]/div[1]/div[2]/table[1]')
tick[Datacode.PREV_CLOSE] = self.save_wrapper(
lambda: float(
html.unescape(table.find('./tr[2]/td[2]').text).replace(',', '').strip()))
tick[Datacode.HIGH_52_WEEK] = self.save_wrapper(
lambda: float(
html.unescape(table.find('./tr[3]/td[2]').text).replace(',', '').strip()))
tick[Datacode.LOW_52_WEEK] = self.save_wrapper(
lambda: float(
html.unescape(table.find('./tr[4]/td[2]').text).replace(',', '').strip()))
tick[Datacode.TIMESTAMP] = time.time()
logger.info(tick)
except BaseException as e:
logger.warning(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)
-118
View File
@@ -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)
+42
View File
@@ -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
View File
@@ -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)
-280
View File
@@ -1,280 +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 argparse
import logging
import sys
import unittest
import financials
from datacode import Datacode
financials = financials.createInstance(None)
logging.basicConfig(level=logging.ERROR)
class Test(unittest.TestCase):
def test_currency(self):
s = financials.getRealtime('EURGBP', Datacode.LAST_PRICE.value, 'GOOGLE')
self.assertEqual('Google.getRealtime(EURGBP, 21) - no match', s, '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('LON:VOD', Datacode.LAST_PRICE.value, 'GOOGLE')
self.assertEqual(type(s), float, 'test_UK_equity LAST_PRICE {}'.format(s))
s = financials.getRealtime('LON:VOD', Datacode.TICKER.value, 'GOOGLE')
self.assertEqual(s, 'VOD', 'test_UK_equity TICKER')
s = financials.getRealtime('LON:VOD', Datacode.NAME.value, 'GOOGLE')
self.assertEqual(type(s), str, 'test_UK_equity NAME')
s = financials.getRealtime('LON:VOD', Datacode.EXCHANGE.value, 'GOOGLE')
self.assertEqual(s, 'LON', 'test_UK_equity EXCHANGE')
s = financials.getRealtime('LON:VOD', Datacode.PREV_CLOSE.value, 'GOOGLE')
self.assertEqual(type(s), float, 'test_UK_equity PREV_CLOSE {}'.format(s))
# MARKET_CAP missing for UK stock but available for German stock - weekend issue (FX) ?
s = financials.getRealtime('LON:VOD', Datacode.MARKET_CAP.value, 'GOOGLE')
self.assertEqual(type(s), float, 'test_UK_equity MARKET_CAP {}'.format(s))
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 {}'.format(s))
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 {}'.format(s))
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')
self.assertTrue(s == 'CET' or s == 'CEST', 'test_DE_equity TIMEZONE: {}'.format(s))
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 {}'.format(s))
s = financials.getRealtime('FRA:C060', Datacode.CURRENCY.value, 'GOOGLE')
self.assertEqual(s, 'EUR', 'test_DE_ETF CURRENCY')
s = financials.getRealtime('FRA:C060', Datacode.TICKER.value, 'GOOGLE')
self.assertEqual(s, 'C060', 'test_DE_ETF TICKER')
s = financials.getRealtime('FRA:C060', Datacode.EXCHANGE.value, 'GOOGLE')
self.assertEqual(s, 'FRA', 'test_DE_ETF EXCHANGE')
s = financials.getRealtime('FRA:C060', Datacode.CURRENCY.value, 'GOOGLE')
self.assertEqual(s, 'EUR', 'test_DE_ETF CURRENCY')
s = financials.getRealtime('FRA:C060', Datacode.MARKET_CAP.value, 'GOOGLE')
self.assertEqual(s, 'Data doesn\'t exist - 27', 'test_DE_ETF TIMESTAMP {}'.format(s))
def test_TY_equity(self):
s = financials.getRealtime('TYO:6503', Datacode.OPEN.value, 'GOOGLE')
self.assertEqual(type(s), float, 'test_TY_equity OPEN {}'.format(s))
s = financials.getRealtime('TYO:6503', Datacode.LOW.value, 'GOOGLE')
self.assertEqual(type(s), float, 'test_TY_equity LOW {}'.format(s))
s = financials.getRealtime('TYO:6503', Datacode.HIGH.value, 'GOOGLE')
self.assertEqual(type(s), float, 'test_TY_equity HIGH {}'.format(s))
s = financials.getRealtime('TYO:6503', Datacode.LOW_52_WEEK.value, 'GOOGLE')
self.assertEqual(type(s), float, 'test_TY_equity LOW_52_WEEK {}'.format(s))
s = financials.getRealtime('TYO:6503', Datacode.HIGH_52_WEEK.value, 'GOOGLE')
self.assertEqual(type(s), float, 'test_TY_equity HIGH_52_WEEK {}'.format(s))
s = financials.getRealtime('TYO:6503', Datacode.MARKET_CAP.value, 'GOOGLE')
self.assertEqual(type(s), float, 'test_TY_equity MARKET_CAP {}'.format(s))
# s = financials.getRealtime('TYO:6503', Datacode.VOLUME.value, 'GOOGLE')
# self.assertEqual(type(s), float, 'test_TY_equity VOLUME {}'.format(s))
s = financials.getRealtime('TYO:6503', Datacode.CURRENCY.value, 'GOOGLE')
self.assertEqual(s, 'JPY', 'test_TY_equity 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 {}'.format(s))
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 {}'.format(s))
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')
self.assertEqual(s, 'IBM Common Stock', 'test_US_equity NAME')
s = financials.getRealtime('NYSE:IBM', Datacode.LOW.value, 'GOOGLE')
self.assertEqual(type(s), float, 'test_US_equity LOW {}'.format(s))
s = financials.getRealtime('NYSE:IBM', Datacode.HIGH.value, 'GOOGLE')
self.assertEqual(type(s), float, 'test_US_equity HIGH {}'.format(s))
s = financials.getRealtime('NYSE:IBM', Datacode.LOW_52_WEEK.value, 'GOOGLE')
self.assertEqual(type(s), float, 'test_US_equity LOW_52_WEEK {}'.format(s))
s = financials.getRealtime('NYSE:IBM', Datacode.HIGH_52_WEEK.value, 'GOOGLE')
self.assertEqual(type(s), float, 'test_US_equity HIGH_52_WEEK {}'.format(s))
s = financials.getRealtime('NYSE:IBM', Datacode.MARKET_CAP.value, 'GOOGLE')
self.assertEqual(type(s), float, 'test_US_equity MARKET_CAP {}'.format(s))
# s = financials.getRealtime('NYSE:IBM', Datacode.VOLUME.value, 'GOOGLE')
# self.assertEqual(type(s), float, 'test_US_equity VOLUME {}'.format(s))
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')
self.assertEqual(s, 'GMT-4', 'test_US_equity TIMEZONE')
# self.assertEqual(s, 'GMT-5', '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 - {}'.format(s))
s = financials.getRealtime('MUTF:VFIAX', Datacode.CURRENCY.value, 'GOOGLE')
self.assertEqual(s, 'USD', 'test_US_mutuals CURRENCY')
s = financials.getRealtime('MUTF:VFIAX', Datacode.TIMEZONE.value, 'GOOGLE')
self.assertEqual(s, 'Data doesn\'t exist - 105', 'test_US_mutuals')
def test_index(self):
s = financials.getRealtime('INDEXDB:DAX', Datacode.LAST_PRICE.value, 'GOOGLE')
self.assertEqual(type(s), float, 'test_index LAST_PRICE {}'.format(s))
s = financials.getRealtime('INDEXDB:DAX', Datacode.CHANGE_IN_PERCENT.value, 'GOOGLE')
self.assertEqual(type(s), float, 'test_index CHANGE_IN_PERCENT')
s = financials.getRealtime('INDEXDB:DAX', Datacode.CHANGE.value, 'GOOGLE')
self.assertEqual(type(s), float, 'test_index CHANGE')
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, 'Google.getRealtime(DOES_NOT_EXISTS, 21) - no match', '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, '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__':
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)
+57
View File
@@ -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)
+342 -60
View File
@@ -14,126 +14,407 @@ 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)
logging.basicConfig(level=logging.ERROR)
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(type(s), str, 'test_currency CURRENCY')
self.assertEqual(str, type(s), 'test_currency CURRENCY')
s = financials.getRealtime('EURGBP=X', Datacode.LAST_PRICE.value, 'YAHOO')
self.assertEqual(type(s), float, 'test_currency LAST_PRICE')
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(type(s), float, 'test_realtime_US_equity HIGH_52_WEEK {}'.format(s))
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(type(s), float, 'test_realtime_US_equity LOW_52_WEEK {}'.format(s))
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(type(s), float, 'test_realtime_US_equity MARKET_CAP {}'.format(s))
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(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.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('C060.DE', 104, 'YAHOO')
self.assertEqual(type(s), str, 't_realtime_UK_ETF AME {}'.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:
@@ -142,16 +423,15 @@ class Test(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))
# Note: quarterly dividend and splits will change past adjusted prices
s = financials.getHistoric('IBM', Datacode.ADJ_CLOSE.value, '2017-01-03', 'YAHOO')
self.assertEqual(s, 152.209076, '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:
@@ -159,22 +439,14 @@ class Test(unittest.TestCase):
financials.yahoo.historicdata = {}
# Inception Date 2014-09-30
s = financials.getHistoric('VERX.L', Datacode.CLOSE.value, '2018-04-02', 'YAHOO') # Easter Monday
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.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, '2018-04-02', 'YAHOO')
@@ -185,13 +457,13 @@ class Test(unittest.TestCase):
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):
@@ -199,16 +471,25 @@ class Test(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_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')
@@ -221,7 +502,7 @@ class Test(unittest.TestCase):
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_historic_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_historic_errors CLOSE {}'.format(s))
@@ -233,7 +514,8 @@ class Test(unittest.TestCase):
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_historic_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_historic_errors CLOSE {}'.format(s))
+10
View File
@@ -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
+232
View File
@@ -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,
}
-332
View File
@@ -1,332 +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 logging
import os
import pathlib
import pprint
import pytz
import re
import time
import traceback
import urllib.parse
from datacode import Datacode
from baseclient import BaseClient, HttpException
from http import cookiejar
import jsonParser
logger = logging.getLogger(__name__)
# logger.setLevel(logging.DEBUG)
def raw(m, key, default=0.0):
try:
return m[key]['raw']
except:
pass
return default
class Yahoo(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 5 minutes
if ticker in self.realtime:
tick = self.realtime[ticker]
if time.time() - 5*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)
cookies = [cookiejar.Cookie(version=0,
name="B",
value="9898htldgiar5&b=3&s=gt",
port=None, port_specified=None,
domain=".yahoo.com", domain_specified=True, domain_initial_dot=True,
path="/", path_specified=True,
secure=True,
expires=None,
discard=False,
comment=None,
comment_url=None,
rest=None)
]
try:
text = self.urlopen(url, redirect=True, data=None, headers=None, cookies=cookies)
except BaseException as e:
logger.error(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 = pattern.search(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:
logger.error(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 None
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
except BaseException as e:
logger.error(traceback.format_exc())
return 'Yahoo.getRealtime({}, {}) - parsing: {}'.format(ticker, datacode, e)
try:
price = results['price']
quoteType = results['quoteType']
summaryDetail = results['summaryDetail']
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'))
tick[Datacode.LOW_52_WEEK] = float(raw(summaryDetail, 'fiftyTwoWeekLow'))
tick[Datacode.HIGH_52_WEEK] = float(raw(summaryDetail, 'fiftyTwoWeekHigh'))
tick[Datacode.MARKET_CAP] = float(raw(summaryDetail, 'marketCap'))
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.TICKER]
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)
logger.error(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
# 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_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 un-fetched
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:
logger.error(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 HttpException:
logger.error(traceback.format_exc())
return None
except BaseException as e:
logger.error(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:
logger.error(traceback.format_exc())
return 'Yahoo.getHistoric({}, {}, {}) - process: {}'.format(ticker, datacode, date, e)
return None
def createInstance(ctx):
return Yahoo(ctx)