tsigna 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tsigna-0.1.0/.gitignore +2 -0
- tsigna-0.1.0/LICENSE +21 -0
- tsigna-0.1.0/PKG-INFO +90 -0
- tsigna-0.1.0/README.md +73 -0
- tsigna-0.1.0/img/tsigna-nvda-1y-75.png +0 -0
- tsigna-0.1.0/img/tsigna-nvda-bollinger-bands-stochastics-1y.png +0 -0
- tsigna-0.1.0/img/tsigna-nvda-moving-averages-volume-1y.png +0 -0
- tsigna-0.1.0/img/tsigna-nvda-rsi-1y-75.png +0 -0
- tsigna-0.1.0/img/tsigna-nvda-wmt-2y-75.png +0 -0
- tsigna-0.1.0/img/tsigna-nvda-wmt-moving-averages-rsi-2y.png +0 -0
- tsigna-0.1.0/pyproject.toml +35 -0
- tsigna-0.1.0/tsigna/__init.py__ +0 -0
- tsigna-0.1.0/tsigna/main.py +656 -0
tsigna-0.1.0/.gitignore
ADDED
tsigna-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Monsieur Linux
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
tsigna-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tsigna
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Terminal tool to plot stocks, crypto, pair ratios, and technical indicators like MACD and RSI.
|
|
5
|
+
Project-URL: Repository, https://github.com/monsieurlinux/tsigna
|
|
6
|
+
Author-email: Monsieur Linux <info@mlinux.ca>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: bitcoin,cli,command-line,crypto,cryptocurrency,finance,python,stock-market,terminal,trading,tui
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Requires-Dist: pandas<3.0.0,>=2.0.0
|
|
14
|
+
Requires-Dist: plotille>=5.0.0
|
|
15
|
+
Requires-Dist: yahooquery>=2.4.1
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+

|
|
19
|
+
|
|
20
|
+
# Tsigna
|
|
21
|
+
|
|
22
|
+
Tsigna is a Python financial analysis tool that runs entirely in the terminal. It is most useful for medium-term trading. It fetches historical stock data from Yahoo Finance, calculates technical indicators including moving averages, MACD, and RSI, and displays them as text-based charts using the plotille library. The tool supports single ticker analysis, ratio comparisons between two tickers, and a special MMRI calculation. Users can customize the time period and split the terminal display to show multiple indicators simultaneously.
|
|
23
|
+
|
|
24
|
+

|
|
25
|
+
|
|
26
|
+
## Background
|
|
27
|
+
|
|
28
|
+
Originally I was looking for a free online tool to plot the **ratio between two tickers**, but I didn't find such a tool so I started working on Tsigna. The name comes from 'T' for terminal and the plural form of signum, the latin word for signal. The 'T' also stands for technical, like in technical indicators, from which we get technical signals.
|
|
29
|
+
|
|
30
|
+

