├── examples ├── __init__.py ├── screenshot.png ├── BTC-chart.json ├── basic_from_stdin.sh ├── basic_from_csv_file.py ├── basic_from_json_file.py ├── custom_renderer_class.py ├── fetch_from_binance.py ├── integrate_with_rich.py └── BTC-USD.csv ├── src ├── tests │ ├── __init__.py │ ├── conftest.py │ ├── data │ │ ├── BTC-chart.json │ │ └── BTC-USD.csv │ ├── test_y_axis.py │ ├── test_candle_set.py │ ├── test_colors.py │ ├── test_chart_data.py │ ├── test_info_bar.py │ ├── test_main.py │ ├── test_chart.py │ └── test_utils.py └── candlestick_chart │ ├── py.typed │ ├── __init__.py │ ├── volume_pane.py │ ├── constants.py │ ├── colors.py │ ├── candle.py │ ├── chart_data.py │ ├── candle_set.py │ ├── __main__.py │ ├── utils.py │ ├── info_bar.py │ ├── y_axis.py │ ├── chart_renderer.py │ └── chart.py ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── release.yml │ └── tests.yml ├── .well-known └── funding-manifest-urls ├── .gitignore ├── checks.sh ├── LICENSE ├── pyproject.toml ├── README.md └── CHANGELOG.md /examples/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/candlestick_chart/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [BoboTiG] 2 | -------------------------------------------------------------------------------- /.well-known/funding-manifest-urls: -------------------------------------------------------------------------------- 1 | https://www.tiger-222.fr/funding.json 2 | -------------------------------------------------------------------------------- /examples/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoboTiG/py-candlestick-chart/HEAD/examples/screenshot.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Files 2 | .coverage 3 | 4 | # Folders 5 | build/ 6 | dist/ 7 | *.egg-info/ 8 | .mypy_cache/ 9 | __pycache__/ 10 | .pytest_cache/ 11 | .ruff_cache/ 12 | venv/ 13 | -------------------------------------------------------------------------------- /checks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | python -m ruff format src examples 5 | python -m ruff check --fix --unsafe-fixes src examples 6 | python -m mypy src examples 7 | echo "ok" 8 | -------------------------------------------------------------------------------- /src/tests/conftest.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pytest 4 | 5 | 6 | @pytest.fixture(scope="session") 7 | def data() -> Path: 8 | return Path(__file__).parent / "data" 9 | -------------------------------------------------------------------------------- /examples/BTC-chart.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "open": 28994.009766, 3 | "high": 29600.626953, 4 | "low": 28803.585938, 5 | "close": 29374.152344 6 | }, 7 | { 8 | "open": 29376.455078, 9 | "high": 33155.117188, 10 | "low": 29091.181641, 11 | "close": 32127.267578 12 | } 13 | ] -------------------------------------------------------------------------------- /src/tests/data/BTC-chart.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "open": 28994.009766, 3 | "high": 29600.626953, 4 | "low": 28803.585938, 5 | "close": 29374.152344 6 | }, 7 | { 8 | "open": 29376.455078, 9 | "high": 33155.117188, 10 | "low": 29091.181641, 11 | "close": 32127.267578 12 | } 13 | ] -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # GitHub Actions 4 | - package-ecosystem: github-actions 5 | directory: / 6 | schedule: 7 | interval: weekly 8 | assignees: 9 | - BoboTiG 10 | 11 | # Python requirements 12 | - package-ecosystem: pip 13 | directory: / 14 | schedule: 15 | interval: weekly 16 | assignees: 17 | - BoboTiG 18 | -------------------------------------------------------------------------------- /examples/basic_from_stdin.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo '[ 4 | { 5 | "open": 28994.009766, 6 | "high": 29600.626953, 7 | "low": 28803.585938, 8 | "close": 29374.152344 9 | }, 10 | { 11 | "open": 29376.455078, 12 | "high": 33155.117188, 13 | "low": 29091.181641, 14 | "close": 32127.267578 15 | } 16 | ]' | candlestick-chart \ 17 | --mode=stdin \ 18 | --chart-name="My BTC Chart" \ 19 | --bear-color="#b967ff" \ 20 | --bull-color="ff6b99" 21 | -------------------------------------------------------------------------------- /examples/basic_from_csv_file.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from candlestick_chart import Chart 4 | from candlestick_chart.utils import parse_candles_from_csv 5 | 6 | 7 | def main() -> int: 8 | # Your CSV data must have "open,high,low,close" header fields. 9 | candles = parse_candles_from_csv("./examples/BTC-USD.csv") 10 | 11 | chart = Chart(candles, title="BTC/USDT") 12 | 13 | # Set customs colors 14 | chart.set_bear_color(1, 205, 254) 15 | chart.set_bull_color(255, 107, 153) 16 | 17 | chart.draw() 18 | return 0 19 | 20 | 21 | if __name__ == "__main__": 22 | sys.exit(main()) 23 | -------------------------------------------------------------------------------- /examples/basic_from_json_file.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from candlestick_chart import Chart 4 | from candlestick_chart.utils import fnum, parse_candles_from_json 5 | 6 | 7 | def main() -> int: 8 | # Your JSON data must have "open,high,low,close" header fields. 9 | candles = parse_candles_from_json("./examples/BTC-chart.json") 10 | 11 | chart = Chart(candles, title="My BTC Chart") 12 | 13 | # Set customs colors 14 | chart.set_bear_color(1, 205, 254) 15 | chart.set_bull_color(255, 107, 153) 16 | chart.set_highlight(fnum(30544.20), (0, 0, 255)) 17 | 18 | chart.draw() 19 | return 0 20 | 21 | 22 | if __name__ == "__main__": 23 | sys.exit(main()) 24 | -------------------------------------------------------------------------------- /src/tests/test_y_axis.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from candlestick_chart import colors 4 | from candlestick_chart.candle import Candle 5 | from candlestick_chart.chart_data import ChartData 6 | from candlestick_chart.y_axis import YAxis 7 | 8 | CANDLES = [Candle(open=1, high=2, low=0.5, close=1.2, volume=1_234_567)] 9 | WIDTH = 80 10 | HEIGHT = 24 11 | CHART_DATA = ChartData(CANDLES, width=WIDTH, height=HEIGHT) 12 | 13 | 14 | @pytest.fixture 15 | def y_axis() -> YAxis: 16 | return YAxis(CHART_DATA) 17 | 18 | 19 | def test_render_empty_with_highlighted_price(y_axis: YAxis) -> None: 20 | price = f"{CANDLES[0].open:.02f}" 21 | highlights = {"10.00": "green", price: "green"} 22 | rendered = y_axis.render_empty(y=8, highlights=highlights) # type: ignore[arg-type] 23 | assert colors.green(f"{price} ┤") in rendered 24 | -------------------------------------------------------------------------------- /src/tests/test_candle_set.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from candlestick_chart.candle_set import CandleSet 4 | from candlestick_chart.utils import parse_candles_from_csv 5 | 6 | EXAMPLES = Path(__file__).parent.parent.parent / "examples" 7 | 8 | 9 | def test_compute_all() -> None: 10 | candles = parse_candles_from_csv(EXAMPLES / "BTC-USD.csv") 11 | candle_set = CandleSet(candles) 12 | 13 | assert candle_set.candles == candles 14 | assert candle_set.min_price == 28_722.755859 15 | assert candle_set.max_price == 67_673.742188 16 | assert candle_set.min_volume == 18_787_986_667.0 17 | assert candle_set.max_volume == 350_967_941_479.0 18 | assert candle_set.variation == 133.037_198_615_531_42 19 | assert candle_set.average == 46_375.191_043_259_58 20 | assert candle_set.last_price == 67_566.828_125 21 | assert candle_set.cumulative_volume == 15_472_149_666_480.0 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | workflow_dispatch: 8 | 9 | jobs: 10 | release: 11 | runs-on: ubuntu-latest 12 | environment: Production 13 | permissions: 14 | id-token: write 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v6 18 | - name: Install Python 19 | uses: actions/setup-python@v6 20 | with: 21 | python-version: "3.x" 22 | cache: pip 23 | - name: Install build dependencies 24 | run: | 25 | python -m pip install -U pip 26 | python -m pip install -e '.[dev]' 27 | - name: Build 28 | run: python -m build 29 | - name: Check 30 | run: twine check --strict dist/* 31 | - name: What will we publish? 32 | run: ls -l dist 33 | - name: Publish 34 | uses: pypa/gh-action-pypi-publish@release/v1 35 | with: 36 | skip-existing: true 37 | print-hash: true 38 | -------------------------------------------------------------------------------- /src/candlestick_chart/__init__.py: -------------------------------------------------------------------------------- 1 | """Draw candlesticks charts right into your terminal. 2 | 3 | This module is maintained by Mickaël Schoentgen . 4 | 5 | You can always get the latest version of this module at: 6 | https://github.com/BoboTiG/py-candlestrick-charts 7 | If that URL should fail, try contacting the author. 8 | """ 9 | 10 | __version__ = "3.1.1-dev" 11 | __author__ = "Mickaël Schoentgen" 12 | __copyright__ = f""" 13 | Copyright (c) 2022-2025, {__author__} 14 | Permission to use, copy, modify, and distribute this software and its 15 | documentation for any purpose and without fee or royalty is hereby 16 | granted, provided that the above copyright notice appear in all copies 17 | and that both that copyright notice and this permission notice appear 18 | in supporting documentation or portions thereof, including 19 | modifications, that you make. 20 | """ 21 | 22 | from candlestick_chart.candle import Candle 23 | from candlestick_chart.chart import Chart 24 | from candlestick_chart.utils import fnum 25 | 26 | __all__ = ("Candle", "Chart", "fnum") 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | Copyright (c) 2022-2025, Mickaël 'Tiger-222' Schoentgen 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /src/tests/test_colors.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from candlestick_chart import colors 4 | 5 | 6 | @pytest.mark.parametrize( 7 | ("custom_color", "expected"), 8 | [ 9 | ("", "𓅂"), 10 | ("green", "\x1b[92m𓅂\x1b[00m"), 11 | ((0, 255, 0), "\x1b[38;2;0;255;0m𓅂\x1b[00m"), 12 | # ANSI color 13 | ("92m", "\x1b[92m𓅂\x1b[00m"), 14 | # ANSI color + background 15 | ("92;47m", "\x1b[92;47m𓅂\x1b[00m"), 16 | ], 17 | ) 18 | def test_color(custom_color: str, expected: str) -> None: 19 | assert colors.color("𓅂", custom_color) == expected 20 | 21 | 22 | @pytest.mark.parametrize( 23 | ("color_name", "expected"), 24 | [ 25 | ("blue", "\x1b[94mfoo\x1b[00m"), 26 | ("bold", "\x1b[01mfoo\x1b[00m"), 27 | ("cyan", "\x1b[96mfoo\x1b[00m"), 28 | ("gray", "\x1b[90mfoo\x1b[00m"), 29 | ("grey", "\x1b[90mfoo\x1b[00m"), 30 | ("magenta", "\x1b[95mfoo\x1b[00m"), 31 | ("red", "\x1b[91mfoo\x1b[00m"), 32 | ("white", "\x1b[97mfoo\x1b[00m"), 33 | ("yellow", "\x1b[93mfoo\x1b[00m"), 34 | ], 35 | ) 36 | def test_colors(color_name: str, expected: str) -> None: 37 | assert getattr(colors, color_name)("foo") == expected 38 | -------------------------------------------------------------------------------- /src/candlestick_chart/volume_pane.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from math import ceil 3 | 4 | from candlestick_chart import constants 5 | from candlestick_chart.candle import Candle, CandleType 6 | from candlestick_chart.colors import truecolor 7 | 8 | 9 | @dataclass(slots=True) 10 | class VolumePane: 11 | height: int 12 | enabled: bool = True 13 | bearish_color: tuple[int, int, int] = (234, 74, 90) 14 | bullish_color: tuple[int, int, int] = (52, 208, 88) 15 | unicode_fill: str = constants.UNICODE_FILL 16 | 17 | def _colorize(self, candle_type: int, string: str) -> str: 18 | color = self.bearish_color if candle_type == CandleType.bearish else self.bullish_color 19 | return truecolor(string, *color) 20 | 21 | def render(self, candle: Candle, y: int, max_volume: float) -> str: 22 | volume_percent_of_max = candle.volume / (max_volume or 1) 23 | ratio = volume_percent_of_max * self.height 24 | 25 | if y < ceil(ratio): 26 | return self._colorize(candle.type, self.unicode_fill) 27 | 28 | if y == 1 and self.unicode_fill == constants.UNICODE_FILL: 29 | return self._colorize(candle.type, constants.UNICODE_HALF_BODY_BOTTOM) 30 | 31 | return " " 32 | -------------------------------------------------------------------------------- /src/candlestick_chart/constants.py: -------------------------------------------------------------------------------- 1 | from types import SimpleNamespace 2 | 3 | # Info bar labels 4 | # Labels can be customized via `Chart.set_label("label", "value")` 5 | # (setting a label to an empty string will hide it from the chart) 6 | LABELS = SimpleNamespace( 7 | average="Avg.", 8 | currency="", 9 | highest="Highest", 10 | lowest="Lowest", 11 | price="Price", 12 | variation="Var.", 13 | volume="Cum. Vol.", 14 | ) 15 | 16 | # Margins, and internal sizes 17 | MARGIN_TOP = 3 18 | MARGIN_RIGHT = 4 19 | CHAR_PRECISION = 6 20 | DEC_PRECISION = 5 21 | WIDTH = CHAR_PRECISION + 1 + DEC_PRECISION + MARGIN_RIGHT 22 | HEIGHT = 2 23 | Y_AXIS_SPACING = 4 24 | 25 | # Numbers formatting 26 | PRECISION = 2 27 | PRECISION_SMALL = 4 28 | 29 | # Chart characters 30 | UNICODE_BODY = "┃" 31 | UNICODE_BOTTOM = "╿" 32 | UNICODE_HALF_BODY_BOTTOM = "╻" 33 | UNICODE_HALF_BODY_TOP = "╹" 34 | UNICODE_FILL = "┃" 35 | UNICODE_TOP = "╽" 36 | UNICODE_VOID = " " 37 | UNICODE_WICK = "│" 38 | UNICODE_WICK_LOWER = "╵" 39 | UNICODE_WICK_UPPER = "╷" 40 | UNICODE_Y_AXIS = "│" 41 | UNICODE_Y_AXIS_LEFT = "┤" 42 | UNICODE_Y_AXIS_RIGHT = "├" 43 | MIN_DIFF_THRESHOLD = 0.25 44 | MAX_DIFF_THRESHOLD = 0.75 45 | 46 | # Chart options 47 | Y_AXIS_ON_THE_RIGHT = False 48 | Y_AXIS_ROUND_DIR = "down" # Or "up" 49 | # Examples: 50 | # 1 / 0.01 51 | # 1 / 0.0025 52 | Y_AXIS_ROUND_MULTIPLIER = 0.0 53 | -------------------------------------------------------------------------------- /src/tests/test_chart_data.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | import pytest 4 | 5 | from candlestick_chart.candle import Candle 6 | from candlestick_chart.chart_data import ChartData 7 | from candlestick_chart.volume_pane import VolumePane 8 | 9 | CANDLE = Candle(open=1, high=2, low=0.5, close=1.2) 10 | 11 | 12 | @pytest.fixture(scope="module") 13 | def chart_data() -> ChartData: 14 | return ChartData([CANDLE], width=100, height=50) 15 | 16 | 17 | def test_candle_eq() -> None: 18 | candle1 = Candle(open=1, high=2, low=0.5, close=1.2) 19 | assert candle1 == CANDLE 20 | 21 | candle2 = Candle(open=1.1, high=2.1, low=0.51, close=1.21) 22 | assert candle2 != CANDLE 23 | 24 | with pytest.raises(NotImplementedError): 25 | assert object() == CANDLE 26 | 27 | 28 | def test_candle_repr() -> None: 29 | assert repr(CANDLE) == "Candle" 30 | 31 | 32 | def test_compute_height_take_into_account_constant_change(chart_data: ChartData) -> None: 33 | volume_pane = VolumePane(0, enabled=False) 34 | chart_data.compute_height(volume_pane) 35 | original_height = chart_data.height 36 | assert original_height 37 | 38 | with patch("candlestick_chart.constants.MARGIN_TOP", 0): 39 | chart_data.compute_height(volume_pane) 40 | new_height = chart_data.height 41 | 42 | assert new_height > original_height 43 | -------------------------------------------------------------------------------- /examples/custom_renderer_class.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from dataclasses import dataclass 3 | from math import floor 4 | 5 | from candlestick_chart import Candle, Chart, constants 6 | from candlestick_chart.chart_renderer import ChartRenderer 7 | from candlestick_chart.colors import truecolor 8 | from candlestick_chart.utils import parse_candles_from_csv 9 | from candlestick_chart.y_axis import YAxis 10 | 11 | 12 | @dataclass 13 | class MyChartRendered(ChartRenderer): 14 | def _render_candle(self, candle: Candle, y: int, y_axis: YAxis) -> str: 15 | height_unit = float(y) 16 | *_, max_y, min_y = y_axis.price_to_heights(candle) 17 | return ( 18 | truecolor("•", *self.bullish_color) 19 | if max_y > height_unit > floor(min_y) 20 | else truecolor(constants.UNICODE_WICK, *self.bearish_color) 21 | if floor(max_y) > height_unit 22 | else constants.UNICODE_VOID 23 | ) 24 | 25 | 26 | def main() -> int: 27 | # Your CSV data must have "open,high,low,close" header fields. 28 | candles = parse_candles_from_csv("./examples/BTC-USD.csv") 29 | 30 | chart = Chart(candles, title="BTC/USDT", renderer_cls=MyChartRendered) 31 | chart.set_volume_pane_enabled(False) 32 | 33 | # Set customs colors 34 | chart.set_bear_color(1, 205, 254) 35 | chart.set_bull_color(255, 107, 153) 36 | 37 | chart.draw() 38 | return 0 39 | 40 | 41 | if __name__ == "__main__": 42 | sys.exit(main()) 43 | -------------------------------------------------------------------------------- /src/tests/test_info_bar.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from candlestick_chart import constants 4 | from candlestick_chart.candle import Candle 5 | from candlestick_chart.candle_set import CandleSet 6 | from candlestick_chart.info_bar import InfoBar 7 | 8 | CANDLES = [Candle(open=1, high=2, low=0.5, close=1.2, volume=1_234_567)] 9 | CANDLE_SET = CandleSet(CANDLES) 10 | WIDTH = 80 11 | 12 | 13 | @pytest.fixture 14 | def info_bar() -> InfoBar: 15 | return InfoBar("BTC/USD") 16 | 17 | 18 | def test_render_with_currency(info_bar: InfoBar) -> None: 19 | label = info_bar.labels.currency 20 | assert label == constants.LABELS.currency 21 | assert not label 22 | 23 | label = "€€€€€€€" 24 | info_bar.labels.currency = label 25 | assert label in info_bar.render(CANDLE_SET, WIDTH) 26 | 27 | 28 | def test_render_without_average(info_bar: InfoBar) -> None: 29 | label = info_bar.labels.average 30 | assert label == constants.LABELS.average 31 | assert label 32 | assert label in info_bar.render(CANDLE_SET, WIDTH) 33 | 34 | info_bar.labels.average = "" 35 | assert label not in info_bar.render(CANDLE_SET, WIDTH) 36 | 37 | 38 | def test_render_without_variation(info_bar: InfoBar) -> None: 39 | label = info_bar.labels.variation 40 | assert label == constants.LABELS.variation 41 | assert label 42 | assert label in info_bar.render(CANDLE_SET, WIDTH) 43 | 44 | info_bar.labels.variation = "" 45 | assert label not in info_bar.render(CANDLE_SET, WIDTH) 46 | -------------------------------------------------------------------------------- /examples/fetch_from_binance.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from dataclasses import dataclass 3 | 4 | from candlestick_chart import Candle, Chart 5 | 6 | 7 | @dataclass(frozen=True, slots=True) 8 | class BinanceKlinesItem: 9 | open_time: int 10 | open: str 11 | high: str 12 | low: str 13 | close: str 14 | volume: str 15 | close_time: int 16 | quote_asset_volume: str 17 | number_of_trades: int 18 | taker_buy_base_asset_volume: str 19 | taker_buy_quote_asset_volume: str 20 | ignore: str 21 | 22 | 23 | def main() -> int: 24 | import requests 25 | 26 | url = "https://api.binance.com/api/v1/klines?symbol=CHZUSDT&interval=1h" 27 | with requests.get(url, timeout=60) as req: 28 | klines = [BinanceKlinesItem(*item) for item in req.json()] 29 | 30 | candles = [ 31 | Candle( 32 | open=float(kline.open), 33 | close=float(kline.close), 34 | high=float(kline.high), 35 | low=float(kline.low), 36 | volume=float(kline.volume), 37 | timestamp=float(kline.open_time), 38 | ) 39 | for kline in klines 40 | ] 41 | 42 | chart = Chart(candles, title="CHZ/USDT") 43 | 44 | chart.set_bull_color(1, 205, 254) 45 | chart.set_bear_color(255, 107, 153) 46 | chart.set_volume_pane_height(4) 47 | chart.set_volume_pane_enabled(True) 48 | chart.set_highlight("0.2377", "93;45m") 49 | 50 | chart.draw() 51 | return 0 52 | 53 | 54 | if __name__ == "__main__": 55 | sys.exit(main()) 56 | -------------------------------------------------------------------------------- /src/tests/test_main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from io import StringIO 3 | from pathlib import Path 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from candlestick_chart.__main__ import main 9 | 10 | EXAMPLES = Path(__file__).parent.parent.parent / "examples" 11 | OPTIONAL_ARGS = [ 12 | "", 13 | "--chart-name=My BTC Chart", 14 | "--bear-color=#b967ff", 15 | "--bull-color=ff6b99", 16 | ] 17 | 18 | 19 | def run(args: list[str]) -> None: 20 | with patch.object(sys, "argv", new=["candlestick-chart", *filter(lambda a: a, args)]): 21 | assert main() == 0 22 | 23 | 24 | @pytest.mark.parametrize("additional_arg", OPTIONAL_ARGS) 25 | def test_mode_stdin(additional_arg: str) -> None: 26 | data = StringIO( 27 | """[ 28 | { 29 | "open": 28994.009766, 30 | "high": 29600.626953, 31 | "low": 28803.585938, 32 | "close": 29374.152344 33 | }, 34 | { 35 | "open": 29376.455078, 36 | "high": 33155.117188, 37 | "low": 29091.181641, 38 | "close": 32127.267578 39 | } 40 | ]""", 41 | ) 42 | with patch.object(sys, "stdin", data): 43 | run(["--mode=stdin", additional_arg]) 44 | 45 | 46 | @pytest.mark.parametrize("additional_arg", OPTIONAL_ARGS) 47 | def test_mode_csv_file(additional_arg: str) -> None: 48 | file = EXAMPLES / "BTC-USD.csv" 49 | run(["--mode=csv-file", f"--file={file}", additional_arg]) 50 | 51 | 52 | @pytest.mark.parametrize("additional_arg", OPTIONAL_ARGS) 53 | def test_mode_json_file(additional_arg: str) -> None: 54 | file = EXAMPLES / "BTC-chart.json" 55 | run(["--mode=json-file", f"--file={file}", additional_arg]) 56 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | 11 | jobs: 12 | tests: 13 | name: "${{ matrix.os.emoji }} ${{ matrix.python.name }}" 14 | runs-on: ${{ matrix.os.runs-on }} 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | os: 19 | - emoji: 🐧 20 | runs-on: [ubuntu-latest] 21 | - emoji: 🍎 22 | runs-on: [macos-latest] 23 | - emoji: 🪟 24 | runs-on: [windows-latest] 25 | python: 26 | - name: CPython 3.14 27 | runs-on: "3.14-dev" 28 | - name: CPython 3.13 29 | runs-on: "3.13" 30 | - name: CPython 3.12 31 | runs-on: "3.12" 32 | - name: CPython 3.11 33 | runs-on: "3.11" 34 | - name: CPython 3.10 35 | runs-on: "3.10" 36 | steps: 37 | - uses: actions/checkout@v6 38 | - uses: actions/setup-python@v6 39 | with: 40 | cache: pip 41 | check-latest: true 42 | python-version: ${{ matrix.python.runs-on }} 43 | - run: python -m pip install -U pip 44 | - run: python -m pip install -e '.[test]' 45 | - run: python -m pytest 46 | 47 | automerge: 48 | name: Automerge 49 | runs-on: ubuntu-latest 50 | needs: [tests] 51 | if: ${{ github.actor == 'dependabot[bot]' }} 52 | steps: 53 | - name: Automerge 54 | run: gh pr merge --auto --rebase "$PR_URL" 55 | env: 56 | PR_URL: ${{github.event.pull_request.html_url}} 57 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 58 | -------------------------------------------------------------------------------- /src/candlestick_chart/colors.py: -------------------------------------------------------------------------------- 1 | from functools import cache 2 | 3 | 4 | def blue(value: str) -> str: 5 | return f"\033[94m{value}\033[00m" 6 | 7 | 8 | def bold(value: str) -> str: 9 | return f"\033[01m{value}\033[00m" 10 | 11 | 12 | def cyan(value: str) -> str: 13 | return f"\033[96m{value}\033[00m" 14 | 15 | 16 | def grey(value: str) -> str: 17 | return f"\033[90m{value}\033[00m" 18 | 19 | 20 | gray = grey 21 | 22 | 23 | def green(value: str) -> str: 24 | return f"\033[92m{value}\033[00m" 25 | 26 | 27 | def magenta(value: str) -> str: 28 | return f"\033[95m{value}\033[00m" 29 | 30 | 31 | def red(value: str) -> str: 32 | return f"\033[91m{value}\033[00m" 33 | 34 | 35 | @cache 36 | def truecolor(value: str, r: int, g: int, b: int) -> str: 37 | return f"\033[38;2;{r};{g};{b}m{value}\033[00m" 38 | 39 | 40 | def white(value: str) -> str: 41 | return f"\033[97m{value}\033[00m" 42 | 43 | 44 | def yellow(value: str) -> str: 45 | return f"\033[93m{value}\033[00m" 46 | 47 | 48 | COLORS = { 49 | "blue": blue, 50 | "cyan": cyan, 51 | "green": green, 52 | "gray": gray, 53 | "grey": grey, 54 | "magenta": magenta, 55 | "red": red, 56 | "white": white, 57 | "yellow": yellow, 58 | } 59 | 60 | 61 | def color(text: str, value: str | tuple[int, int, int]) -> str: 62 | if not value: 63 | return text 64 | 65 | if isinstance(value, tuple): 66 | return truecolor(text, *value) 67 | 68 | if col_fn := COLORS.get(value): 69 | return col_fn(text) 70 | 71 | # ANSI color 72 | # example: "93m" for yellow 73 | # example: "94;43" for blue on yellow background 74 | return f"\033[{value}{text}\033[00m" 75 | -------------------------------------------------------------------------------- /src/candlestick_chart/candle.py: -------------------------------------------------------------------------------- 1 | from typing import NamedTuple 2 | 3 | 4 | class CandleType(NamedTuple): 5 | bearish: int = 0 6 | bullish: int = 1 7 | 8 | 9 | class Candle: 10 | __slots__ = ("open", "close", "high", "low", "volume", "timestamp", "type") 11 | 12 | def __init__(self, **kwargs: float) -> None: 13 | self.open = float(kwargs["open"]) 14 | self.high = float(kwargs["high"]) 15 | self.low = float(kwargs["low"]) 16 | self.close = float(kwargs["close"]) 17 | self.volume = float(kwargs.get("volume", 0.0)) 18 | self.timestamp = float(kwargs.get("timestamp", 0.0)) 19 | self.type = CandleType.bullish if self.open < self.close else CandleType.bearish 20 | 21 | def __eq__(self, other: object) -> bool: 22 | if not isinstance(other, Candle): 23 | raise NotImplementedError 24 | 25 | return ( 26 | self.open == other.open 27 | and self.high == other.high 28 | and self.low == other.low 29 | and self.close == other.close 30 | and self.volume == other.volume 31 | and self.timestamp == other.timestamp 32 | and self.type == other.type 33 | ) 34 | 35 | def __repr__(self) -> str: 36 | return ( 37 | f"{type(self).__name__}<" 38 | f"open={self.open}, " 39 | f"low={self.low}, " 40 | f"high={self.high}, " 41 | f"close={self.close}, " 42 | f"volume={self.volume}, " 43 | f"timestamp={self.timestamp}, " 44 | f"type={'bullish' if self.type == CandleType.bullish else 'bearish'}" 45 | ">" 46 | ) 47 | 48 | 49 | Candles = list[Candle] 50 | -------------------------------------------------------------------------------- /examples/integrate_with_rich.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | 3 | from rich.layout import Layout 4 | from rich.live import Live 5 | from rich.panel import Panel 6 | 7 | from candlestick_chart.candle import Candles 8 | from candlestick_chart.chart import Chart 9 | from candlestick_chart.utils import fnum, parse_candles_from_csv 10 | 11 | 12 | def make_layout() -> Layout: 13 | layout = Layout(name="root") 14 | layout.split( 15 | Layout(name="header", size=3), 16 | Layout(name="main", ratio=1), 17 | ) 18 | layout["main"].split_row( 19 | Layout(name="candlesticks-static"), 20 | Layout(name="candlesticks-dynamic"), 21 | ) 22 | return layout 23 | 24 | 25 | def make_chart(candles: Candles, nature: str) -> Chart: 26 | chart = Chart(candles, title=f"Rich candlesticks {nature}!") 27 | chart.set_bear_color(255, 107, 153) 28 | chart.set_bull_color(1, 205, 254) 29 | chart.set_label("average", "") 30 | chart.set_label("volume", "") 31 | chart.set_highlight(fnum(53730.68), "red") 32 | return chart 33 | 34 | 35 | candles = parse_candles_from_csv("./examples/BTC-USD.csv") 36 | chart_static = make_chart(candles, "static") 37 | chart_dynamic = make_chart([], "dynamic") 38 | 39 | layout = make_layout() 40 | layout["candlesticks-static"].update(Panel(chart_static)) 41 | layout["candlesticks-dynamic"].update(Panel(chart_dynamic)) 42 | 43 | use_reset = False 44 | 45 | with Live(layout, refresh_per_second=120): 46 | if use_reset: 47 | candles_count = 0 48 | while candles_count <= len(candles): 49 | chart_dynamic.update_candles(candles[:candles_count], reset=True) 50 | candles_count += 1 51 | sleep(0.03) 52 | else: 53 | for candle in candles: 54 | chart_dynamic.update_candles([candle]) 55 | sleep(0.03) 56 | -------------------------------------------------------------------------------- /src/candlestick_chart/chart_data.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from shutil import get_terminal_size 4 | from typing import TYPE_CHECKING 5 | 6 | from candlestick_chart import constants 7 | from candlestick_chart.candle_set import CandleSet 8 | 9 | if TYPE_CHECKING: 10 | from candlestick_chart.candle import Candles 11 | from candlestick_chart.volume_pane import VolumePane 12 | 13 | 14 | class ChartData: 15 | __slots__ = ( 16 | "height", 17 | "main_candle_set", 18 | "terminal_size", 19 | "visible_candle_set", 20 | "width", 21 | ) 22 | 23 | def __init__(self, candles: Candles, *, width: int = 0, height: int = 0) -> None: 24 | self.main_candle_set = CandleSet(candles) 25 | self.visible_candle_set = CandleSet([]) 26 | 27 | if not width or not height: 28 | width, height = get_terminal_size() 29 | self.set_size(width, height) 30 | 31 | self.compute_visible_candles() 32 | 33 | def compute_height(self, volume_pane: VolumePane) -> None: 34 | volume_pane_height = volume_pane.height if volume_pane.enabled else 0 35 | self.height: int = self.terminal_size[1] - constants.MARGIN_TOP - constants.HEIGHT - volume_pane_height 36 | 37 | def compute_visible_candles(self) -> None: 38 | nb_visible_candles = self.width - constants.WIDTH 39 | if not constants.Y_AXIS_ON_THE_RIGHT: 40 | nb_visible_candles -= constants.MARGIN_RIGHT 41 | self.visible_candle_set.set_candles(self.main_candle_set.candles[-nb_visible_candles:]) 42 | 43 | def reset_candles(self) -> None: 44 | self.main_candle_set.set_candles([]) 45 | self.visible_candle_set.set_candles([]) 46 | 47 | def add_candles(self, candles: Candles) -> None: 48 | self.main_candle_set.add_candles(candles) 49 | self.visible_candle_set.set_candles([]) 50 | 51 | def set_size(self, width: int, height: int) -> None: 52 | self.terminal_size = width, height 53 | self.width, self.height = width, height 54 | -------------------------------------------------------------------------------- /src/candlestick_chart/candle_set.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from typing import TYPE_CHECKING 5 | 6 | if TYPE_CHECKING: 7 | from candlestick_chart.candle import Candles 8 | 9 | 10 | @dataclass(slots=True) 11 | class CandleSet: 12 | candles: Candles 13 | min_price: float = 0.0 14 | max_price: float = 0.0 15 | min_volume: float = 0.0 16 | max_volume: float = 0.0 17 | variation: float = 0.0 18 | average: float = 0.0 19 | last_price: float = 0.0 20 | cumulative_volume: float = 0.0 21 | 22 | def __post_init__(self) -> None: 23 | self._compute_all() 24 | 25 | def add_candles(self, candles: Candles) -> None: 26 | self.candles.extend(candles) 27 | self._compute_all() 28 | 29 | def set_candles(self, candles: Candles) -> None: 30 | self.candles = candles 31 | self._compute_all() 32 | 33 | def _compute_all(self) -> None: 34 | if not self.candles: 35 | return 36 | 37 | candles = self.candles 38 | 39 | open_value = candles[0].open 40 | self.last_price = close_value = candles[-1].close 41 | self.variation = ((close_value - open_value) / open_value) * 100.0 42 | 43 | cumulative_volume = 0.0 44 | average = 0.0 45 | max_price = 0.0 46 | min_price = float("inf") 47 | max_volume = 0.0 48 | min_volume = float("inf") 49 | 50 | for candle in candles: 51 | volume = candle.volume 52 | cumulative_volume += volume 53 | average += candle.close 54 | max_price = max(candle.high, max_price) 55 | min_price = min(candle.low, min_price) 56 | if volume > max_volume: 57 | max_volume = volume 58 | elif volume < min_volume: 59 | min_volume = volume 60 | 61 | self.cumulative_volume = cumulative_volume 62 | self.average = average / len(candles) 63 | self.max_price = max_price 64 | self.min_price = min_price 65 | self.max_volume = max_volume 66 | self.min_volume = min_volume 67 | -------------------------------------------------------------------------------- /src/candlestick_chart/__main__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from argparse import ArgumentParser, Namespace, RawDescriptionHelpFormatter 5 | from typing import TYPE_CHECKING 6 | 7 | from candlestick_chart import Chart, __version__ 8 | from candlestick_chart.utils import ( 9 | hexa_to_rgb, 10 | parse_candles_from_csv, 11 | parse_candles_from_json, 12 | parse_candles_from_stdin, 13 | ) 14 | 15 | if TYPE_CHECKING: 16 | from argparse import Namespace 17 | 18 | 19 | def get_args() -> Namespace: 20 | parser = ArgumentParser( 21 | prog="candlestick-chart", 22 | description="Draw candlesticks charts right into your terminal.", 23 | epilog=( 24 | "This module is maintained by Mickaël Schoentgen ." 25 | "\nYou can always get the latest version of this module at:" 26 | "\n https://github.com/BoboTiG/py-candlestrick-charts" 27 | "\nIf that URL should fail, try contacting the author." 28 | ), 29 | formatter_class=RawDescriptionHelpFormatter, 30 | ) 31 | parser.add_argument( 32 | "-m", 33 | "--mode", 34 | help="Select the method for retrieving the candles.", 35 | choices=["stdin", "csv-file", "json-file"], 36 | required=True, 37 | ) 38 | parser.add_argument("-f", "--file", help="[MODE:*-file] File to read candles from.") 39 | parser.add_argument("--chart-name", default="", help="Sets the chart name.") 40 | parser.add_argument("--bear-color", help="Sets the descending candles color in hexadecimal.") 41 | parser.add_argument("--bull-color", help="Sets the ascending candles color in hexadecimal.") 42 | parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}") 43 | return parser.parse_args(sys.argv[1:]) 44 | 45 | 46 | def main() -> int: 47 | options = get_args() 48 | 49 | if options.mode == "csv-file": 50 | candles = parse_candles_from_csv(options.file) 51 | elif options.mode == "json-file": 52 | candles = parse_candles_from_json(options.file) 53 | else: # stdin 54 | candles = parse_candles_from_stdin() 55 | 56 | chart = Chart(candles, title=options.chart_name) 57 | 58 | if options.bear_color: 59 | r, g, b = hexa_to_rgb(options.bear_color) 60 | chart.set_bear_color(r, g, b) 61 | if options.bull_color: 62 | r, g, b = hexa_to_rgb(options.bull_color) 63 | chart.set_bull_color(r, g, b) 64 | 65 | chart.draw() 66 | return 0 67 | 68 | 69 | if __name__ == "__main__": 70 | sys.exit(main()) 71 | -------------------------------------------------------------------------------- /src/candlestick_chart/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import math 4 | import re 5 | from pathlib import Path 6 | from typing import TYPE_CHECKING, Any 7 | 8 | from candlestick_chart import constants 9 | from candlestick_chart.candle import Candle 10 | 11 | if TYPE_CHECKING: 12 | from collections.abc import Callable, Iterator 13 | 14 | from candlestick_chart.candle import Candles 15 | 16 | 17 | # For compact numbers formatting 18 | REPLACE_CONSECUTIVE_ZEROES = re.compile(r"(0\.)(0{4,})(.{4}).*").sub 19 | 20 | 21 | def fnum_replace_consecutive_zeroes(match: re.Match[str]) -> str: 22 | p1, p2, p3 = match.groups() 23 | return "".join([p1, f"⦗0×{len(p2)}⦘", p3]) 24 | 25 | 26 | def fnum(value: float | str) -> str: 27 | if isinstance(value, str): 28 | try: 29 | value = int(value) 30 | except ValueError: 31 | value = float(value) 32 | 33 | # 0, 0.00, > 1, and > 1.00 (same for negative numbers) 34 | if not value or abs(value) >= 1: 35 | return f"{value:,}" if isinstance(value, int) else f"{value:,.{constants.PRECISION}f}" 36 | 37 | # 0.000000000012345678 -> 0.⦗0×10⦘1234 38 | formatted = REPLACE_CONSECUTIVE_ZEROES(fnum_replace_consecutive_zeroes, f"{value:.18f}") 39 | return formatted if "0×" in formatted else f"{value:.{constants.PRECISION_SMALL}f}" 40 | 41 | 42 | def hexa_to_rgb(hex_code: str) -> tuple[int, int, int]: 43 | hex_code = hex_code.lstrip("#") 44 | r = int(hex_code[:2], 16) 45 | g = int(hex_code[2:4], 16) 46 | b = int(hex_code[4:6], 16) 47 | return r, g, b 48 | 49 | 50 | def make_candles(iterator: Iterator[Any]) -> Candles: 51 | return [Candle(**item) for item in iterator] 52 | 53 | 54 | def parse_candles_from_csv(file: str | Path) -> Candles: 55 | import csv 56 | 57 | with Path(file).open() as fh: 58 | return make_candles(csv.DictReader(fh)) 59 | 60 | 61 | def parse_candles_from_json(file: str | Path) -> Candles: 62 | import json 63 | 64 | return make_candles(json.loads(Path(file).read_text())) 65 | 66 | 67 | def parse_candles_from_stdin() -> Candles: 68 | import json 69 | import sys 70 | 71 | return make_candles(json.loads("".join(sys.stdin))) 72 | 73 | 74 | def round_price( 75 | value: float, *, fn_down: Callable[[float], float] = math.floor, fn_up: Callable[[float], float] = math.ceil 76 | ) -> str: 77 | if constants.Y_AXIS_ROUND_MULTIPLIER > 0.0: 78 | multiplier = constants.Y_AXIS_ROUND_MULTIPLIER 79 | if constants.Y_AXIS_ROUND_DIR == "down": 80 | value = fn_down(value * multiplier) / multiplier 81 | else: 82 | value = fn_up(value * multiplier) / multiplier 83 | return fnum(value) 84 | -------------------------------------------------------------------------------- /src/candlestick_chart/info_bar.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from copy import copy 4 | from dataclasses import dataclass, field 5 | from typing import TYPE_CHECKING 6 | 7 | from candlestick_chart import constants 8 | from candlestick_chart.colors import bold, green, red, yellow 9 | from candlestick_chart.utils import fnum 10 | 11 | if TYPE_CHECKING: 12 | from types import SimpleNamespace 13 | 14 | from candlestick_chart.candle_set import CandleSet 15 | 16 | 17 | def _get_labels() -> SimpleNamespace: 18 | return copy(constants.LABELS) 19 | 20 | 21 | @dataclass(slots=True) 22 | class InfoBar: 23 | name: str 24 | labels: SimpleNamespace = field(init=False, default_factory=_get_labels) 25 | 26 | def _render_average(self, candle_set: CandleSet) -> str: 27 | if not self.labels.average: 28 | return "" 29 | 30 | color = ( 31 | red 32 | if candle_set.last_price > candle_set.average 33 | else green 34 | if candle_set.last_price < candle_set.average 35 | else yellow 36 | ) 37 | return f"{self.labels.average}: {color(fnum(candle_set.last_price))}" 38 | 39 | def _render_highest(self, candle_set: CandleSet) -> str: 40 | return f"{self.labels.highest}: {green(fnum(candle_set.max_price))}" if self.labels.highest else "" 41 | 42 | def _render_lowest(self, candle_set: CandleSet) -> str: 43 | return f"{self.labels.lowest}: {red(fnum(candle_set.min_price))}" if self.labels.lowest else "" 44 | 45 | def _render_price(self, candle_set: CandleSet) -> str: 46 | price = f"{self.labels.price}: {bold(green(fnum(candle_set.last_price)))}" 47 | if self.labels.currency: 48 | price += f" {self.labels.currency}" 49 | return price 50 | 51 | def _render_variation(self, candle_set: CandleSet) -> str: 52 | if not self.labels.variation: 53 | return "" 54 | 55 | variation_output = ("↖", green) if candle_set.variation > 0.0 else ("↙", red) 56 | var = variation_output[1](f"{variation_output[0]} {candle_set.variation:>+.2f}%") 57 | return f"{self.labels.variation}: {var}" 58 | 59 | def _render_volume(self, candle_set: CandleSet) -> str: 60 | return f"{self.labels.volume}: {green(fnum(int(candle_set.cumulative_volume)))}" if self.labels.volume else "" 61 | 62 | def render(self, candle_set: CandleSet, available_width: int) -> str: 63 | return "".join( 64 | ( 65 | "\n", 66 | "─" * available_width, 67 | "\n", 68 | " | ".join( 69 | filter( 70 | len, 71 | ( 72 | self.name, 73 | self._render_price(candle_set), 74 | self._render_highest(candle_set), 75 | self._render_lowest(candle_set), 76 | self._render_variation(candle_set), 77 | self._render_average(candle_set), 78 | self._render_volume(candle_set), 79 | ), 80 | ), 81 | ), 82 | "\n", 83 | ), 84 | ) 85 | -------------------------------------------------------------------------------- /src/candlestick_chart/y_axis.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from typing import TYPE_CHECKING 5 | 6 | from candlestick_chart import constants 7 | from candlestick_chart.colors import color 8 | from candlestick_chart.utils import round_price 9 | 10 | if TYPE_CHECKING: 11 | from candlestick_chart.candle import Candle 12 | from candlestick_chart.chart_data import ChartData 13 | 14 | 15 | @dataclass(slots=True) 16 | class YAxis: 17 | chart_data: ChartData 18 | 19 | def price_to_heights(self, candle: Candle) -> tuple[float, ...]: 20 | chart_data = self.chart_data 21 | height = chart_data.height 22 | candle_set = chart_data.visible_candle_set 23 | 24 | # sourcery skip: min-max-identity 25 | min_open = min(candle.close, candle.open) 26 | max_open = max(candle.close, candle.open) 27 | min_value = candle_set.min_price 28 | diff = (candle_set.max_price - min_value) or 1 29 | 30 | return ( 31 | (candle.high - min_value) / diff * height, # high_y 32 | (candle.low - min_value) / diff * height, # low_y 33 | (max_open - min_value) / diff * height, # max_y 34 | (min_open - min_value) / diff * height, # min_y 35 | ) 36 | 37 | def render_line(self, y: int, *, highlights: dict[str, str | tuple[int, int, int]] | None = None) -> str: 38 | return ( 39 | self.render_empty(y=y, highlights=highlights) 40 | if y % constants.Y_AXIS_SPACING 41 | else self._render_tick(y, highlights or {}) 42 | ) 43 | 44 | def _render_price(self, y: float, highlights: dict[str, str | tuple[int, int, int]]) -> tuple[bool, str]: 45 | chart_data = self.chart_data 46 | min_value = chart_data.visible_candle_set.min_price 47 | max_value = chart_data.visible_candle_set.max_price 48 | height = chart_data.height 49 | 50 | cell_min_length = constants.CHAR_PRECISION + constants.DEC_PRECISION + 1 51 | price = round_price(min_value + (y * (max_value - min_value) / height)) 52 | price_upper = round_price(min_value + ((y + 1) * (max_value - min_value) / height)) 53 | 54 | has_special_price = False 55 | custom_color: str | tuple[int, int, int] = "" 56 | 57 | for target_price, target_color in highlights.items(): 58 | if not (price <= target_price < price_upper): 59 | continue 60 | price = target_price 61 | has_special_price = True 62 | custom_color = target_color 63 | break 64 | 65 | price = ( 66 | f" {color(f'{constants.UNICODE_Y_AXIS_RIGHT} {price:<{cell_min_length}}', custom_color)}" 67 | if constants.Y_AXIS_ON_THE_RIGHT 68 | else ( 69 | f"{color(f'{price:<{cell_min_length}} {constants.UNICODE_Y_AXIS_LEFT}', custom_color)}" 70 | f"{' ' * constants.MARGIN_RIGHT}" 71 | ) 72 | ) 73 | 74 | return has_special_price, price 75 | 76 | def render_empty( 77 | self, 78 | *, 79 | y: float | None = None, 80 | highlights: dict[str, str | tuple[int, int, int]] | None = None, 81 | ) -> str: 82 | if highlights and y: 83 | has_special_price, price = self._render_price(y, highlights) 84 | if has_special_price: 85 | return price 86 | 87 | if constants.Y_AXIS_ON_THE_RIGHT: 88 | return f" {constants.UNICODE_Y_AXIS}" 89 | 90 | cell = " " * (constants.CHAR_PRECISION + constants.DEC_PRECISION + 2) 91 | margin = " " * constants.MARGIN_RIGHT 92 | return f"{cell}{constants.UNICODE_Y_AXIS}{margin}" 93 | 94 | def _render_tick(self, y: int, highlights: dict[str, str | tuple[int, int, int]]) -> str: 95 | _, price = self._render_price(y, highlights) 96 | return price 97 | -------------------------------------------------------------------------------- /src/tests/test_chart.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | import pytest 4 | from rich.console import Console 5 | 6 | from candlestick_chart import constants 7 | from candlestick_chart.candle import Candle 8 | from candlestick_chart.chart import Chart 9 | 10 | CANDLE = Candle(open=1, high=2, low=0.5, close=1.2, volume=1_234_567) 11 | WIDTH = 80 12 | HEIGHT = 24 13 | 14 | 15 | @pytest.fixture 16 | def chart() -> Chart: 17 | return Chart([CANDLE], title="BTC/USDT", width=WIDTH, height=HEIGHT) 18 | 19 | 20 | def test_rich_repr(chart: Chart) -> None: 21 | console = Console(highlight=False, force_terminal=True, width=WIDTH, height=HEIGHT) 22 | with console.capture() as capture: 23 | console.print(chart) 24 | 25 | output = capture.get() 26 | assert chart.info_bar.name in output 27 | assert constants.UNICODE_Y_AXIS_LEFT in output 28 | assert constants.UNICODE_Y_AXIS_RIGHT not in output 29 | 30 | 31 | def test_rich_repr_with_y_axis_on_the_right(chart: Chart) -> None: 32 | console = Console(highlight=False, force_terminal=True, width=WIDTH, height=HEIGHT) 33 | with ( 34 | patch("candlestick_chart.constants.Y_AXIS_ON_THE_RIGHT", True), # noqa: FBT003 35 | console.capture() as capture, 36 | ): 37 | console.print(chart) 38 | 39 | output = capture.get() 40 | assert chart.info_bar.name in output 41 | assert constants.UNICODE_Y_AXIS_LEFT not in output 42 | assert constants.UNICODE_Y_AXIS_RIGHT in output 43 | 44 | 45 | def test_set_label(chart: Chart) -> None: 46 | value = "foo" 47 | 48 | for label in vars(constants.LABELS): 49 | assert getattr(chart.info_bar.labels, label) != value 50 | chart.set_label(label, value) 51 | assert getattr(chart.info_bar.labels, label) == value 52 | 53 | 54 | def test_set_highlight(chart: Chart) -> None: 55 | assert not chart.highlights 56 | 57 | # Add highlighted prices 58 | chart.set_highlight("4.2", "red") 59 | chart.set_highlight("4.2", "green") 60 | chart.set_highlight("2.4569", "orange") 61 | assert chart.highlights == {"2.4569": "orange", "4.2": "green"} 62 | 63 | # Remove a price 64 | chart.set_highlight("2.4569", "") 65 | assert chart.highlights == {"4.2": "green"} 66 | 67 | 68 | def test_set_name(chart: Chart) -> None: 69 | name = "Magic Chart" 70 | assert chart.info_bar.name != name 71 | 72 | chart.set_name(name) 73 | assert chart.info_bar.name == name 74 | 75 | 76 | def test_set_vol_bear_color(chart: Chart) -> None: 77 | color = (3, 2, 1) 78 | assert chart.volume_pane.bearish_color != color 79 | 80 | chart.set_vol_bear_color(*color) 81 | assert chart.volume_pane.bearish_color == color 82 | 83 | 84 | def test_set_vol_bull_color(chart: Chart) -> None: 85 | color = (1, 2, 3) 86 | assert chart.volume_pane.bullish_color != color 87 | 88 | chart.set_vol_bull_color(*color) 89 | assert chart.volume_pane.bullish_color == color 90 | 91 | 92 | def test_set_volume_pane_enabled(chart: Chart) -> None: 93 | assert chart.volume_pane.enabled 94 | 95 | chart.set_volume_pane_enabled(False) 96 | assert not chart.volume_pane.enabled 97 | 98 | 99 | def test_set_volume_pane_unicode_fill(chart: Chart) -> None: 100 | value = "°" 101 | 102 | assert chart.volume_pane.unicode_fill != value 103 | 104 | chart.set_volume_pane_unicode_fill(value) 105 | assert chart.volume_pane.unicode_fill == value 106 | 107 | 108 | def test_update_candles(chart: Chart) -> None: 109 | assert len(chart.chart_data.main_candle_set.candles) == 1 110 | 111 | chart.update_candles([CANDLE] * 4, reset=True) 112 | assert len(chart.chart_data.main_candle_set.candles) == 4 113 | 114 | candle = CANDLE 115 | candle.volume = 0.00001 116 | chart.update_candles([candle]) 117 | assert len(chart.chart_data.main_candle_set.candles) == 5 118 | 119 | 120 | def test_update_size(chart: Chart) -> None: 121 | assert chart.chart_data.terminal_size == (WIDTH, HEIGHT) 122 | 123 | # No change 124 | chart.update_size(WIDTH, HEIGHT) 125 | assert chart.chart_data.terminal_size == (WIDTH, HEIGHT) 126 | 127 | # New size 128 | chart.update_size(WIDTH * 2, HEIGHT * 2) 129 | assert chart.chart_data.terminal_size == (WIDTH * 2, HEIGHT * 2) 130 | 131 | # No change 132 | chart.update_size(WIDTH * 2, HEIGHT * 2) 133 | assert chart.chart_data.terminal_size == (WIDTH * 2, HEIGHT * 2) 134 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "candlestick-chart" 7 | description = "Draw candlesticks charts right into your terminal, using Python!" 8 | readme = "README.md" 9 | requires-python = ">= 3.10" 10 | authors = [ 11 | { name = "Mickaël Schoentgen", email="contact@tiger-222.fr" }, 12 | ] 13 | maintainers = [ 14 | { name = "Mickaël Schoentgen", email="contact@tiger-222.fr" }, 15 | ] 16 | license = { file = "LICENSE" } 17 | classifiers = [ 18 | "Development Status :: 5 - Production/Stable", 19 | "Environment :: Console", 20 | "Intended Audience :: Developers", 21 | "Intended Audience :: Education", 22 | "Intended Audience :: End Users/Desktop", 23 | "Intended Audience :: Financial and Insurance Industry", 24 | "License :: OSI Approved :: MIT License", 25 | "Programming Language :: Python :: Implementation :: CPython", 26 | "Programming Language :: Python", 27 | "Programming Language :: Python :: 3", 28 | "Programming Language :: Python :: 3 :: Only", 29 | "Programming Language :: Python :: 3.10", 30 | "Programming Language :: Python :: 3.11", 31 | "Programming Language :: Python :: 3.12", 32 | "Programming Language :: Python :: 3.13", 33 | "Programming Language :: Python :: 3.14", 34 | "Topic :: Multimedia :: Graphics :: Viewers", 35 | "Topic :: Office/Business :: Financial", 36 | "Topic :: Scientific/Engineering :: Visualization", 37 | "Topic :: Software Development :: Libraries :: Python Modules", 38 | "Topic :: Software Development :: User Interfaces", 39 | "Topic :: Terminals", 40 | ] 41 | keywords = [ 42 | "candle", 43 | "candlestick", 44 | "chart", 45 | "cli", 46 | "console", 47 | "crypto", 48 | "stock", 49 | "stock-market", 50 | "options", 51 | "terminal", 52 | "trading", 53 | ] 54 | dynamic = ["version"] 55 | 56 | [project.urls] 57 | Homepage = "https://pypi.org/project/py-candlestick-chart" 58 | Changelog = "https://github.com/BoboTiG/py-candlestick-chart/blob/main/CHANGELOG.md" 59 | Source = "https://github.com/BoboTiG/py-candlestick-chart" 60 | Sponsor = "https://github.com/sponsors/BoboTiG" 61 | Tracker = "https://github.com/BoboTiG/py-candlestick-chart/issues" 62 | "Released Versions" = "https://github.com/BoboTiG/py-candlestick-chart/releases" 63 | 64 | [project.scripts] 65 | candlestick-chart = "candlestick_chart.__main__:main" 66 | 67 | [project.optional-dependencies] 68 | test = [ 69 | "mypy==1.19.1", 70 | "pytest==9.0.2", 71 | "pytest-cov==7.0.0", 72 | "requests==2.32.5", 73 | "rich==14.2.0", 74 | "ruff==0.14.10", 75 | "types-requests", 76 | ] 77 | dev = [ 78 | "build==1.3.0", 79 | "twine==6.2.0", 80 | ] 81 | 82 | [tool.hatch.version] 83 | path = "src/candlestick_chart/__init__.py" 84 | 85 | [tool.hatch.build] 86 | skip-excluded-dirs = true 87 | 88 | [tool.hatch.build.targets.sdist] 89 | only-include = [ 90 | "CHANGELOG.md", 91 | "examples", 92 | "src", 93 | ] 94 | 95 | [tool.hatch.build.targets.wheel] 96 | packages = [ 97 | "src/candlestick_chart", 98 | ] 99 | 100 | [tool.coverage.report] 101 | exclude_also = [ 102 | "if TYPE_CHECKING:", 103 | 'if __name__ == "__main__":', 104 | ] 105 | 106 | [tool.mypy] 107 | # Ensure we know what we do 108 | warn_redundant_casts = true 109 | warn_unused_ignores = true 110 | warn_unused_configs = true 111 | 112 | # Imports management 113 | ignore_missing_imports = true 114 | follow_imports = "skip" 115 | 116 | # Ensure full coverage 117 | disallow_untyped_defs = true 118 | disallow_incomplete_defs = true 119 | disallow_untyped_calls = true 120 | 121 | # Restrict dynamic typing (a little) 122 | # e.g. `x: List[Any]` or x: List` 123 | # disallow_any_generics = true 124 | 125 | strict_equality = true 126 | 127 | [tool.pytest.ini_options] 128 | pythonpath = "src" 129 | addopts = """ 130 | --cov=candlestick_chart 131 | --cov-report=term-missing:skip-covered 132 | -r fE 133 | --showlocals 134 | --strict-markers 135 | -vvv 136 | """ 137 | 138 | [tool.ruff] 139 | line-length = 120 140 | indent-width = 4 141 | target-version = "py310" 142 | 143 | [tool.ruff.lint] 144 | fixable = ["ALL"] 145 | extend-select = ["ALL"] 146 | ignore = [ 147 | "C901", # complexity 148 | "COM812", # conflict 149 | "D", # docstrings 150 | "ISC001", # conflict 151 | "PLR2004", # constant variables 152 | "RUF001", # unicode chars 153 | "RUF003", # unicode chars 154 | "S101", # assert in tests 155 | ] 156 | 157 | [tool.ruff.format] 158 | quote-style = "double" 159 | indent-style = "space" 160 | skip-magic-trailing-comma = false 161 | line-ending = "auto" 162 | -------------------------------------------------------------------------------- /src/candlestick_chart/chart_renderer.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from math import ceil, floor 5 | from typing import TYPE_CHECKING 6 | 7 | from candlestick_chart import constants 8 | from candlestick_chart.candle import Candle, CandleType 9 | from candlestick_chart.colors import truecolor 10 | 11 | if TYPE_CHECKING: 12 | from candlestick_chart.chart import Chart 13 | from candlestick_chart.y_axis import YAxis 14 | 15 | 16 | @dataclass(slots=True) 17 | class ChartRenderer: 18 | bearish_color: tuple[int, int, int] = (234, 74, 90) 19 | bullish_color: tuple[int, int, int] = (52, 208, 88) 20 | 21 | def _colorize(self, candle_type: int, string: str) -> str: 22 | color = self.bearish_color if candle_type == CandleType.bearish else self.bullish_color 23 | return truecolor(string, *color) 24 | 25 | def _render_candle(self, candle: Candle, y: int, y_axis: YAxis) -> str: 26 | height_unit = float(y) 27 | high_y, low_y, max_y, min_y = y_axis.price_to_heights(candle) 28 | output = constants.UNICODE_VOID 29 | 30 | ceil_ = ceil 31 | floor_ = floor 32 | 33 | if ceil_(high_y) >= height_unit >= floor_(max_y): 34 | max_diff = max_y - height_unit 35 | high_diff = high_y - height_unit 36 | if max_diff > constants.MAX_DIFF_THRESHOLD: 37 | output = constants.UNICODE_BODY 38 | elif max_diff > constants.MIN_DIFF_THRESHOLD: 39 | output = ( 40 | constants.UNICODE_TOP 41 | if high_diff > constants.MAX_DIFF_THRESHOLD 42 | else constants.UNICODE_HALF_BODY_BOTTOM 43 | ) 44 | elif high_diff > constants.MAX_DIFF_THRESHOLD: 45 | output = constants.UNICODE_WICK 46 | elif high_diff > constants.MIN_DIFF_THRESHOLD: 47 | output = constants.UNICODE_WICK_UPPER 48 | elif ceil_(min_y) >= height_unit >= floor_(low_y): 49 | min_diff = min_y - height_unit 50 | low_diff = low_y - height_unit 51 | if min_diff < constants.MIN_DIFF_THRESHOLD: 52 | output = constants.UNICODE_BODY 53 | elif min_diff < constants.MAX_DIFF_THRESHOLD: 54 | output = ( 55 | constants.UNICODE_BOTTOM 56 | if low_diff < constants.MIN_DIFF_THRESHOLD 57 | else constants.UNICODE_HALF_BODY_TOP 58 | ) 59 | elif low_diff < constants.MIN_DIFF_THRESHOLD: 60 | output = constants.UNICODE_WICK 61 | elif low_diff < constants.MAX_DIFF_THRESHOLD: 62 | output = constants.UNICODE_WICK_LOWER 63 | elif max_y >= height_unit >= ceil_(min_y): 64 | output = constants.UNICODE_BODY 65 | 66 | return self._colorize(candle.type, output) 67 | 68 | def render(self, chart: Chart) -> str: 69 | output: list[str] = [] 70 | chart_data = chart.chart_data 71 | chart_data.compute_height(chart.volume_pane) 72 | candle_set = chart_data.visible_candle_set 73 | candles = candle_set.candles 74 | 75 | graduations_on_right = constants.Y_AXIS_ON_THE_RIGHT 76 | render_line = chart.y_axis.render_line 77 | highlights = chart.highlights or {} 78 | 79 | for y in range(chart_data.height, 0, -1): 80 | if graduations_on_right: 81 | output.append("\n") 82 | else: 83 | output.extend(("\n", render_line(y=y, highlights=highlights))) 84 | 85 | output.extend(self._render_candle(candle, y, chart.y_axis) for candle in candles) 86 | 87 | if graduations_on_right: 88 | output.append(render_line(y=y, highlights=highlights)) 89 | 90 | if chart.volume_pane.enabled: 91 | render_empty = chart.y_axis.render_empty 92 | render = chart.volume_pane.render 93 | max_volume = candle_set.max_volume 94 | 95 | for y in range(chart.volume_pane.height, 0, -1): 96 | if graduations_on_right: 97 | output.append("\n") 98 | else: 99 | output.extend(("\n", render_empty())) 100 | 101 | output.extend(render(candle, y, max_volume) for candle in candles) 102 | 103 | if graduations_on_right: 104 | output.append(render_empty()) 105 | 106 | output.append(chart.info_bar.render(chart_data.main_candle_set, chart_data.width)) 107 | 108 | return "".join(output) 109 | -------------------------------------------------------------------------------- /src/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from io import StringIO 2 | from pathlib import Path 3 | from unittest.mock import patch 4 | 5 | import pytest 6 | 7 | from candlestick_chart import Candle, utils 8 | 9 | 10 | @pytest.mark.parametrize( 11 | ("value", "expected"), 12 | [ 13 | (0, "0"), 14 | (0.0, "0.00"), 15 | (123456789, "123,456,789"), 16 | (1.23456789, "1.23"), 17 | (1234.56789, "1,234.57"), 18 | (1.0, "1.00"), 19 | (0.1, "0.1000"), 20 | (0.01, "0.0100"), 21 | (0.001, "0.0010"), 22 | (0.0001, "0.0001"), 23 | (0.00001, "0.⦗0×4⦘1000"), 24 | (0.000001, "0.⦗0×5⦘1000"), 25 | (0.0000001, "0.⦗0×6⦘1000"), 26 | (0.000000000012340000, "0.⦗0×10⦘1234"), 27 | (0.000000000012345678, "0.⦗0×10⦘1234"), 28 | (0.123456789, "0.1235"), 29 | ], 30 | ) 31 | def test_fnum(value: float, expected: str) -> None: 32 | assert utils.fnum(value) == expected 33 | assert utils.fnum(str(value)) == expected 34 | if value != 0.0: 35 | assert utils.fnum(value * -1) == f"-{expected}" 36 | 37 | 38 | def test_fnum_precision() -> None: 39 | with patch("candlestick_chart.constants.PRECISION", 0): 40 | assert utils.fnum(1.0) == "1" 41 | with patch("candlestick_chart.constants.PRECISION", 10): 42 | assert utils.fnum(1.0) == "1.0000000000" 43 | 44 | 45 | def test_fnum_precision_small() -> None: 46 | with patch("candlestick_chart.constants.PRECISION_SMALL", 0): 47 | assert utils.fnum(0.123456789) == "0" 48 | with patch("candlestick_chart.constants.PRECISION_SMALL", 6): 49 | assert utils.fnum(0.123456789) == "0.123457" 50 | 51 | 52 | @pytest.mark.parametrize( 53 | ("code", "expected"), 54 | [ 55 | ("#b967ff", (185, 103, 255)), 56 | ("ff6b99", (255, 107, 153)), 57 | ], 58 | ) 59 | def test_hexa_to_rgb(code: str, expected: tuple[int, int, int]) -> None: 60 | assert utils.hexa_to_rgb(code) == expected 61 | 62 | 63 | def test_parse_candles_from_csv(data: Path) -> None: 64 | file = str(data / "BTC-USD.csv") 65 | candles = utils.parse_candles_from_csv(file) 66 | assert len(candles) == 312 67 | assert candles[0] == Candle( 68 | open=28994.009766, 69 | close=29374.152344, 70 | high=29600.626953, 71 | low=28803.585938, 72 | volume=40730301359.0, 73 | timestamp=1652997600.0, 74 | ) 75 | assert candles[-1] == Candle( 76 | open=63344.066406, 77 | close=67566.828125, 78 | high=67673.742188, 79 | low=63344.066406, 80 | volume=41125608330.0, 81 | timestamp=1679868000.0, 82 | ) 83 | 84 | 85 | def test_parse_candles_from_json(data: Path) -> None: 86 | file = str(data / "BTC-chart.json") 87 | candles = utils.parse_candles_from_json(file) 88 | assert len(candles) == 2 89 | assert candles[0] == Candle( 90 | open=28994.009766, 91 | close=29374.152344, 92 | high=29600.626953, 93 | low=28803.585938, 94 | volume=0.0, 95 | timestamp=0.0, 96 | ) 97 | assert candles[1] == Candle( 98 | open=29376.455078, 99 | close=32127.267578, 100 | high=33155.117188, 101 | low=29091.181641, 102 | volume=0.0, 103 | timestamp=0.0, 104 | ) 105 | 106 | 107 | def test_parse_candles_from_stdin(monkeypatch: pytest.MonkeyPatch) -> None: 108 | monkeypatch.setattr( 109 | "sys.stdin", 110 | StringIO( 111 | """[ 112 | { 113 | "open": 28994.009766, 114 | "high": 29600.626953, 115 | "low": 28803.585938, 116 | "close": 29374.152344 117 | }, 118 | { 119 | "open": 29376.455078, 120 | "high": 33155.117188, 121 | "low": 29091.181641, 122 | "close": 32127.267578 123 | } 124 | ]""", 125 | ), 126 | ) 127 | candles = utils.parse_candles_from_stdin() 128 | assert len(candles) == 2 129 | assert candles[0] == Candle( 130 | open=28994.009766, 131 | close=29374.152344, 132 | high=29600.626953, 133 | low=28803.585938, 134 | volume=0.0, 135 | timestamp=0.0, 136 | ) 137 | assert candles[1] == Candle( 138 | open=29376.455078, 139 | close=32127.267578, 140 | high=33155.117188, 141 | low=29091.181641, 142 | volume=0.0, 143 | timestamp=0.0, 144 | ) 145 | 146 | 147 | @pytest.mark.parametrize( 148 | ("value", "expected_down", "expected_up"), 149 | [ 150 | (0.0, "0.00", "0.00"), 151 | (0.01234, "0.0100", "0.0200"), 152 | (32_574.913_021_333_333, "32,574.91", "32,574.92"), 153 | ], 154 | ) 155 | def test_round_price(value: float, expected_down: str, expected_up: str) -> None: 156 | with patch("candlestick_chart.constants.Y_AXIS_ROUND_MULTIPLIER", 1 / 0.01): 157 | with patch("candlestick_chart.constants.Y_AXIS_ROUND_DIR", "down"): 158 | assert utils.round_price(value) == expected_down 159 | with patch("candlestick_chart.constants.Y_AXIS_ROUND_DIR", "up"): 160 | assert utils.round_price(value) == expected_up 161 | -------------------------------------------------------------------------------- /src/candlestick_chart/chart.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | from candlestick_chart.chart_data import ChartData 6 | from candlestick_chart.chart_renderer import ChartRenderer 7 | from candlestick_chart.info_bar import InfoBar 8 | from candlestick_chart.volume_pane import VolumePane 9 | from candlestick_chart.y_axis import YAxis 10 | 11 | if TYPE_CHECKING: 12 | from collections.abc import Iterable 13 | 14 | from rich.console import Console, ConsoleOptions 15 | 16 | from candlestick_chart.candle import Candles 17 | 18 | 19 | class Chart: 20 | __slots__ = ( 21 | "chart_data", 22 | "info_bar", 23 | "renderer", 24 | "highlights", 25 | "volume_pane", 26 | "y_axis", 27 | ) 28 | 29 | def __init__( 30 | self, 31 | candles: Candles, 32 | *, 33 | title: str = "My chart", 34 | width: int = 0, 35 | height: int = 0, 36 | renderer_cls: type[ChartRenderer] = ChartRenderer, 37 | ) -> None: 38 | self.renderer = renderer_cls() 39 | self.chart_data = ChartData(candles, width=width, height=height) 40 | self.y_axis = YAxis(self.chart_data) 41 | self.info_bar = InfoBar(title) 42 | self.volume_pane = VolumePane(int(self.chart_data.height / 6)) 43 | 44 | # A dict of price -> color to display custom colors on specific prices on the Y-axis 45 | self.highlights: dict[str, str | tuple[int, int, int]] = {} 46 | 47 | def __rich_console__(self, console: Console, options: ConsoleOptions) -> Iterable[str]: 48 | from rich.ansi import AnsiDecoder 49 | from rich.console import Group 50 | 51 | self.update_size( 52 | options.max_width or console.width, 53 | options.height or console.height, 54 | ) 55 | yield Group(*AnsiDecoder().decode(self._render())) 56 | 57 | def _render(self) -> str: 58 | """Get the full chart as a single string.""" 59 | return self.renderer.render(self) 60 | 61 | def draw(self) -> None: 62 | """Draws the chart by outputting multiples strings in the terminal.""" 63 | print(self._render()) # noqa: T201 64 | 65 | def set_bear_color(self, r: int, g: int, b: int) -> None: 66 | """Set the color of the bearish candle. 67 | The default color is (234, 74, 90). 68 | """ 69 | self.renderer.bearish_color = r, g, b 70 | 71 | def set_bull_color(self, r: int, g: int, b: int) -> None: 72 | """Set the color of the bullish candle. 73 | The default color is (52, 208, 88). 74 | """ 75 | self.renderer.bullish_color = r, g, b 76 | 77 | def set_label(self, label: str, value: str) -> None: 78 | """Set the info bar `label` text with `value`. 79 | An empty string will disable its display. 80 | """ 81 | setattr(self.info_bar.labels, label, value) 82 | 83 | def set_highlight(self, price: str, color: str | tuple[int, int, int]) -> None: 84 | """Set a specific `color` for `price` in the Y-axis. 85 | `price` must be a string to be able to compare to real values 86 | and because comparing floats would almost never match. 87 | Set `color` is an empty string, or None, to reset the color. 88 | 89 | Examples: 90 | >>> chart.set_highlight(fnum(52,348.63), "red") 91 | >>> chart.set_highlight(fnum(52,348.63), (255, 0, 0)) 92 | >>> chart.set_highlight(fnum(52,348.63), "91m") 93 | >>> chart.set_highlight(fnum(52,348.63), "91;47m") 94 | 95 | """ 96 | if color: 97 | self.highlights[price] = color 98 | else: 99 | self.highlights.pop(price, None) 100 | 101 | def set_name(self, name: str) -> None: 102 | """Set the name of the chart in the info bar.""" 103 | self.info_bar.name = name 104 | 105 | def set_vol_bear_color(self, r: int, g: int, b: int) -> None: 106 | """Sets the color of the volume when the candle is bearish. 107 | The default color is (234, 74, 90). 108 | """ 109 | self.volume_pane.bearish_color = r, g, b 110 | 111 | def set_vol_bull_color(self, r: int, g: int, b: int) -> None: 112 | """Sets the color of the volume when the candle is bullish. 113 | The default color is (52, 208, 88). 114 | """ 115 | self.volume_pane.bullish_color = r, g, b 116 | 117 | def set_volume_pane_enabled(self, enabled: bool) -> None: # noqa: FBT001 118 | """Hide or show the volume pane.""" 119 | self.volume_pane.enabled = enabled 120 | 121 | def set_volume_pane_height(self, height: int) -> None: 122 | """Set the volume pane height. 123 | Default is 1/6 of the terminal height. 124 | """ 125 | self.volume_pane.height = height 126 | 127 | def set_volume_pane_unicode_fill(self, unicode_fill: str) -> None: 128 | """Set the character for drawing the volume bars.""" 129 | self.volume_pane.unicode_fill = unicode_fill 130 | 131 | def update_candles(self, candles: Candles, *, reset: bool = False) -> None: 132 | """Convenient helper to update candles.""" 133 | if reset: 134 | self.chart_data.reset_candles() 135 | self.chart_data.add_candles(candles) 136 | self.chart_data.compute_visible_candles() 137 | 138 | def update_size(self, width: int, height: int) -> None: 139 | """Adapt chart width, and height. Yes, it is responsive too!""" 140 | if (width, height) == self.chart_data.terminal_size: 141 | return 142 | 143 | self.chart_data.set_size(width, height) 144 | self.chart_data.compute_visible_candles() 145 | 146 | if self.volume_pane.enabled: 147 | self.set_volume_pane_height(int(self.chart_data.height / 6)) 148 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Candlesticks Chart 2 | 3 | > [!TIP] 4 | > Become **my boss** to help me work on this awesome software, and make the world better: 5 | > 6 | > [![Patreon](https://img.shields.io/badge/Patreon-F96854?style=for-the-badge&logo=patreon&logoColor=white)](https://www.patreon.com/mschoentgen) 7 | 8 | 📈 Draw candlesticks charts right into your terminal, using Python! 9 | 10 | ![Preview](examples/screenshot.png) 11 | 12 | This is a portage from the great [cli-candlestick-chart](https://github.com/Julien-R44/cli-candlestick-chart) (developed by [Julien-R44](https://github.com/Julien-R44), written in Rust). 13 | You are looking at the Python 3.10+ version. 14 | 15 | Note: not tested on macOS, nor Windows (it will likely fail to render colors). 16 | 17 | **Table of contents**: 18 | - [Python Candlesticks Chart](#python-candlesticks-chart) 19 | - [Features](#features) 20 | - [Installation](#installation) 21 | - [Demonstration](#demonstration) 22 | - [Binary Usage](#binary-usage) 23 | - [Examples](#examples) 24 | - [API](#api) 25 | - [Binary](#binary) 26 | - [Read CSV from file](#read-csv-from-file) 27 | - [Read JSON from file](#read-json-from-file) 28 | - [Read from stdin](#read-from-stdin) 29 | - [Developers](#developers) 30 | 31 | ## Features 32 | 33 | - Auto-fit to terminal size 34 | - Practical formatting for big, and tiny, numbers 35 | - Integration with [Rich](https://github.com/Textualize/rich) 36 | - Simple, yet customizable, API 37 | - Exact same API as the [Rust version](https://github.com/Julien-R44/cli-candlestick-chart), plus some [sugar](#demonstration) 38 | - Simple installation, no external dependencies 39 | 40 | ## Installation 41 | 42 | ```bash 43 | $ python -m pip install -U candlestick-chart 44 | ``` 45 | 46 | ## Demonstration 47 | 48 | ```python 49 | from candlestick_chart import Candle, Chart 50 | 51 | 52 | # Add some candles 53 | candles = [ 54 | Candle(open=133.520004, close=133.610001, high=126.760002, low=129.410004), 55 | Candle(open=128.889999, close=131.740005, high=128.429993, low=131.009995), 56 | Candle(open=127.720001, close=131.050003, high=126.379997, low=126.599998), 57 | Candle(open=128.360001, close=131.630005, high=127.860001, low=130.919998), 58 | Candle(open=132.429993, close=132.630005, high=130.229996, low=132.050003), 59 | ] 60 | 61 | # Create and display the chart 62 | # Optional keyword arguments: title, width, height 63 | chart = Chart(candles, title="Optional title") 64 | 65 | # Set the chart title 66 | chart.set_name("BTC/USDT") 67 | 68 | # Set customs colors 69 | chart.set_bear_color(1, 205, 254) 70 | chart.set_bull_color(255, 107, 153) 71 | chart.set_vol_bull_color(1, 205, 254) 72 | chart.set_vol_bear_color(255, 107, 153) 73 | 74 | # Set custom labels (empty string => label not displayed) 75 | chart.set_label("highest", "ATH") 76 | chart.set_label("lowest", "ATL") 77 | chart.set_label("average", "") 78 | chart.set_label("volume", "") 79 | 80 | # Volume pane settings 81 | chart.set_volume_pane_height(6) 82 | chart.set_volume_pane_enabled(False) 83 | 84 | # And, it is also responsive! 85 | new_width = 200 86 | new_height = 150 87 | chart.update_size(new_width, new_height) 88 | 89 | # By the way, did you know that you can add more candles in real-time? 90 | chart.update_candles(candles[:3]) 91 | # Or completely replace current candles 92 | chart.update_candles(candles[:3], reset=True) 93 | 94 | # Set a custom color at price 52,348.63 95 | chart.set_highlight(fnum(52_348.63), "red") 96 | chart.set_highlight(fnum(52_348.63), (255, 0, 0)) 97 | chart.set_highlight(fnum(52_348.63), "91m") 98 | chart.set_highlight(fnum(52_348.63), "91;47m") 99 | 100 | chart.draw() 101 | ``` 102 | 103 | # Binary Usage 104 | 105 | When installing the library, an executable is made available (`candlestick-chart`): 106 | 107 | ```bash 108 | candlestick-chart --help 109 | 110 | options: 111 | -h, --help show this help message and exit 112 | -m {stdin,csv-file,json-file}, --mode {stdin,csv-file,json-file} 113 | Select the method for retrieving the candles. 114 | -f FILE, --file FILE [MODE:*-file] File to read candles from. 115 | --chart-name CHART_NAME 116 | Sets the chart name. 117 | --bear-color BEAR_COLOR 118 | Sets the descending candles color in hexadecimal. 119 | --bull-color BULL_COLOR 120 | Sets the ascending candles color in hexadecimal. 121 | --version show program's version number and exit 122 | ``` 123 | 124 | When requesting the JSON or stdin mode, the library expects a JSON with the following format: 125 | 126 | ```json 127 | [ 128 | { 129 | "open": 28994.009766, 130 | "high": 29600.626953, 131 | "low": 28803.585938, 132 | "close": 29374.152344 133 | }, 134 | ... 135 | ] 136 | ``` 137 | 138 | For all requests, here are supported fields: 139 | 140 | ```python 141 | "open": float # mandatory 142 | "close": float # mandatory 143 | "high": float # mandatory 144 | "low": float # mandatory 145 | "volume": float 146 | "timestamp": float 147 | ``` 148 | 149 | # Examples 150 | 151 | ## API 152 | 153 | - [Basic example with CSV parsing](examples/basic_from_csv_file.py): run with `$ python examples/basic_from_csv_file.py` 154 | - [Basic example with JSON parsing](examples/basic_from_json_file.py): run with `$ python examples/basic_from_json_file.py` 155 | - [Basic example with stdin parsing](examples/basic_from_stdin.sh): run with `$ ./examples/basic_from_stdin.sh` 156 | - [Fetch candles from Binance](examples/fetch_from_binance.py): run with `$ python examples/fetch_from_binance.py` 157 | - [Integration with Rich](examples/integrate_with_rich.py): run with `$ python examples/integrate_with_rich.py` 158 | - [Using a custom chart renderer class](examples/custom_renderer_class.py): run with `$ python examples/custom_renderer_class.py` 159 | 160 | ## Binary 161 | 162 | ### Read CSV from file 163 | 164 | ```bash 165 | candlestick-chart \ 166 | --mode=csv-file \ 167 | --file='./examples/BTC-USD.csv' \ 168 | --chart-name='My BTC Chart' \ 169 | --bear-color='#b967ff' \ 170 | --bull-color='ff6b99' 171 | ``` 172 | ### Read JSON from file 173 | 174 | ```bash 175 | candlestick-chart \ 176 | --mode=json-file \ 177 | --file='./examples/BTC-chart.json' \ 178 | --chart-name='My BTC Chart' \ 179 | --bear-color='#b967ff' \ 180 | --bull-color='ff6b99' 181 | ``` 182 | 183 | ### Read from stdin 184 | 185 | ```bash 186 | echo '[ 187 | { 188 | "open": 28994.009766, 189 | "high": 29600.626953, 190 | "low": 28803.585938, 191 | "close": 29374.152344 192 | }, 193 | { 194 | "open": 29376.455078, 195 | "high": 33155.117188, 196 | "low": 29091.181641, 197 | "close": 32127.267578 198 | } 199 | ]' | candlestick-chart \ 200 | --mode=stdin \ 201 | --chart-name='My BTC Chart' \ 202 | --bear-color='#b967ff' \ 203 | --bull-color='ff6b99' 204 | ``` 205 | 206 | ## Developers 207 | 208 | Setup: 209 | 210 | ```shell 211 | python -m venv venv 212 | . venv/bin/activate 213 | python -m pip install -U pip 214 | ``` 215 | 216 | Install: 217 | 218 | ```shell 219 | python -m pip install -e '.[test]' 220 | ``` 221 | 222 | Test: 223 | 224 | ```shell 225 | python -m pytest 226 | ``` 227 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] - 202x-xx-xx 9 | 10 | ### Added 11 | - 12 | 13 | ### Changed 14 | - 15 | 16 | ### Removed 17 | - 18 | 19 | ## [3.1.0] - 2024-11-02 20 | 21 | ### Added 22 | - Support for Python 3.14. 23 | - Introduce `constants.UNICODE_Y_AXIS`, `constants.UNICODE_Y_AXIS_LEFT`, and `constants.UNICODE_Y_AXIS_RIGHT`, constants to control the Unicode character printed next to prices on the Y-axis (defaults to `│`, `┤`, and `├`, respectively). 24 | - New color: gray (which is an alias to the misspelled "grey"). 25 | - New `utils.round_price()` function to either round down, or up, the price on the Y-axis. Previously, it was a private method of the `YAxis` class. 26 | - 100% coverage with tests! 27 | 28 | ### Changed 29 | - Fixed `CandleSet.min_volume` being always `0.0` (commit [6975d7f]). 30 | - Those functions now also accept a `pathlib.Path` argument, in addition to the original `str`: `utils.parse_candles_from_csv()`, and `utils.parse_candles_from_json()`. 31 | - Updated the `pypa/gh-action-pypi-publish` GitHub action from `master` to `release/v1`. 32 | 33 | ## [3.0.0] - 2024-09-24 34 | 35 | ### Added 36 | - Support for Python 3.13. 37 | - CI to automatically publish releases on tag creation. 38 | - Using now the `ruff` module for the code quality. 39 | 40 | ### Changed 41 | - (breaking change) Enforced usage of proper keyword-arguments. 42 | - Use absolute imports. 43 | - Level up the packaging using `hatchling`. 44 | - Pin all requirements. 45 | - Updated README's code example (fixes [#13]). 46 | - Fixed typos using `codespell` ([#15] by @kianmeng). 47 | 48 | ### Removed 49 | - Modules `black`, `flake8`, and `isort`, are no more used for the code quality. 50 | 51 | ## [2.7.0] - 2024-04-23 52 | 53 | ### Added 54 | - Support for Python 3.12. 55 | - CI to run unit tests. 56 | - Tests for the CLI entry point. 57 | 58 | ### Changed 59 | - Set the default chart name to a blank string in the CLI (fixes [#9]). 60 | - Use `shutil.get_terminal_size()` instead of `os.get_terminal_size()` to be able to run tests without hitting `OSError: [Errno 25] Inappropriate ioctl for device`. 61 | - Fix Mypy error `PEP 484 prohibits implicit Optional`. 62 | 63 | ## [2.6.0] - 2023-04-14 64 | 65 | ### Added 66 | - Allow to use a custom class for the chart rendering via the `Chart(..., renderer_cls=MyClass)` keyword argument (see `examples/custom-renderer-class.py` for inspiration). 67 | - The module is now PEP 561 compatible, and tested with `mypy`. 68 | 69 | ### Changed 70 | - Fixed off-by-one shift when rendering empty lines on the Y-axis (the issue was visible only when the Y-axis was on the left side) (fixes [#7]). 71 | 72 | ## [2.5.1] - 2022-10-21 73 | 74 | ### Changed 75 | - Allow to pass a blank color to `color()`, it will return the text unchanged. 76 | - Refactored price highlights, it should now highlight price that would be hidden by a slightly upper value (like 1.025 being hidden because that exact price is not available, but it is surrounded by 1.02, and 1.03, then it will take the place of 1.02). 77 | - Better-looking Y-axis style (when on the left-side: `PRICE │―` → `PRICE ┤`, and on the right-side: `│― PRICE` → `├ PRICE`). 78 | 79 | ## [2.5.0] - 2022-10-19 80 | 81 | ### Added 82 | - Capability to round prices on the Y-axis via `Y_AXIS_ROUND_DIR` (either `down` [default], or `up`), and `Y_AXIS_ROUND_MULTIPLIER` (`0.0` by default, set something like `1 / 0.01` to round price to 2 decimals), constants. 83 | 84 | ### Changed 85 | - Improve rendering performances by ~60%. 86 | 87 | ## [2.4.0] - 2022-09-23 88 | 89 | ### Added 90 | - Capability to highlight values on the Y-axis using `chart.set_highlight()`. 91 | - New colors: blue, cyan, grey, magenta, and white. 92 | 93 | ## [2.3.0] - 2022-09-23 94 | 95 | ### Added 96 | - Capability to display graduations on the right side by setting the `constants.Y_AXIS_ON_THE_RIGHT` to `True`. 97 | 98 | ### Changed 99 | - Fixed a zero division error when no candle volume is set. 100 | - Chart title is now hidden if it is an empty string. 101 | 102 | ## [2.2.1] - 2022-08-12 103 | 104 | ### Added 105 | - New `constants.PRECISION`, and `constants.PRECISION_SMALL`, constants to control the number of decimals to keep when formatting numbers with `fnum()` (defaults to `2`, and `4`, respectively). 106 | - New `constants.MIN_DIFF_THRESHOLD`, and `constants.MAX_DIFF_THRESHOLD`, constants to control candle top, and bottom, thickness `fnum()` (defaults to `0.25`, and `0.75`, respectively). 107 | 108 | ### Changed 109 | - Fixed formatting of `1.0` number within `fnum()`. 110 | - Fixed imports using `isort`. 111 | 112 | ## [2.2.0] - 2022-08-12 113 | 114 | ### Added 115 | - `Candle.__eq__()` to allow comparing candles. 116 | - Introduced `constants.Y_AXIS_SPACING` to give control Y-axis spacing between graduations (defaults to `4`, reduce to display more graduations, and set a higher number to display less graduations). 117 | 118 | ### Changed 119 | - Constant changes are now taken into account in real-time, it allows tweaking the chart appearance after having imported the module. 120 | - Always show the volume pane when it is enabled. 121 | 122 | ## [2.1.0] - 2022-07-20 123 | 124 | ### Added 125 | - Nice `Candle` Python representation. 126 | 127 | ### Changed 128 | - Fixed a zero division error when min, and max, prices are equals inside a same candle (closes [#4]). 129 | - Fixed small numbers display on the Y-axis (closes [#5]). 130 | - Fixed bearish/bullish colors inversion in the volume pane. 131 | 132 | ## [2.0.0] - 2022-05-22 133 | 134 | ### Changed 135 | - Fixed values computation in the info bar by using the whole candle set rather than only the visible one (closes [#2]). 136 | - Changed the `Chart.update_candles()` behavior: it will update current candles by default, and now accepts a `reset=True` optional argument to actually erase all previous candles first (closes [#3]). 137 | 138 | ## [1.0.0] - 2022-05-21 139 | 140 | ### Added 141 | - First version. 142 | 143 | 144 | [Unreleased]: https://github.com/BoboTiG/py-candlestick-chart/compare/v3.1.0...HEAD 145 | [3.1.0]: https://github.com/BoboTiG/py-candlestick-chart/tree/v3.1.0 146 | [3.0.0]: https://github.com/BoboTiG/py-candlestick-chart/tree/v3.0.0 147 | [2.7.0]: https://github.com/BoboTiG/py-candlestick-chart/tree/v2.7.0 148 | [2.6.0]: https://github.com/BoboTiG/py-candlestick-chart/tree/v2.6.0 149 | [2.5.1]: https://github.com/BoboTiG/py-candlestick-chart/tree/v2.5.1 150 | [2.5.0]: https://github.com/BoboTiG/py-candlestick-chart/tree/v2.5.0 151 | [2.4.0]: https://github.com/BoboTiG/py-candlestick-chart/tree/v2.4.0 152 | [2.3.0]: https://github.com/BoboTiG/py-candlestick-chart/tree/v2.3.0 153 | [2.2.1]: https://github.com/BoboTiG/py-candlestick-chart/tree/v2.2.1 154 | [2.2.0]: https://github.com/BoboTiG/py-candlestick-chart/tree/v2.2.0 155 | [2.1.0]: https://github.com/BoboTiG/py-candlestick-chart/tree/v2.1.0 156 | [2.0.0]: https://github.com/BoboTiG/py-candlestick-chart/tree/v2.0.0 157 | [1.0.0]: https://github.com/BoboTiG/py-candlestick-chart/tree/v1.0.0 158 | 159 | [#2]: https://github.com/BoboTiG/py-candlestick-chart/issues/2 160 | [#3]: https://github.com/BoboTiG/py-candlestick-chart/issues/3 161 | [#4]: https://github.com/BoboTiG/py-candlestick-chart/issues/4 162 | [#5]: https://github.com/BoboTiG/py-candlestick-chart/issues/5 163 | [#7]: https://github.com/BoboTiG/py-candlestick-chart/issues/7 164 | [#9]: https://github.com/BoboTiG/py-candlestick-chart/issues/9 165 | [#13]: https://github.com/BoboTiG/py-candlestick-chart/issues/13 166 | [#15]: https://github.com/BoboTiG/py-candlestick-chart/pull/15 167 | 168 | [6975d7f]: https://github.com/BoboTiG/py-candlestick-chart/commit/6975d7fc1952c03af251bb12e758827cbea5b3ad 169 | -------------------------------------------------------------------------------- /examples/BTC-USD.csv: -------------------------------------------------------------------------------- 1 | timestamp,open,high,low,close,volume 2 | 1652997600,28994.009766,29600.626953,28803.585938,29374.152344,40730301359 3 | 1653084000,29376.455078,33155.117188,29091.181641,32127.267578,67865420765 4 | 1653170400,32129.408203,34608.558594,32052.316406,32782.023438,78665235202 5 | 1653256800,32810.949219,33440.218750,28722.755859,31971.914063,81163475344 6 | 1653343200,31977.041016,34437.589844,30221.187500,33992.429688,67547324782 7 | 1653429600,34013.613281,36879.699219,33514.035156,36824.363281,75289433811 8 | 1653516000,36833.875000,40180.367188,36491.191406,39371.042969,84762141031 9 | 1653602400,39381.765625,41946.738281,36838.636719,40797.609375,88107519480 10 | 1653688800,40788.640625,41436.351563,38980.875000,40254.546875,61984162837 11 | 1653775200,40254.218750,41420.191406,35984.628906,38356.441406,79980747690 12 | 1653861600,38346.531250,38346.531250,30549.599609,35566.656250,123320567399 13 | 1653948000,35516.359375,36568.527344,32697.976563,33922.960938,74773277909 14 | 1654034400,33915.121094,37599.960938,32584.667969,37316.359375,69364315979 15 | 1654120800,37325.109375,39966.406250,36868.562500,39187.328125,63615990033 16 | 1654207200,39156.707031,39577.710938,34659.589844,36825.367188,67760757881 17 | 1654293600,36821.648438,37864.367188,35633.554688,36178.140625,57706187875 18 | 1654380000,36163.648438,36722.351563,34069.320313,35791.277344,52359854336 19 | 1654466400,35792.238281,37299.285156,34883.843750,36630.074219,49511702429 20 | 1654552800,36642.234375,37755.890625,36069.804688,36069.804688,57244195486 21 | 1654639200,36050.113281,36378.328125,33570.476563,35547.750000,66834573161 22 | 1654725600,35549.398438,35552.679688,30250.750000,30825.699219,75643067688 23 | 1654812000,30817.625000,33811.851563,28953.373047,33005.761719,77207272511 24 | 1654898400,32985.757813,33360.976563,31493.160156,32067.642578,48354737975 25 | 1654984800,32064.376953,32944.007813,31106.685547,32289.378906,48643830599 26 | 1655071200,32285.798828,34802.742188,32087.787109,32366.392578,59897054838 27 | 1655157600,32358.613281,32794.550781,31030.265625,32569.849609,60255421470 28 | 1655244000,32564.029297,32564.029297,29367.138672,30432.546875,62576762015 29 | 1655330400,30441.041016,31891.300781,30023.207031,31649.605469,78948162368 30 | 1655416800,34318.671875,38406.261719,32064.814453,34316.386719,117894572511 31 | 1655503200,34295.933594,34834.707031,32940.187500,34269.523438,65141828798 32 | 1655589600,34270.878906,34288.332031,32270.175781,33114.359375,52754542671 33 | 1655676000,33114.578125,34638.214844,32384.228516,33537.175781,61400400660 34 | 1655762400,33533.199219,35896.882813,33489.218750,35510.289063,63088585433 35 | 1655848800,35510.820313,37480.187500,35443.984375,37472.089844,61166818159 36 | 1655935200,37475.105469,38592.175781,36317.500000,36926.066406,68838074392 37 | 1656021600,36931.546875,38225.906250,36658.761719,38144.308594,58598066402 38 | 1656108000,38138.386719,40846.546875,38138.386719,39266.011719,71326033653 39 | 1656194400,39250.191406,39621.835938,37446.152344,38903.441406,65500641143 40 | 1656280800,38886.828125,46203.929688,38076.324219,46196.464844,101467222687 41 | 1656367200,46184.992188,48003.722656,45166.960938,46481.105469,91809846886 42 | 1656453600,46469.761719,47145.566406,43881.152344,44918.183594,87301089896 43 | 1656540000,44898.710938,48463.468750,44187.761719,47909.332031,81388911810 44 | 1656626400,47877.035156,48745.734375,46424.976563,47504.851563,76555041196 45 | 1656712800,47491.203125,48047.746094,46392.281250,47105.515625,70250456155 46 | 1656799200,47114.507813,49487.640625,47114.507813,48717.289063,71248675228 47 | 1656885600,48696.535156,48875.570313,46347.476563,47945.058594,77069903166 48 | 1656972000,47944.457031,50341.101563,47201.304688,49199.871094,77049582886 49 | 1657058400,49207.277344,52533.914063,49072.378906,52149.007813,80820545404 50 | 1657144800,52140.972656,52474.105469,51015.765625,51679.796875,52054723579 51 | 1657231200,51675.980469,56113.652344,50937.277344,55888.132813,63495496918 52 | 1657317600,55887.335938,57505.226563,54626.558594,56099.519531,68145460026 53 | 1657404000,56068.566406,58330.570313,55672.609375,57539.945313,51897585191 54 | 1657490400,57532.738281,57533.390625,48967.566406,54207.320313,92052420332 55 | 1657576800,54204.929688,54204.929688,45290.589844,48824.425781,106102492824 56 | 1657663200,48835.085938,51290.136719,47213.500000,49705.332031,63695521388 57 | 1657749600,49709.082031,51948.968750,47093.851563,47093.851563,54506565949 58 | 1657836000,47180.464844,48370.785156,44454.843750,46339.761719,350967941479 59 | 1657922400,46344.773438,48253.269531,45269.027344,46188.453125,45910946382 60 | 1658008800,46194.015625,46716.429688,43241.617188,45137.769531,53443887451 61 | 1658095200,45159.503906,49784.015625,45115.093750,49631.242188,53891300112 62 | 1658181600,49612.105469,50127.511719,47228.843750,48378.988281,47530897720 63 | 1658268000,48415.816406,52535.136719,48274.320313,50538.242188,53220811975 64 | 1658354400,50522.304688,51735.089844,47656.929688,48561.167969,52343816680 65 | 1658440800,48527.031250,49396.429688,46542.515625,48927.304688,48625928883 66 | 1658527200,48899.230469,49147.218750,47257.527344,48912.382813,34363564661 67 | 1658613600,48918.679688,51384.367188,48918.679688,51206.691406,43137459378 68 | 1658700000,51174.117188,52314.070313,49506.054688,52246.523438,48597428048 69 | 1658786400,52272.968750,54824.117188,51981.832031,54824.117188,50912227385 70 | 1658872800,54824.011719,57258.253906,53290.890625,56008.550781,57295577614 71 | 1658959200,55963.179688,58091.062500,54484.593750,57805.121094,56772343595 72 | 1659045600,57821.218750,57996.621094,55376.648438,57332.089844,55689944702 73 | 1659132000,57343.371094,61683.863281,56217.972656,61243.085938,60669829814 74 | 1659218400,61221.132813,61597.917969,59302.316406,59302.316406,43901225564 75 | 1659304800,59267.429688,60540.992188,55393.164063,55907.199219,66419369890 76 | 1659391200,55840.785156,56833.179688,53555.027344,56804.902344,59749798599 77 | 1659477600,56825.828125,58969.816406,54528.628906,58870.894531,60258313191 78 | 1659564000,58893.078125,60116.250000,54253.578125,57858.921875,55746041000 79 | 1659650400,57850.441406,59498.375000,56643.703125,58346.652344,49063873786 80 | 1659736800,58332.261719,60031.285156,58213.296875,58313.644531,50361731222 81 | 1659823200,58309.914063,58767.898438,56005.617188,57523.421875,51943414539 82 | 1659909600,57517.890625,58471.480469,54288.156250,54529.144531,56521454974 83 | 1659996000,54511.660156,55985.441406,53470.695313,54738.945313,56435023914 84 | 1660082400,54710.488281,57262.382813,52514.332031,52774.265625,70567223787 85 | 1660168800,52726.746094,53392.386719,50856.570313,51704.160156,67999812841 86 | 1660255200,51683.011719,55137.312500,51579.855469,55137.312500,56652197978 87 | 1660341600,55137.566406,56568.214844,54242.910156,55973.511719,47266542233 88 | 1660428000,55974.941406,56610.312500,55071.113281,55950.746094,47686580918 89 | 1660514400,55947.898438,58342.097656,55139.339844,57750.199219,57625587027 90 | 1660600800,57750.132813,59447.222656,57251.550781,58917.691406,54414116432 91 | 1660687200,58930.277344,59930.027344,57726.417969,58918.832031,65520826225 92 | 1660773600,58926.562500,59586.070313,58505.277344,59095.808594,61669163792 93 | 1660860000,59098.878906,60267.187500,58869.281250,59384.312500,58727860620 94 | 1660946400,59397.410156,60110.269531,57603.890625,57603.890625,59641344484 95 | 1661032800,57604.839844,58913.746094,57168.675781,58758.554688,50749662970 96 | 1661119200,58760.875000,59891.296875,57694.824219,59057.878906,60706272115 97 | 1661205600,59171.933594,59479.578125,57646.808594,58192.359375,66058027988 98 | 1661292000,58186.507813,58731.144531,55604.023438,56048.937500,75645303584 99 | 1661378400,56099.914063,58338.738281,55879.085938,58323.953125,53053855641 100 | 1661464800,58326.562500,58937.046875,57807.863281,58245.003906,46655208546 101 | 1661551200,58253.777344,61276.664063,58038.707031,59793.234375,58238470525 102 | 1661637600,59846.230469,60790.554688,59289.796875,60204.964844,46280252580 103 | 1661724000,60175.945313,61253.035156,59589.875000,59893.453125,51828688519 104 | 1661810400,59890.019531,63742.285156,59869.957031,63503.457031,69983454362 105 | 1661896800,63523.753906,64863.097656,61554.796875,63109.695313,77451779687 106 | 1661983200,63075.195313,63821.671875,62208.964844,63314.011719,60954381579 107 | 1662069600,63258.503906,63594.722656,60222.531250,61572.789063,84293007468 108 | 1662156000,61529.921875,62572.175781,60361.351563,60683.820313,66138759198 109 | 1662242400,60701.886719,61057.457031,52829.535156,56216.183594,97468872758 110 | 1662328800,56191.585938,57520.054688,54368.593750,55724.265625,65344865159 111 | 1662415200,55681.792969,57062.148438,53448.046875,56473.031250,67849323955 112 | 1662501600,56471.128906,56757.972656,53695.468750,53906.089844,54926612466 113 | 1662588000,53857.105469,55410.230469,50583.812500,51762.273438,74798630778 114 | 1662674400,51739.808594,52120.792969,47714.664063,51093.652344,86668667320 115 | 1662760800,51143.226563,51167.562500,48805.285156,50050.867188,49014494781 116 | 1662847200,50052.832031,50506.019531,47159.484375,49004.253906,46117114240 117 | 1662933600,49077.792969,54288.003906,48852.796875,54021.753906,58284039825 118 | 1663020000,54030.304688,55416.964844,53319.187500,55033.117188,49448222757 119 | 1663106400,55036.636719,56227.207031,53887.917969,54824.703125,48000572955 120 | 1663192800,54858.089844,55115.843750,52418.027344,53555.109375,46088929780 121 | 1663279200,53568.664063,57900.718750,53129.601563,57750.175781,52395931985 122 | 1663365600,57714.664063,58448.339844,57052.273438,57828.050781,42836427360 123 | 1663452000,57825.863281,57902.593750,56141.906250,56631.078125,38177405335 124 | 1663538400,56620.273438,58973.308594,56590.871094,57200.292969,51713139031 125 | 1663624800,57214.179688,57214.179688,53191.425781,53333.539063,68564706967 126 | 1663711200,53252.164063,57911.363281,52969.054688,57424.007813,69241316747 127 | 1663797600,57441.308594,58363.316406,55382.507813,56396.515625,69523285106 128 | 1663884000,56413.953125,58606.632813,55321.847656,57356.402344,68434023376 129 | 1663970400,57352.765625,59464.613281,56975.210938,58803.777344,65382980634 130 | 1664056800,58877.390625,59210.882813,56482.003906,58232.316406,65906690347 131 | 1664143200,58250.871094,59519.355469,54071.457031,55859.796875,71776546298 132 | 1664229600,55847.242188,56872.542969,54608.652344,56704.574219,61308396325 133 | 1664316000,56714.531250,57939.363281,49150.535156,49150.535156,75215403907 134 | 1664402400,49735.433594,51330.843750,46980.019531,49716.191406,96721152926 135 | 1664488800,49682.980469,51438.117188,48868.578125,49880.535156,55737497453 136 | 1664575200,49855.496094,50639.664063,46664.140625,46760.187500,59161047474 137 | 1664661600,46716.636719,49720.042969,43963.351563,46456.058594,64047871555 138 | 1664748000,46415.898438,46623.558594,42207.289063,43537.511719,74903638450 139 | 1664834400,43488.058594,45812.457031,42367.832031,42909.402344,56187365084 140 | 1664920800,42944.976563,43546.117188,30681.496094,37002.441406,126358098747 141 | 1665007200,36753.667969,42462.984375,35050.617188,40782.738281,88281943359 142 | 1665093600,40596.949219,42172.171875,33616.453125,37304.691406,82051616861 143 | 1665180000,37371.031250,38831.054688,35383.683594,37536.632813,57377273240 144 | 1665266400,37531.449219,38289.218750,31227.339844,34770.582031,78469274361 145 | 1665352800,34700.363281,39835.140625,34551.082031,38705.980469,67359584098 146 | 1665439200,38795.781250,39776.351563,36581.429688,38402.222656,56211915803 147 | 1665525600,38392.625000,40782.078125,37905.835938,39294.199219,51346735160 148 | 1665612000,39316.890625,40379.617188,37247.902344,38436.968750,43210968721 149 | 1665698400,38507.082031,38856.968750,34779.039063,35697.605469,55200191952 150 | 1665784800,35684.156250,37234.500000,33693.929688,34616.066406,45231013335 151 | 1665871200,34607.406250,36400.667969,33520.738281,35678.128906,31646080921 152 | 1665957600,35658.593750,37468.250000,34241.945313,37332.855469,39009847639 153 | 1666044000,37293.792969,37896.734375,35787.085938,36684.925781,34639423297 154 | 1666130400,36699.921875,38231.339844,35966.308594,37575.179688,33070867190 155 | 1666216800,37599.410156,39478.953125,37243.972656,39208.765625,35460750427 156 | 1666303200,39242.484375,39242.484375,35717.722656,36894.406250,41831090187 157 | 1666389600,36880.156250,37917.714844,34900.414063,35551.957031,35959473399 158 | 1666476000,35538.609375,36436.421875,35304.578125,35862.378906,28913440585 159 | 1666562400,35835.265625,36790.570313,33480.640625,33560.707031,33683936663 160 | 1666648800,33589.519531,34017.386719,31114.443359,33472.632813,49902050442 161 | 1666735200,33416.976563,37537.371094,32475.865234,37345.121094,53972919008 162 | 1666821600,37389.515625,38334.324219,35847.593750,36702.597656,43576032854 163 | 1666908000,36697.031250,37608.695313,36044.449219,37334.398438,38699736985 164 | 1666994400,37340.144531,37408.925781,34728.191406,35552.515625,37924228550 165 | 1667080800,35555.789063,39322.781250,34864.109375,39097.859375,40669112838 166 | 1667167200,39016.968750,40978.363281,38757.285156,40218.476563,43148914673 167 | 1667253600,40427.167969,41295.269531,39609.468750,40406.269531,46420149185 168 | 1667340000,40168.691406,40516.777344,38176.035156,38347.062500,39211635100 169 | 1667426400,38341.421875,39513.671875,37439.675781,38053.503906,37096670047 170 | 1667512800,38099.476563,38187.261719,35255.855469,35787.246094,36200887275 171 | 1667599200,35854.527344,36457.796875,34933.062500,35615.871094,31207279719 172 | 1667685600,35563.140625,36059.484375,33432.074219,35698.296875,36664034054 173 | 1667772000,35641.144531,35721.640625,31295.935547,31676.693359,52809038594 174 | 1667858400,31622.376953,33292.453125,28893.621094,32505.660156,58964353058 175 | 1667944800,32515.714844,34753.410156,31772.632813,33723.027344,46317108925 176 | 1668031200,33682.800781,35228.851563,32385.214844,34662.437500,33123368116 177 | 1668117600,34659.105469,35487.246094,31350.884766,31637.779297,40230904226 178 | 1668204000,31594.664063,32637.587891,30184.501953,32186.277344,38585385521 179 | 1668290400,32287.523438,34656.128906,32071.757813,34649.644531,35511640894 180 | 1668376800,34679.121094,35219.890625,33902.074219,34434.335938,33892523752 181 | 1668463200,34475.558594,36542.109375,34252.484375,35867.777344,37901460044 182 | 1668549600,35908.386719,36074.757813,34086.152344,35040.835938,34059036099 183 | 1668636000,35035.984375,35035.984375,32883.781250,33572.117188,37838957079 184 | 1668722400,33549.601563,33939.589844,32770.679688,33897.046875,38728974942 185 | 1668808800,33854.421875,34909.261719,33402.695313,34668.546875,24383958643 186 | 1668895200,34665.566406,35937.566406,34396.476563,35287.781250,24924307911 187 | 1668981600,35284.343750,35284.343750,33213.660156,33746.003906,26721554282 188 | 1669068000,33723.507813,35038.535156,33599.917969,34235.195313,26501259870 189 | 1669154400,34225.679688,34997.664063,33839.289063,33855.328125,24796027477 190 | 1669240800,33889.605469,33907.906250,32133.183594,32877.371094,29910396946 191 | 1669327200,32861.671875,34042.292969,32318.880859,33798.011719,27436021028 192 | 1669413600,33811.242188,34209.070313,33116.011719,33520.519531,22971873468 193 | 1669500000,33509.078125,34584.703125,33346.738281,34240.187500,20108729370 194 | 1669586400,34254.015625,34592.156250,32697.308594,33155.847656,24321499537 195 | 1669672800,33125.468750,33327.101563,32261.419922,32702.025391,19120856669 196 | 1669759200,32723.845703,33061.398438,31639.125000,32822.347656,21376531210 197 | 1669845600,32827.875000,33159.640625,31175.708984,31780.730469,21300524237 198 | 1669932000,31841.550781,32218.406250,31100.673828,31421.539063,23699476918 199 | 1670018400,31397.308594,31935.945313,31223.990234,31533.068359,18895018942 200 | 1670104800,31533.884766,32398.996094,31215.492188,31796.810547,18787986667 201 | 1670191200,31800.011719,31885.859375,30563.734375,30817.832031,20434789545 202 | 1670277600,30838.285156,31006.187500,29360.955078,29807.347656,23148267245 203 | 1670364000,29796.285156,32752.326172,29526.183594,32110.693359,28203024559 204 | 1670450400,32138.873047,32576.400391,31745.298828,32313.105469,19555230518 205 | 1670536800,32305.958984,33581.550781,32057.892578,33581.550781,22552046192 206 | 1670623200,33593.730469,34490.390625,33424.859375,34292.445313,21664706865 207 | 1670709600,34290.292969,35364.925781,33881.835938,35350.187500,20856685287 208 | 1670796000,35384.031250,40499.675781,35287.312500,37337.535156,51022126212 209 | 1670882400,37276.035156,39406.941406,36441.726563,39406.941406,35097370560 210 | 1670968800,39503.187500,40816.070313,38862.437500,39995.906250,38702404695 211 | 1671055200,39995.453125,40593.070313,39352.058594,40008.421875,27167146027 212 | 1671141600,40027.484375,42235.546875,38397.355469,42235.546875,33072782960 213 | 1671228000,42196.304688,42231.449219,41110.832031,41626.195313,25802845343 214 | 1671314400,41460.843750,42541.679688,39540.941406,39974.894531,26688438115 215 | 1671400800,39907.261719,40419.179688,38746.347656,39201.945313,25595265436 216 | 1671487200,39178.402344,39750.031250,37782.050781,38152.980469,26189830450 217 | 1671573600,38213.332031,39952.296875,37589.164063,39747.503906,25372562724 218 | 1671660000,39744.515625,41341.933594,37458.003906,40869.554688,35185031017 219 | 1671746400,40865.867188,43271.660156,39932.179688,42816.500000,38226483046 220 | 1671832800,42832.796875,44689.859375,42618.566406,44555.800781,40030862141 221 | 1671919200,44574.437500,45282.351563,43331.910156,43798.117188,36302664750 222 | 1672005600,43791.925781,46456.832031,42848.687500,46365.402344,38734079049 223 | 1672092000,46280.847656,46637.988281,44705.554688,45585.031250,33546019517 224 | 1672178400,45599.703125,46735.632813,45351.710938,45593.636719,34319709073 225 | 1672264800,45576.878906,46228.910156,43861.445313,44428.289063,33723620826 226 | 1672351200,44439.691406,47831.976563,44282.417969,47793.320313,31744259539 227 | 1672437600,47810.687500,48098.683594,46177.632813,47096.945313,31211354442 228 | 1672524000,47096.667969,47357.105469,45579.589844,47047.003906,30988958446 229 | 1672610400,47019.960938,47998.097656,45700.320313,46004.484375,32776876610 230 | 1672696800,45936.457031,47139.570313,44512.417969,44695.359375,33451362600 231 | 1672783200,44686.750000,45952.062500,44364.027344,44801.187500,32194123075 232 | 1672869600,44741.882813,46970.761719,43998.316406,46717.578125,37204312299 233 | 1672956000,46723.121094,49342.152344,46650.707031,49339.175781,34706867452 234 | 1673042400,49327.074219,49717.019531,48312.199219,48905.492188,40585205312 235 | 1673128800,48869.105469,49471.609375,48199.941406,49321.652344,25370975378 236 | 1673215200,49291.675781,50482.078125,49074.605469,49546.148438,34305053719 237 | 1673301600,49562.347656,49878.769531,47687.117188,47706.117188,35361168834 238 | 1673388000,47727.257813,49202.878906,47163.613281,48960.789063,32646349931 239 | 1673474400,49002.640625,49347.582031,46405.781250,46942.218750,32666549568 240 | 1673560800,46894.554688,49112.785156,46394.281250,49058.667969,34511076995 241 | 1673647200,49072.585938,49283.503906,48499.238281,48902.402344,28568103401 242 | 1673733600,48911.250000,49644.113281,47925.855469,48829.832031,25889650240 243 | 1673820000,48834.851563,48925.605469,46950.273438,47054.984375,31847007016 244 | 1673906400,47024.339844,48189.550781,46750.093750,47166.687500,34730363427 245 | 1673992800,47099.773438,49111.089844,46562.437500,48847.027344,39139399125 246 | 1674079200,48807.847656,50343.421875,48652.320313,49327.722656,39508070319 247 | 1674165600,49288.250000,50982.273438,48386.085938,50025.375000,43206179619 248 | 1674252000,50009.324219,50545.582031,49548.781250,49944.625000,37471327794 249 | 1674338400,49937.859375,51868.679688,49538.597656,51753.410156,30322676319 250 | 1674424800,51769.003906,52700.941406,51053.679688,52633.535156,38884105426 251 | 1674511200,52660.480469,52853.765625,43285.207031,46811.128906,65210059683 252 | 1674597600,46827.761719,47334.054688,44561.394531,46091.390625,49007762488 253 | 1674684000,45774.742188,47261.949219,45669.738281,46391.421875,38672657013 254 | 1674770400,46396.664063,47031.742188,44344.484375,44883.910156,39154666597 255 | 1674856800,44869.839844,45969.292969,44818.265625,45201.457031,34499835245 256 | 1674943200,45206.628906,46364.878906,44790.460938,46063.269531,27881980161 257 | 1675029600,46057.214844,46598.679688,43591.320313,44963.074219,40969943253 258 | 1675116000,44960.050781,47218.125000,44752.332031,47092.492188,38652152880 259 | 1675202400,47098.000000,48450.468750,46773.328125,48176.347656,30484496466 260 | 1675288800,48158.906250,48486.828125,47079.558594,47783.359375,31764293754 261 | 1675375200,47771.003906,48160.921875,46832.523438,47267.519531,28727713711 262 | 1675461600,47273.527344,48791.781250,47087.285156,48278.363281,28575630451 263 | 1675548000,48268.855469,48328.367188,46919.804688,47260.218750,26967722648 264 | 1675634400,47261.406250,47328.199219,42598.914063,42843.800781,43909845642 265 | 1675720800,43012.234375,43607.609375,39787.609375,40693.675781,48701090088 266 | 1675807200,40677.953125,43978.621094,40625.632813,43574.507813,38139709246 267 | 1675893600,43560.296875,44942.175781,43109.339844,44895.097656,34244064430 268 | 1675980000,44894.300781,45080.492188,40936.558594,42839.750000,42839345714 269 | 1676066400,42840.890625,42996.257813,41759.921875,42716.593750,31604717236 270 | 1676152800,42721.628906,43919.300781,40848.460938,43208.539063,30661222077 271 | 1676239200,43234.183594,44313.246094,42190.632813,42235.730469,30980029059 272 | 1676325600,42200.898438,42775.144531,40931.664063,41034.542969,30214940550 273 | 1676412000,41064.984375,42545.257813,40829.667969,41564.363281,30602359905 274 | 1676498400,41551.269531,44092.601563,41444.582031,43790.894531,31141681925 275 | 1676584800,43816.742188,48436.011719,43320.023438,48116.941406,42850641582 276 | 1676671200,48137.468750,48282.062500,47465.496094,47711.488281,30614346492 277 | 1676757600,47680.027344,49130.691406,47157.289063,48199.953125,26638115879 278 | 1676844000,48208.906250,49456.777344,47045.003906,49112.902344,33383173002 279 | 1676930400,49174.960938,51839.984375,49072.839844,51514.812500,35873904236 280 | 1677016800,51486.664063,55568.464844,50488.191406,55361.449219,49034730168 281 | 1677103200,55338.625000,55338.625000,53525.468750,53805.984375,36807860413 282 | 1677189600,53802.144531,55922.980469,53688.054688,53967.847656,34800873924 283 | 1677276000,53929.781250,55397.945313,53735.144531,54968.222656,32491211414 284 | 1677362400,54952.820313,56401.304688,54264.257813,54771.578125,39527792364 285 | 1677448800,54734.125000,57793.039063,54519.765625,57484.789063,42637331698 286 | 1677535200,57526.832031,57627.878906,54477.972656,56041.058594,41083758949 287 | 1677621600,56038.257813,57688.660156,54370.972656,57401.097656,41684252783 288 | 1677708000,57372.832031,58478.734375,56957.074219,57321.523438,36615791366 289 | 1677794400,57345.902344,62757.128906,56868.144531,61593.949219,51780081801 290 | 1677880800,61609.527344,62274.476563,60206.121094,60892.179688,34250964237 291 | 1677967200,60887.652344,61645.523438,59164.468750,61553.617188,29032367511 292 | 1678053600,61548.804688,62614.660156,60012.757813,62026.078125,38055562075 293 | 1678140000,62043.164063,64434.535156,61622.933594,64261.992188,40471196346 294 | 1678226400,64284.585938,66930.390625,63610.675781,65992.835938,40788955582 295 | 1678312800,66002.234375,66600.546875,62117.410156,62210.171875,45908121370 296 | 1678399200,62237.890625,63715.023438,60122.796875,60692.265625,38434082775 297 | 1678485600,60694.628906,61743.878906,59826.523438,61393.617188,26882546034 298 | 1678572000,61368.343750,61505.804688,59643.343750,60930.835938,27316183882 299 | 1678658400,60893.925781,63729.324219,60691.800781,63039.824219,31064911614 300 | 1678744800,63032.761719,63229.027344,59991.160156,60363.792969,34878965587 301 | 1678831200,60352.000000,61435.183594,58208.187500,58482.386719,43657076893 302 | 1678917600,58470.730469,62128.632813,58206.917969,60622.136719,45257083247 303 | 1679004000,60624.871094,62927.609375,60329.964844,62227.964844,36856881767 304 | 1679090400,62239.363281,62330.144531,60918.386719,61888.832031,32157938616 305 | 1679176800,61850.488281,62406.171875,60074.328125,61318.957031,32241199927 306 | 1679263200,61320.449219,62419.003906,59695.183594,61004.406250,36150572843 307 | 1679349600,60963.253906,64242.792969,60673.054688,63226.402344,37746665647 308 | 1679436000,63254.335938,63516.937500,61184.238281,62970.046875,36124731509 309 | 1679522400,62941.804688,63123.289063,60799.664063,61452.230469,32615846901 310 | 1679608800,61460.078125,62541.468750,60844.609375,61125.675781,30605102446 311 | 1679695200,61068.875000,61590.683594,60163.781250,61527.480469,29094934221 312 | 1679781600,61554.921875,63326.988281,61432.488281,63326.988281,24726754302 313 | 1679868000,63344.066406,67673.742188,63344.066406,67566.828125,41125608330 314 | -------------------------------------------------------------------------------- /src/tests/data/BTC-USD.csv: -------------------------------------------------------------------------------- 1 | timestamp,open,high,low,close,volume 2 | 1652997600,28994.009766,29600.626953,28803.585938,29374.152344,40730301359 3 | 1653084000,29376.455078,33155.117188,29091.181641,32127.267578,67865420765 4 | 1653170400,32129.408203,34608.558594,32052.316406,32782.023438,78665235202 5 | 1653256800,32810.949219,33440.218750,28722.755859,31971.914063,81163475344 6 | 1653343200,31977.041016,34437.589844,30221.187500,33992.429688,67547324782 7 | 1653429600,34013.613281,36879.699219,33514.035156,36824.363281,75289433811 8 | 1653516000,36833.875000,40180.367188,36491.191406,39371.042969,84762141031 9 | 1653602400,39381.765625,41946.738281,36838.636719,40797.609375,88107519480 10 | 1653688800,40788.640625,41436.351563,38980.875000,40254.546875,61984162837 11 | 1653775200,40254.218750,41420.191406,35984.628906,38356.441406,79980747690 12 | 1653861600,38346.531250,38346.531250,30549.599609,35566.656250,123320567399 13 | 1653948000,35516.359375,36568.527344,32697.976563,33922.960938,74773277909 14 | 1654034400,33915.121094,37599.960938,32584.667969,37316.359375,69364315979 15 | 1654120800,37325.109375,39966.406250,36868.562500,39187.328125,63615990033 16 | 1654207200,39156.707031,39577.710938,34659.589844,36825.367188,67760757881 17 | 1654293600,36821.648438,37864.367188,35633.554688,36178.140625,57706187875 18 | 1654380000,36163.648438,36722.351563,34069.320313,35791.277344,52359854336 19 | 1654466400,35792.238281,37299.285156,34883.843750,36630.074219,49511702429 20 | 1654552800,36642.234375,37755.890625,36069.804688,36069.804688,57244195486 21 | 1654639200,36050.113281,36378.328125,33570.476563,35547.750000,66834573161 22 | 1654725600,35549.398438,35552.679688,30250.750000,30825.699219,75643067688 23 | 1654812000,30817.625000,33811.851563,28953.373047,33005.761719,77207272511 24 | 1654898400,32985.757813,33360.976563,31493.160156,32067.642578,48354737975 25 | 1654984800,32064.376953,32944.007813,31106.685547,32289.378906,48643830599 26 | 1655071200,32285.798828,34802.742188,32087.787109,32366.392578,59897054838 27 | 1655157600,32358.613281,32794.550781,31030.265625,32569.849609,60255421470 28 | 1655244000,32564.029297,32564.029297,29367.138672,30432.546875,62576762015 29 | 1655330400,30441.041016,31891.300781,30023.207031,31649.605469,78948162368 30 | 1655416800,34318.671875,38406.261719,32064.814453,34316.386719,117894572511 31 | 1655503200,34295.933594,34834.707031,32940.187500,34269.523438,65141828798 32 | 1655589600,34270.878906,34288.332031,32270.175781,33114.359375,52754542671 33 | 1655676000,33114.578125,34638.214844,32384.228516,33537.175781,61400400660 34 | 1655762400,33533.199219,35896.882813,33489.218750,35510.289063,63088585433 35 | 1655848800,35510.820313,37480.187500,35443.984375,37472.089844,61166818159 36 | 1655935200,37475.105469,38592.175781,36317.500000,36926.066406,68838074392 37 | 1656021600,36931.546875,38225.906250,36658.761719,38144.308594,58598066402 38 | 1656108000,38138.386719,40846.546875,38138.386719,39266.011719,71326033653 39 | 1656194400,39250.191406,39621.835938,37446.152344,38903.441406,65500641143 40 | 1656280800,38886.828125,46203.929688,38076.324219,46196.464844,101467222687 41 | 1656367200,46184.992188,48003.722656,45166.960938,46481.105469,91809846886 42 | 1656453600,46469.761719,47145.566406,43881.152344,44918.183594,87301089896 43 | 1656540000,44898.710938,48463.468750,44187.761719,47909.332031,81388911810 44 | 1656626400,47877.035156,48745.734375,46424.976563,47504.851563,76555041196 45 | 1656712800,47491.203125,48047.746094,46392.281250,47105.515625,70250456155 46 | 1656799200,47114.507813,49487.640625,47114.507813,48717.289063,71248675228 47 | 1656885600,48696.535156,48875.570313,46347.476563,47945.058594,77069903166 48 | 1656972000,47944.457031,50341.101563,47201.304688,49199.871094,77049582886 49 | 1657058400,49207.277344,52533.914063,49072.378906,52149.007813,80820545404 50 | 1657144800,52140.972656,52474.105469,51015.765625,51679.796875,52054723579 51 | 1657231200,51675.980469,56113.652344,50937.277344,55888.132813,63495496918 52 | 1657317600,55887.335938,57505.226563,54626.558594,56099.519531,68145460026 53 | 1657404000,56068.566406,58330.570313,55672.609375,57539.945313,51897585191 54 | 1657490400,57532.738281,57533.390625,48967.566406,54207.320313,92052420332 55 | 1657576800,54204.929688,54204.929688,45290.589844,48824.425781,106102492824 56 | 1657663200,48835.085938,51290.136719,47213.500000,49705.332031,63695521388 57 | 1657749600,49709.082031,51948.968750,47093.851563,47093.851563,54506565949 58 | 1657836000,47180.464844,48370.785156,44454.843750,46339.761719,350967941479 59 | 1657922400,46344.773438,48253.269531,45269.027344,46188.453125,45910946382 60 | 1658008800,46194.015625,46716.429688,43241.617188,45137.769531,53443887451 61 | 1658095200,45159.503906,49784.015625,45115.093750,49631.242188,53891300112 62 | 1658181600,49612.105469,50127.511719,47228.843750,48378.988281,47530897720 63 | 1658268000,48415.816406,52535.136719,48274.320313,50538.242188,53220811975 64 | 1658354400,50522.304688,51735.089844,47656.929688,48561.167969,52343816680 65 | 1658440800,48527.031250,49396.429688,46542.515625,48927.304688,48625928883 66 | 1658527200,48899.230469,49147.218750,47257.527344,48912.382813,34363564661 67 | 1658613600,48918.679688,51384.367188,48918.679688,51206.691406,43137459378 68 | 1658700000,51174.117188,52314.070313,49506.054688,52246.523438,48597428048 69 | 1658786400,52272.968750,54824.117188,51981.832031,54824.117188,50912227385 70 | 1658872800,54824.011719,57258.253906,53290.890625,56008.550781,57295577614 71 | 1658959200,55963.179688,58091.062500,54484.593750,57805.121094,56772343595 72 | 1659045600,57821.218750,57996.621094,55376.648438,57332.089844,55689944702 73 | 1659132000,57343.371094,61683.863281,56217.972656,61243.085938,60669829814 74 | 1659218400,61221.132813,61597.917969,59302.316406,59302.316406,43901225564 75 | 1659304800,59267.429688,60540.992188,55393.164063,55907.199219,66419369890 76 | 1659391200,55840.785156,56833.179688,53555.027344,56804.902344,59749798599 77 | 1659477600,56825.828125,58969.816406,54528.628906,58870.894531,60258313191 78 | 1659564000,58893.078125,60116.250000,54253.578125,57858.921875,55746041000 79 | 1659650400,57850.441406,59498.375000,56643.703125,58346.652344,49063873786 80 | 1659736800,58332.261719,60031.285156,58213.296875,58313.644531,50361731222 81 | 1659823200,58309.914063,58767.898438,56005.617188,57523.421875,51943414539 82 | 1659909600,57517.890625,58471.480469,54288.156250,54529.144531,56521454974 83 | 1659996000,54511.660156,55985.441406,53470.695313,54738.945313,56435023914 84 | 1660082400,54710.488281,57262.382813,52514.332031,52774.265625,70567223787 85 | 1660168800,52726.746094,53392.386719,50856.570313,51704.160156,67999812841 86 | 1660255200,51683.011719,55137.312500,51579.855469,55137.312500,56652197978 87 | 1660341600,55137.566406,56568.214844,54242.910156,55973.511719,47266542233 88 | 1660428000,55974.941406,56610.312500,55071.113281,55950.746094,47686580918 89 | 1660514400,55947.898438,58342.097656,55139.339844,57750.199219,57625587027 90 | 1660600800,57750.132813,59447.222656,57251.550781,58917.691406,54414116432 91 | 1660687200,58930.277344,59930.027344,57726.417969,58918.832031,65520826225 92 | 1660773600,58926.562500,59586.070313,58505.277344,59095.808594,61669163792 93 | 1660860000,59098.878906,60267.187500,58869.281250,59384.312500,58727860620 94 | 1660946400,59397.410156,60110.269531,57603.890625,57603.890625,59641344484 95 | 1661032800,57604.839844,58913.746094,57168.675781,58758.554688,50749662970 96 | 1661119200,58760.875000,59891.296875,57694.824219,59057.878906,60706272115 97 | 1661205600,59171.933594,59479.578125,57646.808594,58192.359375,66058027988 98 | 1661292000,58186.507813,58731.144531,55604.023438,56048.937500,75645303584 99 | 1661378400,56099.914063,58338.738281,55879.085938,58323.953125,53053855641 100 | 1661464800,58326.562500,58937.046875,57807.863281,58245.003906,46655208546 101 | 1661551200,58253.777344,61276.664063,58038.707031,59793.234375,58238470525 102 | 1661637600,59846.230469,60790.554688,59289.796875,60204.964844,46280252580 103 | 1661724000,60175.945313,61253.035156,59589.875000,59893.453125,51828688519 104 | 1661810400,59890.019531,63742.285156,59869.957031,63503.457031,69983454362 105 | 1661896800,63523.753906,64863.097656,61554.796875,63109.695313,77451779687 106 | 1661983200,63075.195313,63821.671875,62208.964844,63314.011719,60954381579 107 | 1662069600,63258.503906,63594.722656,60222.531250,61572.789063,84293007468 108 | 1662156000,61529.921875,62572.175781,60361.351563,60683.820313,66138759198 109 | 1662242400,60701.886719,61057.457031,52829.535156,56216.183594,97468872758 110 | 1662328800,56191.585938,57520.054688,54368.593750,55724.265625,65344865159 111 | 1662415200,55681.792969,57062.148438,53448.046875,56473.031250,67849323955 112 | 1662501600,56471.128906,56757.972656,53695.468750,53906.089844,54926612466 113 | 1662588000,53857.105469,55410.230469,50583.812500,51762.273438,74798630778 114 | 1662674400,51739.808594,52120.792969,47714.664063,51093.652344,86668667320 115 | 1662760800,51143.226563,51167.562500,48805.285156,50050.867188,49014494781 116 | 1662847200,50052.832031,50506.019531,47159.484375,49004.253906,46117114240 117 | 1662933600,49077.792969,54288.003906,48852.796875,54021.753906,58284039825 118 | 1663020000,54030.304688,55416.964844,53319.187500,55033.117188,49448222757 119 | 1663106400,55036.636719,56227.207031,53887.917969,54824.703125,48000572955 120 | 1663192800,54858.089844,55115.843750,52418.027344,53555.109375,46088929780 121 | 1663279200,53568.664063,57900.718750,53129.601563,57750.175781,52395931985 122 | 1663365600,57714.664063,58448.339844,57052.273438,57828.050781,42836427360 123 | 1663452000,57825.863281,57902.593750,56141.906250,56631.078125,38177405335 124 | 1663538400,56620.273438,58973.308594,56590.871094,57200.292969,51713139031 125 | 1663624800,57214.179688,57214.179688,53191.425781,53333.539063,68564706967 126 | 1663711200,53252.164063,57911.363281,52969.054688,57424.007813,69241316747 127 | 1663797600,57441.308594,58363.316406,55382.507813,56396.515625,69523285106 128 | 1663884000,56413.953125,58606.632813,55321.847656,57356.402344,68434023376 129 | 1663970400,57352.765625,59464.613281,56975.210938,58803.777344,65382980634 130 | 1664056800,58877.390625,59210.882813,56482.003906,58232.316406,65906690347 131 | 1664143200,58250.871094,59519.355469,54071.457031,55859.796875,71776546298 132 | 1664229600,55847.242188,56872.542969,54608.652344,56704.574219,61308396325 133 | 1664316000,56714.531250,57939.363281,49150.535156,49150.535156,75215403907 134 | 1664402400,49735.433594,51330.843750,46980.019531,49716.191406,96721152926 135 | 1664488800,49682.980469,51438.117188,48868.578125,49880.535156,55737497453 136 | 1664575200,49855.496094,50639.664063,46664.140625,46760.187500,59161047474 137 | 1664661600,46716.636719,49720.042969,43963.351563,46456.058594,64047871555 138 | 1664748000,46415.898438,46623.558594,42207.289063,43537.511719,74903638450 139 | 1664834400,43488.058594,45812.457031,42367.832031,42909.402344,56187365084 140 | 1664920800,42944.976563,43546.117188,30681.496094,37002.441406,126358098747 141 | 1665007200,36753.667969,42462.984375,35050.617188,40782.738281,88281943359 142 | 1665093600,40596.949219,42172.171875,33616.453125,37304.691406,82051616861 143 | 1665180000,37371.031250,38831.054688,35383.683594,37536.632813,57377273240 144 | 1665266400,37531.449219,38289.218750,31227.339844,34770.582031,78469274361 145 | 1665352800,34700.363281,39835.140625,34551.082031,38705.980469,67359584098 146 | 1665439200,38795.781250,39776.351563,36581.429688,38402.222656,56211915803 147 | 1665525600,38392.625000,40782.078125,37905.835938,39294.199219,51346735160 148 | 1665612000,39316.890625,40379.617188,37247.902344,38436.968750,43210968721 149 | 1665698400,38507.082031,38856.968750,34779.039063,35697.605469,55200191952 150 | 1665784800,35684.156250,37234.500000,33693.929688,34616.066406,45231013335 151 | 1665871200,34607.406250,36400.667969,33520.738281,35678.128906,31646080921 152 | 1665957600,35658.593750,37468.250000,34241.945313,37332.855469,39009847639 153 | 1666044000,37293.792969,37896.734375,35787.085938,36684.925781,34639423297 154 | 1666130400,36699.921875,38231.339844,35966.308594,37575.179688,33070867190 155 | 1666216800,37599.410156,39478.953125,37243.972656,39208.765625,35460750427 156 | 1666303200,39242.484375,39242.484375,35717.722656,36894.406250,41831090187 157 | 1666389600,36880.156250,37917.714844,34900.414063,35551.957031,35959473399 158 | 1666476000,35538.609375,36436.421875,35304.578125,35862.378906,28913440585 159 | 1666562400,35835.265625,36790.570313,33480.640625,33560.707031,33683936663 160 | 1666648800,33589.519531,34017.386719,31114.443359,33472.632813,49902050442 161 | 1666735200,33416.976563,37537.371094,32475.865234,37345.121094,53972919008 162 | 1666821600,37389.515625,38334.324219,35847.593750,36702.597656,43576032854 163 | 1666908000,36697.031250,37608.695313,36044.449219,37334.398438,38699736985 164 | 1666994400,37340.144531,37408.925781,34728.191406,35552.515625,37924228550 165 | 1667080800,35555.789063,39322.781250,34864.109375,39097.859375,40669112838 166 | 1667167200,39016.968750,40978.363281,38757.285156,40218.476563,43148914673 167 | 1667253600,40427.167969,41295.269531,39609.468750,40406.269531,46420149185 168 | 1667340000,40168.691406,40516.777344,38176.035156,38347.062500,39211635100 169 | 1667426400,38341.421875,39513.671875,37439.675781,38053.503906,37096670047 170 | 1667512800,38099.476563,38187.261719,35255.855469,35787.246094,36200887275 171 | 1667599200,35854.527344,36457.796875,34933.062500,35615.871094,31207279719 172 | 1667685600,35563.140625,36059.484375,33432.074219,35698.296875,36664034054 173 | 1667772000,35641.144531,35721.640625,31295.935547,31676.693359,52809038594 174 | 1667858400,31622.376953,33292.453125,28893.621094,32505.660156,58964353058 175 | 1667944800,32515.714844,34753.410156,31772.632813,33723.027344,46317108925 176 | 1668031200,33682.800781,35228.851563,32385.214844,34662.437500,33123368116 177 | 1668117600,34659.105469,35487.246094,31350.884766,31637.779297,40230904226 178 | 1668204000,31594.664063,32637.587891,30184.501953,32186.277344,38585385521 179 | 1668290400,32287.523438,34656.128906,32071.757813,34649.644531,35511640894 180 | 1668376800,34679.121094,35219.890625,33902.074219,34434.335938,33892523752 181 | 1668463200,34475.558594,36542.109375,34252.484375,35867.777344,37901460044 182 | 1668549600,35908.386719,36074.757813,34086.152344,35040.835938,34059036099 183 | 1668636000,35035.984375,35035.984375,32883.781250,33572.117188,37838957079 184 | 1668722400,33549.601563,33939.589844,32770.679688,33897.046875,38728974942 185 | 1668808800,33854.421875,34909.261719,33402.695313,34668.546875,24383958643 186 | 1668895200,34665.566406,35937.566406,34396.476563,35287.781250,24924307911 187 | 1668981600,35284.343750,35284.343750,33213.660156,33746.003906,26721554282 188 | 1669068000,33723.507813,35038.535156,33599.917969,34235.195313,26501259870 189 | 1669154400,34225.679688,34997.664063,33839.289063,33855.328125,24796027477 190 | 1669240800,33889.605469,33907.906250,32133.183594,32877.371094,29910396946 191 | 1669327200,32861.671875,34042.292969,32318.880859,33798.011719,27436021028 192 | 1669413600,33811.242188,34209.070313,33116.011719,33520.519531,22971873468 193 | 1669500000,33509.078125,34584.703125,33346.738281,34240.187500,20108729370 194 | 1669586400,34254.015625,34592.156250,32697.308594,33155.847656,24321499537 195 | 1669672800,33125.468750,33327.101563,32261.419922,32702.025391,19120856669 196 | 1669759200,32723.845703,33061.398438,31639.125000,32822.347656,21376531210 197 | 1669845600,32827.875000,33159.640625,31175.708984,31780.730469,21300524237 198 | 1669932000,31841.550781,32218.406250,31100.673828,31421.539063,23699476918 199 | 1670018400,31397.308594,31935.945313,31223.990234,31533.068359,18895018942 200 | 1670104800,31533.884766,32398.996094,31215.492188,31796.810547,18787986667 201 | 1670191200,31800.011719,31885.859375,30563.734375,30817.832031,20434789545 202 | 1670277600,30838.285156,31006.187500,29360.955078,29807.347656,23148267245 203 | 1670364000,29796.285156,32752.326172,29526.183594,32110.693359,28203024559 204 | 1670450400,32138.873047,32576.400391,31745.298828,32313.105469,19555230518 205 | 1670536800,32305.958984,33581.550781,32057.892578,33581.550781,22552046192 206 | 1670623200,33593.730469,34490.390625,33424.859375,34292.445313,21664706865 207 | 1670709600,34290.292969,35364.925781,33881.835938,35350.187500,20856685287 208 | 1670796000,35384.031250,40499.675781,35287.312500,37337.535156,51022126212 209 | 1670882400,37276.035156,39406.941406,36441.726563,39406.941406,35097370560 210 | 1670968800,39503.187500,40816.070313,38862.437500,39995.906250,38702404695 211 | 1671055200,39995.453125,40593.070313,39352.058594,40008.421875,27167146027 212 | 1671141600,40027.484375,42235.546875,38397.355469,42235.546875,33072782960 213 | 1671228000,42196.304688,42231.449219,41110.832031,41626.195313,25802845343 214 | 1671314400,41460.843750,42541.679688,39540.941406,39974.894531,26688438115 215 | 1671400800,39907.261719,40419.179688,38746.347656,39201.945313,25595265436 216 | 1671487200,39178.402344,39750.031250,37782.050781,38152.980469,26189830450 217 | 1671573600,38213.332031,39952.296875,37589.164063,39747.503906,25372562724 218 | 1671660000,39744.515625,41341.933594,37458.003906,40869.554688,35185031017 219 | 1671746400,40865.867188,43271.660156,39932.179688,42816.500000,38226483046 220 | 1671832800,42832.796875,44689.859375,42618.566406,44555.800781,40030862141 221 | 1671919200,44574.437500,45282.351563,43331.910156,43798.117188,36302664750 222 | 1672005600,43791.925781,46456.832031,42848.687500,46365.402344,38734079049 223 | 1672092000,46280.847656,46637.988281,44705.554688,45585.031250,33546019517 224 | 1672178400,45599.703125,46735.632813,45351.710938,45593.636719,34319709073 225 | 1672264800,45576.878906,46228.910156,43861.445313,44428.289063,33723620826 226 | 1672351200,44439.691406,47831.976563,44282.417969,47793.320313,31744259539 227 | 1672437600,47810.687500,48098.683594,46177.632813,47096.945313,31211354442 228 | 1672524000,47096.667969,47357.105469,45579.589844,47047.003906,30988958446 229 | 1672610400,47019.960938,47998.097656,45700.320313,46004.484375,32776876610 230 | 1672696800,45936.457031,47139.570313,44512.417969,44695.359375,33451362600 231 | 1672783200,44686.750000,45952.062500,44364.027344,44801.187500,32194123075 232 | 1672869600,44741.882813,46970.761719,43998.316406,46717.578125,37204312299 233 | 1672956000,46723.121094,49342.152344,46650.707031,49339.175781,34706867452 234 | 1673042400,49327.074219,49717.019531,48312.199219,48905.492188,40585205312 235 | 1673128800,48869.105469,49471.609375,48199.941406,49321.652344,25370975378 236 | 1673215200,49291.675781,50482.078125,49074.605469,49546.148438,34305053719 237 | 1673301600,49562.347656,49878.769531,47687.117188,47706.117188,35361168834 238 | 1673388000,47727.257813,49202.878906,47163.613281,48960.789063,32646349931 239 | 1673474400,49002.640625,49347.582031,46405.781250,46942.218750,32666549568 240 | 1673560800,46894.554688,49112.785156,46394.281250,49058.667969,34511076995 241 | 1673647200,49072.585938,49283.503906,48499.238281,48902.402344,28568103401 242 | 1673733600,48911.250000,49644.113281,47925.855469,48829.832031,25889650240 243 | 1673820000,48834.851563,48925.605469,46950.273438,47054.984375,31847007016 244 | 1673906400,47024.339844,48189.550781,46750.093750,47166.687500,34730363427 245 | 1673992800,47099.773438,49111.089844,46562.437500,48847.027344,39139399125 246 | 1674079200,48807.847656,50343.421875,48652.320313,49327.722656,39508070319 247 | 1674165600,49288.250000,50982.273438,48386.085938,50025.375000,43206179619 248 | 1674252000,50009.324219,50545.582031,49548.781250,49944.625000,37471327794 249 | 1674338400,49937.859375,51868.679688,49538.597656,51753.410156,30322676319 250 | 1674424800,51769.003906,52700.941406,51053.679688,52633.535156,38884105426 251 | 1674511200,52660.480469,52853.765625,43285.207031,46811.128906,65210059683 252 | 1674597600,46827.761719,47334.054688,44561.394531,46091.390625,49007762488 253 | 1674684000,45774.742188,47261.949219,45669.738281,46391.421875,38672657013 254 | 1674770400,46396.664063,47031.742188,44344.484375,44883.910156,39154666597 255 | 1674856800,44869.839844,45969.292969,44818.265625,45201.457031,34499835245 256 | 1674943200,45206.628906,46364.878906,44790.460938,46063.269531,27881980161 257 | 1675029600,46057.214844,46598.679688,43591.320313,44963.074219,40969943253 258 | 1675116000,44960.050781,47218.125000,44752.332031,47092.492188,38652152880 259 | 1675202400,47098.000000,48450.468750,46773.328125,48176.347656,30484496466 260 | 1675288800,48158.906250,48486.828125,47079.558594,47783.359375,31764293754 261 | 1675375200,47771.003906,48160.921875,46832.523438,47267.519531,28727713711 262 | 1675461600,47273.527344,48791.781250,47087.285156,48278.363281,28575630451 263 | 1675548000,48268.855469,48328.367188,46919.804688,47260.218750,26967722648 264 | 1675634400,47261.406250,47328.199219,42598.914063,42843.800781,43909845642 265 | 1675720800,43012.234375,43607.609375,39787.609375,40693.675781,48701090088 266 | 1675807200,40677.953125,43978.621094,40625.632813,43574.507813,38139709246 267 | 1675893600,43560.296875,44942.175781,43109.339844,44895.097656,34244064430 268 | 1675980000,44894.300781,45080.492188,40936.558594,42839.750000,42839345714 269 | 1676066400,42840.890625,42996.257813,41759.921875,42716.593750,31604717236 270 | 1676152800,42721.628906,43919.300781,40848.460938,43208.539063,30661222077 271 | 1676239200,43234.183594,44313.246094,42190.632813,42235.730469,30980029059 272 | 1676325600,42200.898438,42775.144531,40931.664063,41034.542969,30214940550 273 | 1676412000,41064.984375,42545.257813,40829.667969,41564.363281,30602359905 274 | 1676498400,41551.269531,44092.601563,41444.582031,43790.894531,31141681925 275 | 1676584800,43816.742188,48436.011719,43320.023438,48116.941406,42850641582 276 | 1676671200,48137.468750,48282.062500,47465.496094,47711.488281,30614346492 277 | 1676757600,47680.027344,49130.691406,47157.289063,48199.953125,26638115879 278 | 1676844000,48208.906250,49456.777344,47045.003906,49112.902344,33383173002 279 | 1676930400,49174.960938,51839.984375,49072.839844,51514.812500,35873904236 280 | 1677016800,51486.664063,55568.464844,50488.191406,55361.449219,49034730168 281 | 1677103200,55338.625000,55338.625000,53525.468750,53805.984375,36807860413 282 | 1677189600,53802.144531,55922.980469,53688.054688,53967.847656,34800873924 283 | 1677276000,53929.781250,55397.945313,53735.144531,54968.222656,32491211414 284 | 1677362400,54952.820313,56401.304688,54264.257813,54771.578125,39527792364 285 | 1677448800,54734.125000,57793.039063,54519.765625,57484.789063,42637331698 286 | 1677535200,57526.832031,57627.878906,54477.972656,56041.058594,41083758949 287 | 1677621600,56038.257813,57688.660156,54370.972656,57401.097656,41684252783 288 | 1677708000,57372.832031,58478.734375,56957.074219,57321.523438,36615791366 289 | 1677794400,57345.902344,62757.128906,56868.144531,61593.949219,51780081801 290 | 1677880800,61609.527344,62274.476563,60206.121094,60892.179688,34250964237 291 | 1677967200,60887.652344,61645.523438,59164.468750,61553.617188,29032367511 292 | 1678053600,61548.804688,62614.660156,60012.757813,62026.078125,38055562075 293 | 1678140000,62043.164063,64434.535156,61622.933594,64261.992188,40471196346 294 | 1678226400,64284.585938,66930.390625,63610.675781,65992.835938,40788955582 295 | 1678312800,66002.234375,66600.546875,62117.410156,62210.171875,45908121370 296 | 1678399200,62237.890625,63715.023438,60122.796875,60692.265625,38434082775 297 | 1678485600,60694.628906,61743.878906,59826.523438,61393.617188,26882546034 298 | 1678572000,61368.343750,61505.804688,59643.343750,60930.835938,27316183882 299 | 1678658400,60893.925781,63729.324219,60691.800781,63039.824219,31064911614 300 | 1678744800,63032.761719,63229.027344,59991.160156,60363.792969,34878965587 301 | 1678831200,60352.000000,61435.183594,58208.187500,58482.386719,43657076893 302 | 1678917600,58470.730469,62128.632813,58206.917969,60622.136719,45257083247 303 | 1679004000,60624.871094,62927.609375,60329.964844,62227.964844,36856881767 304 | 1679090400,62239.363281,62330.144531,60918.386719,61888.832031,32157938616 305 | 1679176800,61850.488281,62406.171875,60074.328125,61318.957031,32241199927 306 | 1679263200,61320.449219,62419.003906,59695.183594,61004.406250,36150572843 307 | 1679349600,60963.253906,64242.792969,60673.054688,63226.402344,37746665647 308 | 1679436000,63254.335938,63516.937500,61184.238281,62970.046875,36124731509 309 | 1679522400,62941.804688,63123.289063,60799.664063,61452.230469,32615846901 310 | 1679608800,61460.078125,62541.468750,60844.609375,61125.675781,30605102446 311 | 1679695200,61068.875000,61590.683594,60163.781250,61527.480469,29094934221 312 | 1679781600,61554.921875,63326.988281,61432.488281,63326.988281,24726754302 313 | 1679868000,63344.066406,67673.742188,63344.066406,67566.828125,41125608330 314 | --------------------------------------------------------------------------------