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.
@@ -0,0 +1,2 @@
1
+ dist/
2
+ *.txt
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
+ ![NVDA stock price, moving averages and volume over 1 year](https://github.com/monsieurlinux/tsigna/raw/main/img/tsigna-nvda-moving-averages-volume-1y.png "NVDA stock price, moving averages and volume over 1 year")
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
+ ![NVDA stock price, Bollinger Bands and Stochastics over 1 year](https://github.com/monsieurlinux/tsigna/raw/main/img/tsigna-nvda-bollinger-bands-stochastics-1y.png "NVDA stock price, Bollinger Bands and Stochastics over 1 year")
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
+ ![Ratio between NVDA and WMT stock prices, moving averages and RSI indicator over 2 years](https://github.com/monsieurlinux/tsigna/raw/main/img/tsigna-nvda-wmt-moving-averages-rsi-2y.png "Ratio between NVDA and WMT stock prices, moving averages and RSI indicator over 2 years")
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
+ ![NVDA stock price, moving averages and volume over 1 year](https://github.com/monsieurlinux/tsigna/raw/main/img/tsigna-nvda-moving-averages-volume-1y.png "NVDA stock price, moving averages and volume over 1 year")
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
+ ![NVDA stock price, Bollinger Bands and Stochastics over 1 year](https://github.com/monsieurlinux/tsigna/raw/main/img/tsigna-nvda-bollinger-bands-stochastics-1y.png "NVDA stock price, Bollinger Bands and Stochastics over 1 year")
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
+ ![Ratio between NVDA and WMT stock prices, moving averages and RSI indicator over 2 years](https://github.com/monsieurlinux/tsigna/raw/main/img/tsigna-nvda-wmt-moving-averages-rsi-2y.png "Ratio between NVDA and WMT stock prices, moving averages and RSI indicator over 2 years")
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
@@ -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()