|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
Tsigna has been developped with Python 3.11 but may work with older versions. It depends on the [pandas](https://github.com/pandas-dev/pandas), [plotille](https://github.com/tammoippen/plotille) and [yahooquery](https://github.com/dpguthrie/yahooquery) external libraries and their dependencies. They will all be installed automatically with the following command. It is recommended to make the installation within a [virtual environment](https://docs.python.org/3/tutorial/venv.html).
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install tsignal
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
### Basic Usage
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
tsigna [arguments] ticker1 [ticker2]
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Command-Line Arguments
|
|
49
|
+
|
|
50
|
+
| Argument | Short Flag | Description |
|
|
51
|
+
| ------------------ | ---------- | -------------------------------------------------- |
|
|
52
|
+
| `--help` | `-h` | Show help message |
|
|
53
|
+
| `--atr` | `-a` | Display ATR indicator (Average True Range) |
|
|
54
|
+
| `--atr-only` | `-A` | Display **only** ATR indicator |
|
|
55
|
+
| `--bollinger` | `-b` | Display Bollinger Bands indicator |
|
|
56
|
+
| `--mfi` | `-f` | Display MFI indicator (Money Flow Index) |
|
|
57
|
+
| `--mfi-only` | `-F` | Display **only** MFI indicator |
|
|
58
|
+
| `--indicator-info` | `-i` | Show indicator information |
|
|
59
|
+
| `--macd` | `-m` | Display MACD indicator (Moving Average Convergence Divergence) |
|
|
60
|
+
| `--macd-only` | `-M` | Display **only** MACD indicator |
|
|
61
|
+
| `--no-cache` | `-n` | Bypass cache and get latest data |
|
|
62
|
+
| `--obv` | `-o` | Display OBV indicator (On-Balance Volume) |
|
|
63
|
+
| `--obv-only` | `-O` | Display **only** OBV indicator |
|
|
64
|
+
| `--rsi` | `-r` | Display RSI indicator (Relative Strength Index) |
|
|
65
|
+
| `--rsi-only` | `-R` | Display **only** RSI indicator |
|
|
66
|
+
| `--stoch` | `-s` | Display Stochastics indicator |
|
|
67
|
+
| `--stoch-only` | `-S` | Display **only** Stochastics indicator |
|
|
68
|
+
| `--volume` | `-v` | Display volume |
|
|
69
|
+
| `--volume-only` | `-V` | Display **only** volume |
|
|
70
|
+
| `--years` | `-y` | Set years to plot, use 0 for ytd (default: 1) |
|
|
71
|
+
|
|
72
|
+
## Configuration
|
|
73
|
+
|
|
74
|
+
You can edit the configuration constants directly at the top of the tsigna.py file if you wish to change the default behavior. For example you can change the expiration time of the cache (it is 5 minutes by default) or disable it. You can also change the colors of the lines, the parameters of the technical indicators, etc.
|
|
75
|
+
|
|
76
|
+
## Not Financial Advice
|
|
77
|
+
|
|
78
|
+
I do not own any of the stocks in the examples, I chose them because they are very popular.
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
Copyright (c) 2025 Monsieur Linux
|
|
83
|
+
|
|
84
|
+
This project is licensed under the MIT License. See the LICENSE file for details.
|
|
85
|
+
|
|
86
|
+
## Acknowledgements
|
|
87
|
+
|
|
88
|
+
Tsigna is not doing much more than getting data from [yahooquery](https://github.com/dpguthrie/yahooquery), processing it with [pandas](https://github.com/pandas-dev/pandas), and plotting it with [plotille](https://github.com/tammoippen/plotille), so thanks to the creators and contributors of these powerful libraries for making it possible.
|
|
89
|
+
|
|
90
|
+
Thanks also to the [ticker](https://github.com/achannarasappa/ticker) tool, which is very useful to track prices in real time from the terminal.
|
tsigna-0.1.0/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
# Tsigna
|
|
4
|
+
|
|
5
|
+
Tsigna is a Python financial analysis tool that runs entirely in the terminal. It is most useful for medium-term trading. It fetches historical stock data from Yahoo Finance, calculates technical indicators including moving averages, MACD, and RSI, and displays them as text-based charts using the plotille library. The tool supports single ticker analysis, ratio comparisons between two tickers, and a special MMRI calculation. Users can customize the time period and split the terminal display to show multiple indicators simultaneously.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## Background
|
|
10
|
+
|
|
11
|
+
Originally I was looking for a free online tool to plot the **ratio between two tickers**, but I didn't find such a tool so I started working on Tsigna. The name comes from 'T' for terminal and the plural form of signum, the latin word for signal. The 'T' also stands for technical, like in technical indicators, from which we get technical signals.
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
Tsigna has been developped with Python 3.11 but may work with older versions. It depends on the [pandas](https://github.com/pandas-dev/pandas), [plotille](https://github.com/tammoippen/plotille) and [yahooquery](https://github.com/dpguthrie/yahooquery) external libraries and their dependencies. They will all be installed automatically with the following command. It is recommended to make the installation within a [virtual environment](https://docs.python.org/3/tutorial/venv.html).
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install tsignal
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
### Basic Usage
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
tsigna [arguments] ticker1 [ticker2]
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Command-Line Arguments
|
|
32
|
+
|
|
33
|
+
| Argument | Short Flag | Description |
|
|
34
|
+
| ------------------ | ---------- | -------------------------------------------------- |
|
|
35
|
+
| `--help` | `-h` | Show help message |
|
|
36
|
+
| `--atr` | `-a` | Display ATR indicator (Average True Range) |
|
|
37
|
+
| `--atr-only` | `-A` | Display **only** ATR indicator |
|
|
38
|
+
| `--bollinger` | `-b` | Display Bollinger Bands indicator |
|
|
39
|
+
| `--mfi` | `-f` | Display MFI indicator (Money Flow Index) |
|
|
40
|
+
| `--mfi-only` | `-F` | Display **only** MFI indicator |
|
|
41
|
+
| `--indicator-info` | `-i` | Show indicator information |
|
|
42
|
+
| `--macd` | `-m` | Display MACD indicator (Moving Average Convergence Divergence) |
|
|
43
|
+
| `--macd-only` | `-M` | Display **only** MACD indicator |
|
|
44
|
+
| `--no-cache` | `-n` | Bypass cache and get latest data |
|
|
45
|
+
| `--obv` | `-o` | Display OBV indicator (On-Balance Volume) |
|
|
46
|
+
| `--obv-only` | `-O` | Display **only** OBV indicator |
|
|
47
|
+
| `--rsi` | `-r` | Display RSI indicator (Relative Strength Index) |
|
|
48
|
+
| `--rsi-only` | `-R` | Display **only** RSI indicator |
|
|
49
|
+
| `--stoch` | `-s` | Display Stochastics indicator |
|
|
50
|
+
| `--stoch-only` | `-S` | Display **only** Stochastics indicator |
|
|
51
|
+
| `--volume` | `-v` | Display volume |
|
|
52
|
+
| `--volume-only` | `-V` | Display **only** volume |
|
|
53
|
+
| `--years` | `-y` | Set years to plot, use 0 for ytd (default: 1) |
|
|
54
|
+
|
|
55
|
+
## Configuration
|
|
56
|
+
|
|
57
|
+
You can edit the configuration constants directly at the top of the tsigna.py file if you wish to change the default behavior. For example you can change the expiration time of the cache (it is 5 minutes by default) or disable it. You can also change the colors of the lines, the parameters of the technical indicators, etc.
|
|
58
|
+
|
|
59
|
+
## Not Financial Advice
|
|
60
|
+
|
|
61
|
+
I do not own any of the stocks in the examples, I chose them because they are very popular.
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
Copyright (c) 2025 Monsieur Linux
|
|
66
|
+
|
|
67
|
+
This project is licensed under the MIT License. See the LICENSE file for details.
|
|
68
|
+
|
|
69
|
+
## Acknowledgements
|
|
70
|
+
|
|
71
|
+
Tsigna is not doing much more than getting data from [yahooquery](https://github.com/dpguthrie/yahooquery), processing it with [pandas](https://github.com/pandas-dev/pandas), and plotting it with [plotille](https://github.com/tammoippen/plotille), so thanks to the creators and contributors of these powerful libraries for making it possible.
|
|
72
|
+
|
|
73
|
+
Thanks also to the [ticker](https://github.com/achannarasappa/ticker) tool, which is very useful to track prices in real time from the terminal.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling >= 1.28"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tsigna"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Terminal tool to plot stocks, crypto, pair ratios, and technical indicators like MACD and RSI."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [
|
|
14
|
+
{name = "Monsieur Linux", email = "info@mlinux.ca"}
|
|
15
|
+
]
|
|
16
|
+
keywords = ["python", "cli", "cryptocurrency", "terminal", "bitcoin", "crypto", "command-line", "finance", "trading", "tui", "stock-market"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"pandas>=2.0.0,<3.0.0",
|
|
23
|
+
"plotille>=5.0.0",
|
|
24
|
+
"yahooquery>=2.4.1",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Repository = "https://github.com/monsieurlinux/tsigna"
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
tsigna = "tsigna.main:main"
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.wheel]
|
|
34
|
+
packages = ["tsigna"]
|
|
35
|
+
|
|
File without changes
|
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Terminal tool to plot stocks, crypto, pair ratios, and technical indicators.
|
|
5
|
+
|
|
6
|
+
This is a Python financial analysis tool that runs entirely in the terminal. It
|
|
7
|
+
fetches historical stock data from Yahoo Finance, calculates technical
|
|
8
|
+
indicators including moving averages, MACD, and RSI, and displays them as text-
|
|
9
|
+
based charts using the plotille library. The tool supports single ticker
|
|
10
|
+
analysis, ratio comparisons between two tickers, and a special MMRI calculation.
|
|
11
|
+
Users can customize the time period and split the terminal display to show
|
|
12
|
+
multiple indicators simultaneously.
|
|
13
|
+
|
|
14
|
+
Copyright (c) 2025 Monsieur Linux
|
|
15
|
+
|
|
16
|
+
Licensed under the MIT License. See the LICENSE file for details.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
# Standard library imports
|
|
20
|
+
import argparse
|
|
21
|
+
import logging
|
|
22
|
+
import math
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
import requests
|
|
25
|
+
from curl_cffi import requests as curlreqs
|
|
26
|
+
import shutil
|
|
27
|
+
import sys
|
|
28
|
+
import textwrap
|
|
29
|
+
import time
|
|
30
|
+
|
|
31
|
+
# Third-party library imports
|
|
32
|
+
import pandas as pd
|
|
33
|
+
import plotille
|
|
34
|
+
from yahooquery import Ticker # Alternative fork: ybankinplay
|
|
35
|
+
|
|
36
|
+
# Configuration constants
|
|
37
|
+
CACHE_ENABLE = True
|
|
38
|
+
CACHE_PATH = Path.home() / f'.{Path(__file__).stem}'
|
|
39
|
+
CACHE_EXPIRY = 300 # 300 seconds = 5 minutes
|
|
40
|
+
YEARS_TO_PLOT = 1
|
|
41
|
+
INDICATOR_HEIGHT_RATIO = 0.3
|
|
42
|
+
MMRI_DIVISOR = 1.61
|
|
43
|
+
MOVING_AVG_1 = 20
|
|
44
|
+
MOVING_AVG_2 = 50
|
|
45
|
+
MOVING_AVG_3 = 200
|
|
46
|
+
BB_PERIOD = 20
|
|
47
|
+
BB_STD_DEV = 2
|
|
48
|
+
MACD_FAST_LEN = 12
|
|
49
|
+
MACD_SLOW_LEN = 26
|
|
50
|
+
MACD_SIGNAL_LEN = 9
|
|
51
|
+
RSI_PERIOD = 14
|
|
52
|
+
RSI_OVERBOUGHT_LEVEL = 70
|
|
53
|
+
RSI_OVERSOLD_LEVEL = 30
|
|
54
|
+
MFI_PERIOD = 14
|
|
55
|
+
MFI_OVERBOUGHT_LEVEL = 80
|
|
56
|
+
MFI_OVERSOLD_LEVEL = 20
|
|
57
|
+
STOCH_K_PERIOD = 14
|
|
58
|
+
STOCH_K_SMOOTHING = 3 # Set to 1 for fast stochastics
|
|
59
|
+
STOCH_D_PERIOD = 3
|
|
60
|
+
STOCH_OVERBOUGHT_LEVEL = 80
|
|
61
|
+
STOCH_OVERSOLD_LEVEL = 20
|
|
62
|
+
ATR_PERIOD = 14
|
|
63
|
+
|
|
64
|
+
# Valid colors (standard 8-color ANSI palette):
|
|
65
|
+
# black, red, green, yellow, blue, magenta, cyan, and white
|
|
66
|
+
# An optional 'bright_' prefix can be added, e.g. 'bright_green'
|
|
67
|
+
TEXT_COLOR = 'white'
|
|
68
|
+
MAIN_LINE_COLOR = 'blue'
|
|
69
|
+
OVERBOUGHT_COLOR = 'red'
|
|
70
|
+
OVERSOLD_COLOR = 'green'
|
|
71
|
+
MOVING_AVG_1_COLOR = 'green'
|
|
72
|
+
MOVING_AVG_2_COLOR = 'yellow'
|
|
73
|
+
MOVING_AVG_3_COLOR = 'red'
|
|
74
|
+
BB_SMA_COLOR = 'yellow'
|
|
75
|
+
BB_UPPER_BAND_COLOR = 'red'
|
|
76
|
+
BB_LOWER_BAND_COLOR = 'green'
|
|
77
|
+
MACD_SIGNAL_COLOR = 'red'
|
|
78
|
+
MACD_HISTOGRAM_COLOR = 'green'
|
|
79
|
+
STOCH_K_COLOR = 'blue'
|
|
80
|
+
STOCH_D_COLOR = 'yellow'
|
|
81
|
+
|
|
82
|
+
INDICATOR_DESCRIPTIONS = {
|
|
83
|
+
"ATR (Average True Range)": {
|
|
84
|
+
"category": "Volatility Indicator",
|
|
85
|
+
"description": "Measures market volatility by decomposing the entire range of an asset price for that period. Use it to set stop-loss levels (placing them wider than the ATR) or to determine position sizing based on current market noise."
|
|
86
|
+
},
|
|
87
|
+
"Bollinger Bands": {
|
|
88
|
+
"category": "Volatility Indicator",
|
|
89
|
+
"description": "Consists of a middle band (SMA) and two outer bands that expand and contract based on volatility. Use it to identify overbought/oversold conditions (price near bands) and potential breakouts after periods of low volatility (squeeze)."
|
|
90
|
+
},
|
|
91
|
+
"MACD (Moving Average Convergence Divergence)": {
|
|
92
|
+
"category": "Trend / Momentum Indicator",
|
|
93
|
+
"description": "Shows the relationship between two moving averages. Look for signal line crossovers to identify trend direction and divergences (where price moves opposite to the indicator) to spot potential reversals."
|
|
94
|
+
},
|
|
95
|
+
"MFI (Money Flow Index)": {
|
|
96
|
+
"category": "Momentum Oscillator",
|
|
97
|
+
"description": "Incorporates both price and volume data to measure buying and selling pressure. Use it like RSI to identify overbought/oversold levels, but rely on it more heavily as volume-driven divergence is often a stronger signal."
|
|
98
|
+
},
|
|
99
|
+
"Moving Averages": {
|
|
100
|
+
"category": "Trend Indicator",
|
|
101
|
+
"description": "Smooths out price data to identify the direction of the trend. Use it to determine entry points (e.g., buy when price crosses above the line) or dynamic support/resistance levels."
|
|
102
|
+
},
|
|
103
|
+
"OBV (On-Balance Volume)": {
|
|
104
|
+
"category": "Volume Indicator",
|
|
105
|
+
"description": "A cumulative indicator that adds volume on up days and subtracts volume on down days. Use it to confirm the strength of a trend (e.g., rising price + rising OBV = strong trend) or spot reversals via divergence."
|
|
106
|
+
},
|
|
107
|
+
"RSI (Relative Strength Index)": {
|
|
108
|
+
"category": "Momentum Oscillator",
|
|
109
|
+
"description": "Measures the speed and change of price movements on a scale of 0 to 100. Use it to identify overbought conditions (above 70) or oversold conditions (below 30) and to spot bullish or bearish divergences."
|
|
110
|
+
},
|
|
111
|
+
"Stochastics": {
|
|
112
|
+
"category": "Momentum Oscillator",
|
|
113
|
+
"description": "Compares a particular closing price of an asset to a range of its prices over a certain period of time. Look for the lines to cross in overbought (above 80) or oversold (below 20) areas to time reversals."
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
INDICATOR_CATEGORIES = {
|
|
118
|
+
"Trend Indicator": "Identifies market direction.",
|
|
119
|
+
"Trend / Momentum Indicator": "Measures trend strength and direction.",
|
|
120
|
+
"Momentum Oscillator": "Identifies overbought or oversold levels.",
|
|
121
|
+
"Volatility Indicator": "Measures price fluctuation range.",
|
|
122
|
+
"Volume Indicator": "Confirms trend strength via activity.",
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# Get a logger for this script
|
|
126
|
+
logger = logging.getLogger(__name__)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def main():
|
|
130
|
+
parser = argparse.ArgumentParser()
|
|
131
|
+
|
|
132
|
+
parser.add_argument('ticker1', nargs='?',
|
|
133
|
+
help='first or only ticker (or special MMRI ticker)')
|
|
134
|
+
parser.add_argument('ticker2', nargs='?',
|
|
135
|
+
help='second ticker for ratio plot')
|
|
136
|
+
parser.add_argument('-a', '--atr', action='store_true',
|
|
137
|
+
help='display ATR indicator (Average True Range)')
|
|
138
|
+
parser.add_argument('-A', '--atr-only', action='store_true',
|
|
139
|
+
help='display only ATR indicator')
|
|
140
|
+
parser.add_argument('-b', '--bollinger', action='store_true',
|
|
141
|
+
help='display Bollinger Bands indicator')
|
|
142
|
+
parser.add_argument('-f', '--mfi', action='store_true',
|
|
143
|
+
help='display MFI indicator (Money Flow Index)')
|
|
144
|
+
parser.add_argument('-F', '--mfi-only', action='store_true',
|
|
145
|
+
help='display only MFI indicator')
|
|
146
|
+
parser.add_argument('-i', '--indicator-info', action='store_true',
|
|
147
|
+
help='show indicator information')
|
|
148
|
+
parser.add_argument('-m', '--macd', action='store_true',
|
|
149
|
+
help='display MACD indicator (Moving Average Convergence Divergence)')
|
|
150
|
+
parser.add_argument('-M', '--macd-only', action='store_true',
|
|
151
|
+
help='display only MACD indicator')
|
|
152
|
+
parser.add_argument('-n', '--no-cache', action='store_true',
|
|
153
|
+
help='bypass cache and get latest data')
|
|
154
|
+
parser.add_argument('-o', '--obv', action='store_true',
|
|
155
|
+
help='display OBV indicator (On-Balance Volume)')
|
|
156
|
+
parser.add_argument('-O', '--obv-only', action='store_true',
|
|
157
|
+
help='display only OBV indicator')
|
|
158
|
+
parser.add_argument('-r', '--rsi', action='store_true',
|
|
159
|
+
help='display RSI indicator (Relative Strength Index)')
|
|
160
|
+
parser.add_argument('-R', '--rsi-only', action='store_true',
|
|
161
|
+
help='display only RSI indicator')
|
|
162
|
+
parser.add_argument('-s', '--stoch', action='store_true',
|
|
163
|
+
help='display Stochastics indicator')
|
|
164
|
+
parser.add_argument('-S', '--stoch-only', action='store_true',
|
|
165
|
+
help='display only Stochastics indicator')
|
|
166
|
+
parser.add_argument('-v', '--volume', action='store_true',
|
|
167
|
+
help='display volume')
|
|
168
|
+
parser.add_argument('-V', '--volume-only', action='store_true',
|
|
169
|
+
help='display only volume')
|
|
170
|
+
parser.add_argument('-y', '--years', type=int, default=YEARS_TO_PLOT,
|
|
171
|
+
help='set years to plot, use 0 for ytd (default: 1)')
|
|
172
|
+
args = parser.parse_args()
|
|
173
|
+
|
|
174
|
+
ticker1, ticker2, plot_name = get_tickers_and_plot_name(args)
|
|
175
|
+
|
|
176
|
+
if args.indicator_info:
|
|
177
|
+
# User asked for indicator information
|
|
178
|
+
print_indicator_info()
|
|
179
|
+
return
|
|
180
|
+
elif not ticker1:
|
|
181
|
+
# User did not ask for information nor provide a ticker
|
|
182
|
+
parser.print_usage()
|
|
183
|
+
logger.error(f'Please provide a ticker or use -i for indicator information')
|
|
184
|
+
return
|
|
185
|
+
elif ticker2 and (args.mfi or args.mfi_only):
|
|
186
|
+
logger.error(f'MFI indicator not available for ratio plot')
|
|
187
|
+
return
|
|
188
|
+
elif ticker2 and (args.stoch or args.stoch_only):
|
|
189
|
+
logger.error(f'Stochastics indicator not available for ratio plot')
|
|
190
|
+
return
|
|
191
|
+
elif ticker2 and (args.atr or args.atr_only):
|
|
192
|
+
logger.error(f'ATR indicator not available for ratio plot')
|
|
193
|
+
return
|
|
194
|
+
elif ticker2 and (args.obv or args.obv_only):
|
|
195
|
+
logger.error(f'OBV indicator not available for ratio plot')
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
main_ind = 'bb' if args.bollinger else 'ma'
|
|
199
|
+
xtra_ind = []
|
|
200
|
+
if args.volume or args.volume_only: xtra_ind.append('vol')
|
|
201
|
+
if args.macd or args.macd_only: xtra_ind.append('macd')
|
|
202
|
+
if args.rsi or args.rsi_only: xtra_ind.append('rsi')
|
|
203
|
+
if args.mfi or args.mfi_only: xtra_ind.append('mfi')
|
|
204
|
+
if args.stoch or args.stoch_only: xtra_ind.append('stoch')
|
|
205
|
+
if args.atr or args.atr_only: xtra_ind.append('atr')
|
|
206
|
+
if args.obv or args.obv_only: xtra_ind.append('obv')
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
df1, df2 = get_data(ticker1, ticker2, no_cache=args.no_cache)
|
|
210
|
+
except KeyError as e:
|
|
211
|
+
logger.error(f'Invalid ticker: {e}')
|
|
212
|
+
except (requests.exceptions.RequestException,
|
|
213
|
+
curlreqs.exceptions.RequestException) as e:
|
|
214
|
+
logger.error(f'Network error: {e}')
|
|
215
|
+
except AssertionError as e:
|
|
216
|
+
logger.error(f'Assert failed: {e}')
|
|
217
|
+
except Exception as e:
|
|
218
|
+
logger.exception(f'Unexpected error: {e}')
|
|
219
|
+
else:
|
|
220
|
+
df = process_data(df1, df2, args.years, plot_name, main_ind, xtra_ind)
|
|
221
|
+
if args.volume_only:
|
|
222
|
+
plot_data(df, plot_name, 'vol')
|
|
223
|
+
elif args.macd_only:
|
|
224
|
+
plot_data(df, plot_name, 'macd')
|
|
225
|
+
elif args.rsi_only:
|
|
226
|
+
plot_data(df, plot_name, 'rsi')
|
|
227
|
+
elif args.mfi_only:
|
|
228
|
+
plot_data(df, plot_name, 'mfi')
|
|
229
|
+
elif args.stoch_only:
|
|
230
|
+
plot_data(df, plot_name, 'stoch')
|
|
231
|
+
elif args.atr_only:
|
|
232
|
+
plot_data(df, plot_name, 'atr')
|
|
233
|
+
elif args.obv_only:
|
|
234
|
+
plot_data(df, plot_name, 'obv')
|
|
235
|
+
else:
|
|
236
|
+
num_ind = len(xtra_ind)
|
|
237
|
+
if num_ind > 2:
|
|
238
|
+
logger.error(f'A maximum of two indicators can be displayed')
|
|
239
|
+
elif num_ind == 2:
|
|
240
|
+
plot_data(df, plot_name, main_ind, 1-2*INDICATOR_HEIGHT_RATIO)
|
|
241
|
+
plot_data(df, plot_name, xtra_ind[0], INDICATOR_HEIGHT_RATIO)
|
|
242
|
+
plot_data(df, plot_name, xtra_ind[1], INDICATOR_HEIGHT_RATIO)
|
|
243
|
+
elif num_ind == 1:
|
|
244
|
+
plot_data(df, plot_name, main_ind, 1-INDICATOR_HEIGHT_RATIO)
|
|
245
|
+
plot_data(df, plot_name, xtra_ind[0], INDICATOR_HEIGHT_RATIO)
|
|
246
|
+
else:
|
|
247
|
+
plot_data(df, plot_name, main_ind)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def get_tickers_and_plot_name(args):
|
|
251
|
+
ticker1 = args.ticker1.upper() if args.ticker1 else None
|
|
252
|
+
ticker2 = args.ticker2.upper() if args.ticker2 else None
|
|
253
|
+
plot_name = None
|
|
254
|
+
|
|
255
|
+
if ticker1 and ticker2:
|
|
256
|
+
# Plot the ratio between ticker1 and ticker2
|
|
257
|
+
plot_name = ticker1 + ' vs ' + ticker2
|
|
258
|
+
elif ticker1:
|
|
259
|
+
plot_name = ticker1
|
|
260
|
+
if ticker1 == 'MMRI':
|
|
261
|
+
# Special "ticker" to plot the Mannarino Market Risk Indicator
|
|
262
|
+
# MMRI = DX * 10Y / 1.61
|
|
263
|
+
ticker1 = 'DX=F'
|
|
264
|
+
ticker2 = '^TNX' # '10Y=F' has no historical data
|
|
265
|
+
|
|
266
|
+
return ticker1, ticker2, plot_name
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def get_data(ticker1, ticker2, no_cache=False):
|
|
270
|
+
fetch_data = True
|
|
271
|
+
df2 = pd.DataFrame()
|
|
272
|
+
CACHE_PATH.mkdir(parents=True, exist_ok=True)
|
|
273
|
+
path1 = Path(f'{CACHE_PATH}/{ticker1.lower()}.csv') if ticker1 else None
|
|
274
|
+
path2 = Path(f'{CACHE_PATH}/{ticker2.lower()}.csv') if ticker2 else None
|
|
275
|
+
|
|
276
|
+
if CACHE_ENABLE and not no_cache:
|
|
277
|
+
fetch_data = False
|
|
278
|
+
now = time.time()
|
|
279
|
+
|
|
280
|
+
if path1.is_file() and (now - path1.stat().st_mtime < CACHE_EXPIRY):
|
|
281
|
+
logger.info(f'Getting {ticker1} data from cache')
|
|
282
|
+
df1 = pd.read_csv(path1, parse_dates=['date'])
|
|
283
|
+
df1.set_index('date', inplace=True)
|
|
284
|
+
else:
|
|
285
|
+
fetch_data = True
|
|
286
|
+
|
|
287
|
+
if ticker2:
|
|
288
|
+
if path2.is_file() and (now - path2.stat().st_mtime < CACHE_EXPIRY):
|
|
289
|
+
logger.info(f'Getting {ticker2} data from cache')
|
|
290
|
+
df2 = pd.read_csv(path2, parse_dates=['date'])
|
|
291
|
+
df2.set_index('date', inplace=True)
|
|
292
|
+
else:
|
|
293
|
+
fetch_data = True
|
|
294
|
+
|
|
295
|
+
if fetch_data:
|
|
296
|
+
logger.info('Getting ticker(s) data from Yahoo Finance')
|
|
297
|
+
tickers = [ticker1, ticker2] if ticker2 else [ticker1]
|
|
298
|
+
tickers = Ticker(tickers)
|
|
299
|
+
|
|
300
|
+
df = tickers.history(period='10y', interval='1d')
|
|
301
|
+
df1 = df.loc[ticker1]
|
|
302
|
+
if CACHE_ENABLE: df1.to_csv(path1, index=True)
|
|
303
|
+
|
|
304
|
+
if ticker2:
|
|
305
|
+
df2 = df.loc[ticker2]
|
|
306
|
+
if CACHE_ENABLE: df2.to_csv(path2, index=True)
|
|
307
|
+
|
|
308
|
+
# Make sure all dates have the same format (remove time from last date)
|
|
309
|
+
# normalize() sets the time to midnight while keeping pandas dates types
|
|
310
|
+
df1.index = pd.to_datetime(df1.index,utc=True,format='ISO8601').normalize()
|
|
311
|
+
df2.index = pd.to_datetime(df2.index,utc=True,format='ISO8601').normalize()
|
|
312
|
+
assert df1.index.is_unique, f'Duplicate date for {ticker1}'
|
|
313
|
+
assert df2.index.is_unique, f'Duplicate date for {ticker2}'
|
|
314
|
+
df1 = df1.groupby(df1.index).last() # Make sure there are no duplicates
|
|
315
|
+
df2 = df2.groupby(df2.index).last()
|
|
316
|
+
|
|
317
|
+
return df1, df2
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def process_data(df1, df2, years, plot_name, main_ind = 'ma' , xtra_ind = []):
|
|
321
|
+
if df2.empty:
|
|
322
|
+
# Only one ticker has been provided, so this is the data to plot
|
|
323
|
+
df = df1
|
|
324
|
+
else:
|
|
325
|
+
# Two tickers has been provided, so compute the ratios between them
|
|
326
|
+
# Align the indices by finding dates present in both DataFrames
|
|
327
|
+
dates = df1.index.unique().intersection(df2.index)
|
|
328
|
+
|
|
329
|
+
# Extract the series for 'adjclose' only for the common dates
|
|
330
|
+
values1 = df1.loc[dates, 'adjclose']
|
|
331
|
+
values2 = df2.loc[dates, 'adjclose']
|
|
332
|
+
|
|
333
|
+
# Filter out dates where values are not positive
|
|
334
|
+
valid_mask = (values1 > 0) & (values2 > 0)
|
|
335
|
+
values1 = values1[valid_mask]
|
|
336
|
+
values2 = values2[valid_mask]
|
|
337
|
+
|
|
338
|
+
# Calculate the values using vectorized operations
|
|
339
|
+
if plot_name == 'MMRI':
|
|
340
|
+
values = (values1 * values2) / MMRI_DIVISOR
|
|
341
|
+
else:
|
|
342
|
+
values = values1 / values2
|
|
343
|
+
|
|
344
|
+
df = values.to_frame('adjclose')
|
|
345
|
+
|
|
346
|
+
# Calculate and add columns for requested indicators
|
|
347
|
+
if 'ma' in main_ind: df = add_moving_averages(df)
|
|
348
|
+
if 'bb' in main_ind: df = add_bollinger_bands(df)
|
|
349
|
+
if 'macd' in xtra_ind: df = add_macd(df)
|
|
350
|
+
if 'rsi' in xtra_ind: df = add_rsi(df)
|
|
351
|
+
|
|
352
|
+
if 'low' in df.columns:
|
|
353
|
+
# Indicators N/A for ratio plots (OHLC prices and/or volume required)
|
|
354
|
+
if 'mfi' in xtra_ind: df = add_mfi(df)
|
|
355
|
+
if 'stoch' in xtra_ind: df = add_stochastics(df)
|
|
356
|
+
if 'atr' in xtra_ind: df = add_atr(df)
|
|
357
|
+
if 'obv' in xtra_ind: df = add_obv(df)
|
|
358
|
+
|
|
359
|
+
# Keep only the data range to be plotted (use pandas dates types)
|
|
360
|
+
today = pd.Timestamp.now(tz='UTC').normalize()
|
|
361
|
+
|
|
362
|
+
if years == 0:
|
|
363
|
+
start_day = today.replace(month=1, day=1) # ytd plot
|
|
364
|
+
else:
|
|
365
|
+
start_day = today.replace(year=today.year - years)
|
|
366
|
+
|
|
367
|
+
df = df[df.index >= start_day]
|
|
368
|
+
|
|
369
|
+
logger.debug(f'today is {today}')
|
|
370
|
+
logger.debug(f'start_day is {start_day}')
|
|
371
|
+
|
|
372
|
+
return df
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def plot_data(df, plot_name, plot_type, height_ratio=1):
|
|
376
|
+
# Display the plot in the terminal
|
|
377
|
+
dates = df.index.tolist()
|
|
378
|
+
|
|
379
|
+
if plot_type == 'vol':
|
|
380
|
+
volume = df['volume'].tolist()
|
|
381
|
+
all_values = volume
|
|
382
|
+
elif plot_type == 'macd':
|
|
383
|
+
macd = df['macd'].tolist()
|
|
384
|
+
signal = df['signal'].tolist()
|
|
385
|
+
histogram = df['histogram'].tolist()
|
|
386
|
+
all_values = macd + signal + histogram
|
|
387
|
+
elif plot_type == 'rsi':
|
|
388
|
+
rsi = df['rsi'].tolist()
|
|
389
|
+
overbought = [RSI_OVERBOUGHT_LEVEL] * len(dates)
|
|
390
|
+
oversold = [RSI_OVERSOLD_LEVEL] * len(dates)
|
|
391
|
+
all_values = rsi + overbought + oversold
|
|
392
|
+
elif plot_type == 'mfi':
|
|
393
|
+
mfi = df['mfi'].tolist()
|
|
394
|
+
overbought = [MFI_OVERBOUGHT_LEVEL] * len(dates)
|
|
395
|
+
oversold = [MFI_OVERSOLD_LEVEL] * len(dates)
|
|
396
|
+
all_values = mfi + overbought + oversold
|
|
397
|
+
elif plot_type == 'stoch':
|
|
398
|
+
stoch_k = df['stoch_k'].tolist()
|
|
399
|
+
stoch_d = df['stoch_d'].tolist()
|
|
400
|
+
overbought = [STOCH_OVERBOUGHT_LEVEL] * len(dates)
|
|
401
|
+
oversold = [STOCH_OVERSOLD_LEVEL] * len(dates)
|
|
402
|
+
all_values = stoch_k + stoch_d + overbought + oversold
|
|
403
|
+
elif plot_type == 'atr':
|
|
404
|
+
atr = df['atr'].tolist()
|
|
405
|
+
all_values = atr
|
|
406
|
+
elif plot_type == 'obv':
|
|
407
|
+
obv = df['obv'].tolist()
|
|
408
|
+
all_values = obv
|
|
409
|
+
elif plot_type == 'bb':
|
|
410
|
+
close = df['adjclose'].tolist()
|
|
411
|
+
sma = df['sma'].tolist()
|
|
412
|
+
upper = df['upper'].tolist()
|
|
413
|
+
lower = df['lower'].tolist()
|
|
414
|
+
all_values = close + sma + upper + lower
|
|
415
|
+
else: # Main plot with moving averages
|
|
416
|
+
close = df['adjclose'].tolist()
|
|
417
|
+
ma1 = df['ma1'].tolist()
|
|
418
|
+
ma2 = df['ma2'].tolist()
|
|
419
|
+
ma3 = df['ma3'].tolist()
|
|
420
|
+
all_values = close + ma1 + ma2 + ma3
|
|
421
|
+
|
|
422
|
+
fig = plotille.Figure()
|
|
423
|
+
|
|
424
|
+
# Determine the dimensions and limits of the plot
|
|
425
|
+
fig.width = shutil.get_terminal_size()[0] - 21
|
|
426
|
+
fig.height = math.floor(shutil.get_terminal_size()[1] * height_ratio) - 5
|
|
427
|
+
fig.set_x_limits(dates[0], dates[-1])
|
|
428
|
+
fig.set_y_limits(min(all_values), max(all_values))
|
|
429
|
+
fig.y_ticks_fkt = get_y_tick
|
|
430
|
+
|
|
431
|
+
# Eventually get more color choices, but beware of compatibility issues
|
|
432
|
+
# https://github.com/tammoippen/plotille/blob/master/plotille/_colors.py
|
|
433
|
+
# https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit
|
|
434
|
+
#fig.color_mode = 'rgb'
|
|
435
|
+
|
|
436
|
+
# Prepare the plots and text to display
|
|
437
|
+
if plot_type == 'vol':
|
|
438
|
+
fig.plot(dates, volume, lc=MAIN_LINE_COLOR)
|
|
439
|
+
last = f'{volume[-1]:,.0f}'
|
|
440
|
+
text = f'Volume last value: {last}'
|
|
441
|
+
elif plot_type == 'macd':
|
|
442
|
+
fig.plot(dates, signal, lc=MACD_SIGNAL_COLOR)
|
|
443
|
+
fig.plot(dates, macd, lc=MAIN_LINE_COLOR)
|
|
444
|
+
fig.plot(dates, histogram, lc=MACD_HISTOGRAM_COLOR)
|
|
445
|
+
last = f'{histogram[-1]:,.2f}'
|
|
446
|
+
text = f'MACD histogram last value: {last}'
|
|
447
|
+
elif plot_type == 'rsi':
|
|
448
|
+
fig.plot(dates, overbought, lc=OVERBOUGHT_COLOR)
|
|
449
|
+
fig.plot(dates, oversold, lc=OVERSOLD_COLOR)
|
|
450
|
+
fig.plot(dates, rsi, lc=MAIN_LINE_COLOR)
|
|
451
|
+
last = f'{rsi[-1]:,.2f}'
|
|
452
|
+
text = f'RSI last value: {last}'
|
|
453
|
+
elif plot_type == 'mfi':
|
|
454
|
+
fig.plot(dates, overbought, lc=OVERBOUGHT_COLOR)
|
|
455
|
+
fig.plot(dates, oversold, lc=OVERSOLD_COLOR)
|
|
456
|
+
fig.plot(dates, mfi, lc=MAIN_LINE_COLOR)
|
|
457
|
+
last = f'{mfi[-1]:,.2f}'
|
|
458
|
+
text = f'MFI last value: {last}'
|
|
459
|
+
elif plot_type == 'stoch':
|
|
460
|
+
fig.plot(dates, overbought, lc=OVERBOUGHT_COLOR)
|
|
461
|
+
fig.plot(dates, oversold, lc=OVERSOLD_COLOR)
|
|
462
|
+
fig.plot(dates, stoch_k, lc=STOCH_K_COLOR)
|
|
463
|
+
fig.plot(dates, stoch_d, lc=STOCH_D_COLOR)
|
|
464
|
+
last = f'{stoch_d[-1]:,.2f}'
|
|
465
|
+
text = f'Stochastics last value: {last}'
|
|
466
|
+
elif plot_type == 'atr':
|
|
467
|
+
fig.plot(dates, atr, lc=MAIN_LINE_COLOR)
|
|
468
|
+
last = f'{atr[-1]:,.2f}'
|
|
469
|
+
text = f'ATR last value: {last}'
|
|
470
|
+
elif plot_type == 'obv':
|
|
471
|
+
fig.plot(dates, obv, lc=MAIN_LINE_COLOR)
|
|
472
|
+
last = f'{obv[-1]:,.0f}'
|
|
473
|
+
text = f'OBV last value: {last}'
|
|
474
|
+
elif plot_type == 'bb':
|
|
475
|
+
fig.plot(dates, sma, lc=BB_SMA_COLOR)
|
|
476
|
+
fig.plot(dates, close, lc=MAIN_LINE_COLOR)
|
|
477
|
+
fig.plot(dates, upper, lc=BB_UPPER_BAND_COLOR)
|
|
478
|
+
fig.plot(dates, lower, lc=BB_LOWER_BAND_COLOR)
|
|
479
|
+
last = f'{close[-1]:,.2f}'
|
|
480
|
+
change = f'{(close[-1] / close[0] - 1) * 100:+.0f}'
|
|
481
|
+
text = f'{plot_name} last value: {last} ({change}%)'
|
|
482
|
+
else: # Main plot with moving averages
|
|
483
|
+
fig.plot(dates, ma3, lc=MOVING_AVG_3_COLOR)
|
|
484
|
+
fig.plot(dates, ma2, lc=MOVING_AVG_2_COLOR)
|
|
485
|
+
fig.plot(dates, ma1, lc=MOVING_AVG_1_COLOR)
|
|
486
|
+
fig.plot(dates, close, lc=MAIN_LINE_COLOR)
|
|
487
|
+
last = f'{close[-1]:,.2f}'
|
|
488
|
+
change = f'{(close[-1] / close[0] - 1) * 100:+.0f}'
|
|
489
|
+
text = f'{plot_name} last value: {last} ({change}%)'
|
|
490
|
+
|
|
491
|
+
# Display the last value text
|
|
492
|
+
x = dates[0] + (dates[-1] - dates[0]) * 0.55
|
|
493
|
+
y = min(all_values)
|
|
494
|
+
fig.text([x], [y], [text], lc=TEXT_COLOR)
|
|
495
|
+
|
|
496
|
+
print(fig.show(legend=False))
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def get_y_tick(min_, max_):
|
|
500
|
+
tick = ''
|
|
501
|
+
|
|
502
|
+
# Make sure we don't exceed 10 characters
|
|
503
|
+
if min_ > 9999999999:
|
|
504
|
+
tick = min_ # Leave the tick in scientific notation
|
|
505
|
+
elif min_ > 999999.99:
|
|
506
|
+
tick = f"{min_:.4e}" # Convert the tick to scientific notation
|
|
507
|
+
else:
|
|
508
|
+
tick = f"{min_:,.2f}" # Show 2 decimals and thousands separator
|
|
509
|
+
|
|
510
|
+
return tick
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def add_moving_averages(df):
|
|
514
|
+
# Calculate and add moving averages
|
|
515
|
+
df = df.copy()
|
|
516
|
+
df['ma1'] = df['adjclose'].rolling(window=MOVING_AVG_1).mean()
|
|
517
|
+
df['ma2'] = df['adjclose'].rolling(window=MOVING_AVG_2).mean()
|
|
518
|
+
df['ma3'] = df['adjclose'].rolling(window=MOVING_AVG_3).mean()
|
|
519
|
+
df = df.fillna(0)
|
|
520
|
+
return df
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def add_macd(df):
|
|
524
|
+
# Calculate and add MACD indicator (Moving Average Convergence Divergence)
|
|
525
|
+
df = df.copy()
|
|
526
|
+
fast = df['adjclose'].ewm(span=MACD_FAST_LEN, adjust=False).mean()
|
|
527
|
+
slow = df['adjclose'].ewm(span=MACD_SLOW_LEN, adjust=False).mean()
|
|
528
|
+
df['macd'] = fast - slow
|
|
529
|
+
df['signal'] = df['macd'].ewm(span=MACD_SIGNAL_LEN, adjust=False).mean()
|
|
530
|
+
df['histogram'] = df['macd'] - df['signal']
|
|
531
|
+
return df
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def add_rsi(df):
|
|
535
|
+
# Calculate and add RSI indicator (Relative Strength Index)
|
|
536
|
+
# Calculate the average gain and average loss using Wilder's Smoothing
|
|
537
|
+
# We use a 'com' span of period-1 to match the standard RSI calculation
|
|
538
|
+
df = df.copy()
|
|
539
|
+
delta = df['adjclose'].diff() # Difference from the previous day
|
|
540
|
+
gain = delta.where(delta > 0, 0) # Keep gains and replace losses with 0
|
|
541
|
+
loss = -delta.where(delta < 0, 0) # keep -losses and replace gains with 0
|
|
542
|
+
avg_gain = gain.ewm(com=RSI_PERIOD-1, adjust=False).mean() # Average gain
|
|
543
|
+
avg_loss = loss.ewm(com=RSI_PERIOD-1, adjust=False).mean() # Average loss
|
|
544
|
+
rs = avg_gain / (avg_loss + 1e-10) # RS (avoid division by zero)
|
|
545
|
+
df['rsi'] = 100 - (100 / (1 + rs)) # RSI (normalize to a scale of 0 to 100)
|
|
546
|
+
return df
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def add_mfi(df):
|
|
550
|
+
# Calculate and add MFI indicator (Money Flow Index)
|
|
551
|
+
df = df.copy()
|
|
552
|
+
typical_price = (df['high'] + df['low'] + df['close']) / 3
|
|
553
|
+
money_flow = typical_price * df['volume']
|
|
554
|
+
delta = typical_price.diff() # Difference from the previous day
|
|
555
|
+
pos_mf = money_flow.where(delta > 0, 0) # Positive money flow
|
|
556
|
+
neg_mf = money_flow.where(delta < 0, 0) # Negative money flow
|
|
557
|
+
avg_pos_mf = pos_mf.rolling(window=MFI_PERIOD, min_periods=1).mean()
|
|
558
|
+
avg_neg_mf = neg_mf.rolling(window=MFI_PERIOD, min_periods=1).mean()
|
|
559
|
+
mfr = avg_pos_mf / (avg_neg_mf + 1e-10) # Avoid division by zero
|
|
560
|
+
df['mfi'] = 100 - (100 / (1 + mfr)) # Normalize to a scale of 0 to 100
|
|
561
|
+
df['mfi'] = df['mfi'].fillna(100) # Fill NaN values
|
|
562
|
+
return df
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def add_stochastics(df):
|
|
566
|
+
# Calculate and add Stochastic Oscillator indicator
|
|
567
|
+
df = df.copy()
|
|
568
|
+
low_min = df['low'].rolling(window=STOCH_K_PERIOD).min() # Lowest low
|
|
569
|
+
high_max = df['high'].rolling(window=STOCH_K_PERIOD).max() # Highest high
|
|
570
|
+
fast_k = ((df['close'] - low_min) / (high_max - low_min)) * 100 # Fast
|
|
571
|
+
df['stoch_k'] = fast_k.rolling(window=STOCH_K_SMOOTHING).mean() # Smoothed
|
|
572
|
+
df['stoch_d'] = df['stoch_k'].rolling(window=STOCH_D_PERIOD).mean() # Slow
|
|
573
|
+
return df
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def add_bollinger_bands(df):
|
|
577
|
+
# Calculate and add Bollinger Bands indicator
|
|
578
|
+
df = df.copy()
|
|
579
|
+
df['sma'] = df['adjclose'].rolling(window=BB_PERIOD).mean() # Rolling mean
|
|
580
|
+
std = df['adjclose'].rolling(window=BB_PERIOD).std() # Rolling std deviation
|
|
581
|
+
df['upper'] = df['sma'] + (std * BB_STD_DEV) # Upper band
|
|
582
|
+
df['lower'] = df['sma'] - (std * BB_STD_DEV) # Lower band
|
|
583
|
+
return df
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def add_atr(df):
|
|
587
|
+
# Calculate and add ATR indicator (Average True Range)
|
|
588
|
+
df = df.copy()
|
|
589
|
+
tr1 = df['high'] - df['low'] # high - low
|
|
590
|
+
tr2 = (df['high'] - df['close'].shift()).abs() # high - previous close
|
|
591
|
+
tr3 = (df['low'] - df['close'].shift()).abs() # low - previous close
|
|
592
|
+
tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1) # Max of the 3 components
|
|
593
|
+
df['atr'] = tr.ewm(com=ATR_PERIOD-1, adjust=False).mean() # Wilder's Smoothing
|
|
594
|
+
return df
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def add_obv(df):
|
|
598
|
+
# Calculate and add OBV indicator (On-Balance Volume)
|
|
599
|
+
df = df.copy()
|
|
600
|
+
price_change = df['adjclose'].diff() # Price direction
|
|
601
|
+
direction = pd.Series(1, index=df.index) # Start with 1
|
|
602
|
+
direction = direction.where(price_change >= 0, -1) # Set to -1 if dropped
|
|
603
|
+
direction = direction.mask(price_change == 0, 0) # Set to 0 if no change
|
|
604
|
+
df['obv'] = (direction * df['volume']).cumsum()
|
|
605
|
+
return df
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def print_indicator_info():
|
|
609
|
+
width = shutil.get_terminal_size()[0]
|
|
610
|
+
indent = ' ' * 4
|
|
611
|
+
|
|
612
|
+
# Print a section for each indicator with its description
|
|
613
|
+
for name, data in INDICATOR_DESCRIPTIONS.items():
|
|
614
|
+
title = plotille.color(name.upper(), fg='green')
|
|
615
|
+
category = plotille.color(data['category'], fg='blue')
|
|
616
|
+
description = f"{category} - {data['description']}"
|
|
617
|
+
wrapper = textwrap.TextWrapper(width=width, initial_indent=indent,
|
|
618
|
+
subsequent_indent=indent)
|
|
619
|
+
description = wrapper.fill(description)
|
|
620
|
+
print(f"\n{title}")
|
|
621
|
+
print(description)
|
|
622
|
+
|
|
623
|
+
# Print a section with the indicator categories descriptions
|
|
624
|
+
title = plotille.color('INDICATOR CATEGORIES', fg='green')
|
|
625
|
+
print(f"\n{title}")
|
|
626
|
+
|
|
627
|
+
for category, description in INDICATOR_CATEGORIES.items():
|
|
628
|
+
category = plotille.color(category, fg='blue')
|
|
629
|
+
description = f"{category} - {description}"
|
|
630
|
+
wrapper = textwrap.TextWrapper(width=width, initial_indent=indent,
|
|
631
|
+
subsequent_indent=indent)
|
|
632
|
+
description = wrapper.fill(description)
|
|
633
|
+
print(description)
|
|
634
|
+
|
|
635
|
+
print()
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def log_data_frame(df, description = ''):
|
|
639
|
+
""" This function is used only for debugging. """
|
|
640
|
+
logger.debug(f'DataFrame {description}\n{df}')
|
|
641
|
+
#logger.debug(f'DataFrame index data type: {df.index.dtype}')
|
|
642
|
+
#logger.debug(f'DataFrame index class: {type(df.index)}')
|
|
643
|
+
#logger.debug(f'DataFrame columns data types\n{df.dtypes}')
|
|
644
|
+
#logger.debug(f'DataFrame statistics\n{df.describe()}') # Mean, min, max...
|
|
645
|
+
sys.exit()
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
if __name__ == '__main__':
|
|
649
|
+
# Configure the root logger
|
|
650
|
+
logging.basicConfig(level=logging.WARNING,
|
|
651
|
+
format='%(levelname)s - %(message)s')
|
|
652
|
+
|
|
653
|
+
# Configure this script's logger
|
|
654
|
+
#logger.setLevel(logging.DEBUG)
|
|
655
|
+
|
|
656
|
+
main()
|